From ea8a06378d6733c0bcac28bed3aa3baef1e30ead Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Fri, 16 Jun 2023 14:49:19 +0200 Subject: [PATCH] chore: add non-interface modules to monorepo (#1828) Adds all supported non-interface modules to the monorepo. --- .github/workflows/interop-test.yml | 5 - .github/workflows/main.yml | 20 +- .release-please-manifest.json | 27 +- .release-please.json | 24 +- interop/BrowserDockerfile | 2 +- interop/Dockerfile | 8 +- interop/LICENSE | 4 + interop/LICENSE-APACHE | 5 + interop/LICENSE-MIT | 19 + interop/README.md | 36 + interop/package.json | 58 +- interop/test/ping.spec.ts | 6 + interop/tsconfig.json | 38 +- package.json | 1 + packages/crypto/.aegir.js | 7 + packages/crypto/CHANGELOG.md | 738 +++ packages/crypto/LICENSE | 4 + packages/crypto/LICENSE-APACHE | 5 + packages/crypto/LICENSE-MIT | 19 + packages/crypto/README.md | 330 ++ packages/crypto/benchmark/ed25519/compat.cjs | 201 + packages/crypto/benchmark/ed25519/index.js | 138 + .../crypto/benchmark/ed25519/package.json | 20 + packages/crypto/benchmark/ephemeral-keys.cjs | 22 + packages/crypto/benchmark/key-stretcher.cjs | 32 + packages/crypto/benchmark/rsa.cjs | 36 + packages/crypto/package.json | 119 + packages/crypto/src/aes/cipher-mode.ts | 15 + packages/crypto/src/aes/ciphers-browser.ts | 32 + packages/crypto/src/aes/ciphers.ts | 4 + packages/crypto/src/aes/index.ts | 25 + .../crypto/src/ciphers/aes-gcm.browser.ts | 109 + packages/crypto/src/ciphers/aes-gcm.ts | 102 + packages/crypto/src/ciphers/interface.ts | 15 + packages/crypto/src/hmac/index-browser.ts | 35 + packages/crypto/src/hmac/index.ts | 20 + packages/crypto/src/hmac/lengths.ts | 6 + packages/crypto/src/index.ts | 11 + packages/crypto/src/keys/ecdh-browser.ts | 137 + packages/crypto/src/keys/ecdh.ts | 33 + packages/crypto/src/keys/ed25519-browser.ts | 64 + packages/crypto/src/keys/ed25519-class.ts | 146 + packages/crypto/src/keys/ed25519.ts | 137 + packages/crypto/src/keys/ephemeral-keys.ts | 9 + packages/crypto/src/keys/exporter.ts | 14 + packages/crypto/src/keys/importer.ts | 13 + packages/crypto/src/keys/index.ts | 129 + packages/crypto/src/keys/interface.ts | 35 + packages/crypto/src/keys/jwk2pem.ts | 21 + packages/crypto/src/keys/key-stretcher.ts | 78 + packages/crypto/src/keys/keys.proto | 33 + packages/crypto/src/keys/keys.ts | 156 + packages/crypto/src/keys/rsa-browser.ts | 157 + packages/crypto/src/keys/rsa-class.ts | 156 + packages/crypto/src/keys/rsa-utils.ts | 74 + packages/crypto/src/keys/rsa.ts | 69 + packages/crypto/src/keys/secp256k1-class.ts | 119 + packages/crypto/src/keys/secp256k1.ts | 69 + packages/crypto/src/pbkdf2.ts | 39 + packages/crypto/src/random-bytes.ts | 9 + packages/crypto/src/util.ts | 42 + packages/crypto/src/webcrypto.ts | 24 + packages/crypto/stats.md | 153 + packages/crypto/test/aes/aes.spec.ts | 105 + packages/crypto/test/crypto.spec.ts | 155 + packages/crypto/test/fixtures/aes.ts | 36 + packages/crypto/test/fixtures/go-aes.ts | 19 + .../crypto/test/fixtures/go-elliptic-key.ts | 12 + .../crypto/test/fixtures/go-key-ed25519.ts | 41 + packages/crypto/test/fixtures/go-key-rsa.ts | 30 + .../crypto/test/fixtures/go-key-secp256k1.ts | 29 + .../crypto/test/fixtures/go-stretch-key.ts | 30 + packages/crypto/test/fixtures/secp256k1.ts | 8 + .../helpers/test-garbage-error-handling.ts | 28 + packages/crypto/test/hmac/hmac.spec.ts | 17 + packages/crypto/test/keys/ed25519.spec.ts | 224 + .../crypto/test/keys/ephemeral-keys.spec.ts | 62 + packages/crypto/test/keys/importer.spec.ts | 20 + .../crypto/test/keys/key-stretcher.spec.ts | 59 + packages/crypto/test/keys/rsa.spec.ts | 414 ++ packages/crypto/test/keys/secp256k1.spec.ts | 216 + packages/crypto/test/random-bytes.spec.ts | 22 + packages/crypto/test/util.spec.ts | 34 + packages/crypto/test/workaround.spec.ts | 26 + packages/crypto/tsconfig.json | 18 + .../interface-address-manager/package.json | 89 +- .../interface-compliance-tests/package.json | 85 - .../package.json | 85 - .../package.json | 91 +- .../tsconfig.json | 3 + .../package.json | 87 +- .../interface-connection-gater/package.json | 89 +- .../interface-connection-manager/package.json | 91 +- .../tsconfig.json | 3 + packages/interface-connection/package.json | 89 +- .../interface-content-routing/package.json | 89 +- packages/interface-dht/package.json | 89 +- packages/interface-keychain/package.json | 89 +- packages/interface-keys/package.json | 87 +- packages/interface-libp2p/package.json | 89 +- packages/interface-metrics/package.json | 87 +- packages/interface-mocks/package.json | 97 +- packages/interface-mocks/tsconfig.json | 15 + .../package.json | 87 +- .../interface-peer-discovery/package.json | 87 +- packages/interface-peer-id/package.json | 89 +- packages/interface-peer-info/package.json | 89 +- packages/interface-peer-routing/package.json | 87 +- packages/interface-peer-store/package.json | 89 +- .../package.json | 89 +- .../tsconfig.json | 3 + packages/interface-pubsub/package.json | 87 +- .../package.json | 85 - packages/interface-record/package.json | 87 +- packages/interface-registrar/package.json | 87 +- .../package.json | 89 +- packages/interface-stream-muxer/package.json | 89 +- packages/interface-stream-muxer/tsconfig.json | 3 + .../package.json | 91 +- packages/interface-transport/package.json | 89 +- packages/interfaces/package.json | 87 +- packages/kad-dht/.aegir.js | 7 + packages/kad-dht/CHANGELOG.md | 1654 +++++++ packages/kad-dht/LICENSE | 4 + packages/kad-dht/LICENSE-APACHE | 5 + packages/kad-dht/LICENSE-MIT | 19 + packages/kad-dht/README.md | 103 + packages/kad-dht/package.json | 132 + packages/kad-dht/src/constants.ts | 57 + .../kad-dht/src/content-fetching/index.ts | 263 ++ packages/kad-dht/src/content-routing/index.ts | 205 + packages/kad-dht/src/dual-kad-dht.ts | 400 ++ packages/kad-dht/src/index.ts | 321 ++ packages/kad-dht/src/kad-dht.ts | 364 ++ packages/kad-dht/src/message/dht.proto | 75 + packages/kad-dht/src/message/dht.ts | 331 ++ packages/kad-dht/src/message/index.ts | 110 + packages/kad-dht/src/network.ts | 206 + packages/kad-dht/src/peer-list/index.ts | 54 + .../src/peer-list/peer-distance-list.ts | 92 + packages/kad-dht/src/peer-routing/index.ts | 317 ++ packages/kad-dht/src/providers.ts | 286 ++ packages/kad-dht/src/query-self.ts | 163 + packages/kad-dht/src/query/events.ts | 149 + packages/kad-dht/src/query/manager.ts | 227 + packages/kad-dht/src/query/query-path.ts | 254 + packages/kad-dht/src/query/types.ts | 22 + .../generated-prefix-list-browser.ts | 1026 +++++ .../routing-table/generated-prefix-list.ts | 4098 +++++++++++++++++ packages/kad-dht/src/routing-table/index.ts | 333 ++ .../kad-dht/src/routing-table/k-bucket.ts | 523 +++ packages/kad-dht/src/routing-table/refresh.ts | 257 ++ .../kad-dht/src/rpc/handlers/add-provider.ts | 63 + .../kad-dht/src/rpc/handlers/find-node.ts | 72 + .../kad-dht/src/rpc/handlers/get-providers.ts | 105 + .../kad-dht/src/rpc/handlers/get-value.ts | 131 + packages/kad-dht/src/rpc/handlers/ping.ts | 13 + .../kad-dht/src/rpc/handlers/put-value.ts | 58 + packages/kad-dht/src/rpc/index.ts | 115 + packages/kad-dht/src/topology-listener.ts | 77 + packages/kad-dht/src/utils.ts | 151 + .../kad-dht/test/enable-server-mode.spec.ts | 74 + packages/kad-dht/test/fixtures/msg-1 | Bin 0 -> 14 bytes packages/kad-dht/test/fixtures/msg-2 | Bin 0 -> 69 bytes packages/kad-dht/test/fixtures/msg-3 | Bin 0 -> 14 bytes packages/kad-dht/test/fixtures/msg-4 | Bin 0 -> 40 bytes packages/kad-dht/test/fixtures/msg-5 | Bin 0 -> 40 bytes packages/kad-dht/test/fixtures/msg-6 | Bin 0 -> 40 bytes packages/kad-dht/test/fixtures/msg-7 | Bin 0 -> 88 bytes packages/kad-dht/test/fixtures/msg-8 | Bin 0 -> 88 bytes .../kad-dht/test/generate-peers/.gitignore | 1 + .../test/generate-peers/generate-peer.go | 85 + .../generate-peers/generate-peers.node.ts | 101 + packages/kad-dht/test/kad-dht.spec.ts | 889 ++++ packages/kad-dht/test/kad-utils.spec.ts | 98 + packages/kad-dht/test/message.node.ts | 31 + packages/kad-dht/test/message.spec.ts | 86 + packages/kad-dht/test/multiple-nodes.spec.ts | 108 + packages/kad-dht/test/network.spec.ts | 113 + .../kad-dht/test/peer-distance-list.spec.ts | 113 + packages/kad-dht/test/peer-list.spec.ts | 26 + packages/kad-dht/test/providers.node.ts | 63 + packages/kad-dht/test/providers.spec.ts | 113 + packages/kad-dht/test/query-self.spec.ts | 128 + packages/kad-dht/test/query.spec.ts | 851 ++++ packages/kad-dht/test/routing-table.spec.ts | 345 ++ .../test/rpc/handlers/add-provider.spec.ts | 85 + .../test/rpc/handlers/find-node.spec.ts | 166 + .../test/rpc/handlers/get-providers.spec.ts | 112 + .../test/rpc/handlers/get-value.spec.ts | 148 + .../kad-dht/test/rpc/handlers/ping.spec.ts | 31 + .../test/rpc/handlers/put-value.spec.ts | 80 + packages/kad-dht/test/rpc/index.node.ts | 114 + packages/kad-dht/test/utils/create-peer-id.ts | 20 + packages/kad-dht/test/utils/create-values.ts | 22 + packages/kad-dht/test/utils/index.ts | 11 + .../kad-dht/test/utils/sort-closest-peers.ts | 28 + packages/kad-dht/test/utils/test-dht.ts | 178 + packages/kad-dht/tsconfig.json | 78 + packages/keychain/CHANGELOG.md | 204 + packages/keychain/LICENSE | 4 + packages/keychain/LICENSE-APACHE | 5 + packages/keychain/LICENSE-MIT | 19 + packages/keychain/README.md | 96 + packages/keychain/doc/private-key.png | Bin 0 -> 25518 bytes packages/keychain/doc/private-key.xml | 1 + packages/keychain/package.json | 76 + packages/keychain/src/errors.ts | 18 + packages/keychain/src/index.ts | 579 +++ packages/keychain/src/util.ts | 16 + packages/keychain/test/keychain.spec.ts | 552 +++ packages/keychain/test/peerid.spec.ts | 76 + packages/keychain/tsconfig.json | 35 + packages/libp2p/package.json | 48 +- packages/libp2p/tsconfig.json | 54 + packages/logger/CHANGELOG.md | 184 + packages/logger/LICENSE | 4 + packages/logger/LICENSE-APACHE | 5 + packages/logger/LICENSE-MIT | 19 + packages/logger/README.md | 77 + packages/logger/package.json | 67 + packages/logger/src/index.ts | 90 + packages/logger/test/index.spec.ts | 134 + packages/logger/tsconfig.json | 18 + packages/metrics-prometheus/.aegir.js | 6 + packages/metrics-prometheus/CHANGELOG.md | 86 + packages/metrics-prometheus/LICENSE | 4 + packages/metrics-prometheus/LICENSE-APACHE | 5 + packages/metrics-prometheus/LICENSE-MIT | 19 + packages/metrics-prometheus/README.md | 97 + packages/metrics-prometheus/package.json | 64 + .../metrics-prometheus/src/counter-group.ts | 58 + packages/metrics-prometheus/src/counter.ts | 50 + packages/metrics-prometheus/src/index.ts | 357 ++ .../metrics-prometheus/src/metric-group.ts | 78 + packages/metrics-prometheus/src/metric.ts | 62 + packages/metrics-prometheus/src/utils.ts | 18 + .../test/counter-groups.spec.ts | 120 + .../metrics-prometheus/test/counters.spec.ts | 85 + .../test/custom-registry.spec.ts | 23 + .../test/fixtures/random-metric-name.ts | 7 + .../test/metric-groups.spec.ts | 167 + .../metrics-prometheus/test/metrics.spec.ts | 114 + .../metrics-prometheus/test/streams.spec.ts | 167 + .../metrics-prometheus/test/utils.spec.ts | 12 + packages/metrics-prometheus/tsconfig.json | 27 + packages/multistream-select/CHANGELOG.md | 200 + packages/multistream-select/LICENSE | 4 + packages/multistream-select/LICENSE-APACHE | 5 + packages/multistream-select/LICENSE-MIT | 19 + packages/multistream-select/README.md | 68 + packages/multistream-select/package.json | 82 + packages/multistream-select/src/constants.ts | 6 + packages/multistream-select/src/handle.ts | 92 + packages/multistream-select/src/index.ts | 25 + .../multistream-select/src/multistream.ts | 100 + packages/multistream-select/src/select.ts | 161 + .../multistream-select/test/dialer.spec.ts | 139 + .../test/integration.spec.ts | 120 + .../multistream-select/test/listener.spec.ts | 154 + .../test/multistream.spec.ts | 132 + packages/multistream-select/tsconfig.json | 18 + packages/peer-collections/CHANGELOG.md | 114 + packages/peer-collections/LICENSE | 4 + packages/peer-collections/LICENSE-APACHE | 5 + packages/peer-collections/LICENSE-MIT | 19 + packages/peer-collections/README.md | 52 + packages/peer-collections/package.json | 61 + packages/peer-collections/src/index.ts | 3 + packages/peer-collections/src/list.ts | 154 + packages/peer-collections/src/map.ts | 90 + packages/peer-collections/src/set.ts | 124 + packages/peer-collections/src/util.ts | 31 + packages/peer-collections/test/list.spec.ts | 35 + packages/peer-collections/test/map.spec.ts | 30 + packages/peer-collections/test/set.spec.ts | 110 + packages/peer-collections/tsconfig.json | 21 + .../peer-discovery-bootstrap/CHANGELOG.md | 477 ++ packages/peer-discovery-bootstrap/LICENSE | 4 + .../peer-discovery-bootstrap/LICENSE-APACHE | 5 + packages/peer-discovery-bootstrap/LICENSE-MIT | 19 + packages/peer-discovery-bootstrap/README.md | 103 + .../peer-discovery-bootstrap/package.json | 69 + .../peer-discovery-bootstrap/src/index.ts | 164 + .../test/bootstrap.spec.ts | 125 + .../test/compliance.spec.ts | 23 + .../test/fixtures/default-peers.ts | 10 + .../test/fixtures/some-invalid-peers.ts | 9 + .../peer-discovery-bootstrap/tsconfig.json | 38 + packages/peer-discovery-mdns/.aegir.js | 8 + packages/peer-discovery-mdns/CHANGELOG.md | 500 ++ packages/peer-discovery-mdns/LICENSE | 4 + packages/peer-discovery-mdns/LICENSE-APACHE | 5 + packages/peer-discovery-mdns/LICENSE-MIT | 19 + packages/peer-discovery-mdns/README.md | 113 + packages/peer-discovery-mdns/package.json | 69 + packages/peer-discovery-mdns/src/index.ts | 175 + packages/peer-discovery-mdns/src/query.ts | 111 + packages/peer-discovery-mdns/src/utils.ts | 9 + .../test/compliance.spec.ts | 53 + .../test/multicast-dns.spec.ts | 222 + packages/peer-discovery-mdns/tsconfig.json | 39 + packages/peer-id-factory/CHANGELOG.md | 214 + packages/peer-id-factory/LICENSE | 4 + packages/peer-id-factory/LICENSE-APACHE | 5 + packages/peer-id-factory/LICENSE-MIT | 19 + packages/peer-id-factory/README.md | 68 + packages/peer-id-factory/package.json | 71 + packages/peer-id-factory/src/index.ts | 91 + packages/peer-id-factory/src/proto.proto | 7 + packages/peer-id-factory/src/proto.ts | 83 + .../test/fixtures/go-private-key.ts | 5 + .../test/fixtures/sample-id.ts | 7 + packages/peer-id-factory/test/index.spec.ts | 313 ++ packages/peer-id-factory/tsconfig.json | 24 + packages/peer-id/CHANGELOG.md | 236 + packages/peer-id/LICENSE | 4 + packages/peer-id/LICENSE-APACHE | 5 + packages/peer-id/LICENSE-MIT | 19 + packages/peer-id/README.md | 62 + packages/peer-id/package.json | 62 + packages/peer-id/src/index.ts | 272 ++ packages/peer-id/test/index.spec.ts | 102 + packages/peer-id/tsconfig.json | 18 + packages/peer-record/CHANGELOG.md | 257 ++ packages/peer-record/LICENSE | 4 + packages/peer-record/LICENSE-APACHE | 5 + packages/peer-record/LICENSE-MIT | 19 + packages/peer-record/README.md | 187 + packages/peer-record/package.json | 80 + .../peer-record/src/envelope/envelope.proto | 19 + packages/peer-record/src/envelope/envelope.ts | 97 + packages/peer-record/src/envelope/index.ts | 162 + packages/peer-record/src/errors.ts | 4 + packages/peer-record/src/index.ts | 5 + .../peer-record/src/peer-record/consts.ts | 8 + packages/peer-record/src/peer-record/index.ts | 104 + .../src/peer-record/peer-record.proto | 18 + .../src/peer-record/peer-record.ts | 147 + packages/peer-record/test/envelope.spec.ts | 89 + packages/peer-record/test/peer-record.spec.ts | 156 + packages/peer-record/tsconfig.json | 36 + packages/peer-store/CHANGELOG.md | 394 ++ packages/peer-store/LICENSE | 4 + packages/peer-store/LICENSE-APACHE | 5 + packages/peer-store/LICENSE-MIT | 19 + packages/peer-store/README.md | 49 + packages/peer-store/package.json | 87 + packages/peer-store/src/errors.ts | 4 + packages/peer-store/src/index.ts | 215 + packages/peer-store/src/pb/peer.proto | 34 + packages/peer-store/src/pb/peer.ts | 396 ++ packages/peer-store/src/store.ts | 187 + .../peer-store/src/utils/bytes-to-peer.ts | 43 + .../peer-store/src/utils/dedupe-addresses.ts | 51 + .../src/utils/peer-data-to-datastore-peer.ts | 116 + .../src/utils/peer-id-to-datastore-key.ts | 15 + packages/peer-store/src/utils/to-peer-pb.ts | 237 + packages/peer-store/test/index.spec.ts | 287 ++ packages/peer-store/test/merge.spec.ts | 247 + packages/peer-store/test/patch.spec.ts | 231 + packages/peer-store/test/save.spec.ts | 252 + .../test/utils/dedupe-addresses.spec.ts | 79 + packages/peer-store/tsconfig.json | 42 + packages/record/CHANGELOG.md | 323 ++ packages/record/LICENSE | 4 + packages/record/LICENSE-APACHE | 5 + packages/record/LICENSE-MIT | 19 + packages/record/README.md | 50 + packages/record/package.json | 94 + packages/record/src/index.ts | 70 + packages/record/src/record.proto | 20 + packages/record/src/record.ts | 87 + packages/record/src/selectors.ts | 50 + packages/record/src/utils.ts | 46 + packages/record/src/validators.ts | 69 + .../record/test/fixtures/go-key-records.ts | 5 + packages/record/test/fixtures/go-record.ts | 26 + packages/record/test/record.spec.ts | 49 + packages/record/test/selection.spec.ts | 73 + packages/record/test/utils.spec.ts | 47 + packages/record/test/validator.spec.ts | 137 + packages/record/tsconfig.json | 21 + packages/stream-multiplexer-mplex/.aegir.js | 7 + .../stream-multiplexer-mplex/CHANGELOG.md | 765 +++ packages/stream-multiplexer-mplex/LICENSE | 4 + .../stream-multiplexer-mplex/LICENSE-APACHE | 5 + packages/stream-multiplexer-mplex/LICENSE-MIT | 19 + packages/stream-multiplexer-mplex/README.md | 73 + .../benchmark/send-and-receive.js | 71 + .../examples/dialer.js | 45 + .../examples/listener.js | 37 + .../stream-multiplexer-mplex/examples/util.js | 17 + .../stream-multiplexer-mplex/package.json | 96 + .../src/alloc-unsafe-browser.ts | 3 + .../src/alloc-unsafe.ts | 3 + .../stream-multiplexer-mplex/src/decode.ts | 142 + .../stream-multiplexer-mplex/src/encode.ts | 84 + .../stream-multiplexer-mplex/src/index.ts | 81 + .../src/message-types.ts | 79 + .../stream-multiplexer-mplex/src/mplex.ts | 328 ++ .../stream-multiplexer-mplex/src/stream.ts | 71 + .../test/coder.spec.ts | 94 + .../test/compliance.spec.ts | 16 + .../test/fixtures/decode.ts | 19 + .../test/fixtures/utils.ts | 18 + .../test/mplex.spec.ts | 227 + .../test/restrict-size.spec.ts | 125 + .../test/stream.spec.ts | 613 +++ .../stream-multiplexer-mplex/tsconfig.json | 27 + packages/topology/CHANGELOG.md | 248 + packages/topology/LICENSE | 4 + packages/topology/LICENSE-APACHE | 5 + packages/topology/LICENSE-MIT | 19 + packages/topology/README.md | 45 + packages/topology/package.json | 74 + packages/topology/src/index.ts | 49 + packages/topology/tsconfig.json | 18 + packages/tracked-map/.gitignore | 6 + packages/tracked-map/CHANGELOG.md | 139 + packages/tracked-map/LICENSE | 4 + packages/tracked-map/LICENSE-APACHE | 5 + packages/tracked-map/LICENSE-MIT | 19 + packages/tracked-map/README.md | 63 + packages/tracked-map/package.json | 62 + packages/tracked-map/src/index.ts | 65 + packages/tracked-map/test/index.spec.ts | 93 + packages/tracked-map/tsconfig.json | 15 + packages/transport-tcp/.aegir.js | 9 + packages/transport-tcp/CHANGELOG.md | 908 ++++ packages/transport-tcp/LICENSE | 4 + packages/transport-tcp/LICENSE-APACHE | 5 + packages/transport-tcp/LICENSE-MIT | 19 + packages/transport-tcp/README.md | 87 + packages/transport-tcp/package.json | 77 + packages/transport-tcp/src/constants.ts | 10 + packages/transport-tcp/src/index.ts | 252 + packages/transport-tcp/src/listener.ts | 334 ++ packages/transport-tcp/src/socket-to-conn.ts | 196 + packages/transport-tcp/src/utils.ts | 53 + .../transport-tcp/test/compliance.spec.ts | 42 + .../transport-tcp/test/connection.spec.ts | 91 + packages/transport-tcp/test/filter.spec.ts | 42 + .../transport-tcp/test/listen-dial.spec.ts | 389 ++ .../test/max-connections-close.spec.ts | 121 + .../test/max-connections.spec.ts | 82 + .../transport-tcp/test/socket-to-conn.spec.ts | 428 ++ packages/transport-tcp/tsconfig.json | 36 + packages/transport-webrtc/.aegir.js | 61 + packages/transport-webrtc/CHANGELOG.md | 242 + packages/transport-webrtc/LICENSE | 4 + packages/transport-webrtc/LICENSE-APACHE | 5 + packages/transport-webrtc/LICENSE-MIT | 19 + packages/transport-webrtc/README.md | 188 + packages/transport-webrtc/examples/README.md | 4 + .../examples/browser-to-browser/README.md | 61 + .../examples/browser-to-browser/index.html | 49 + .../examples/browser-to-browser/index.js | 133 + .../examples/browser-to-browser/package.json | 27 + .../examples/browser-to-browser/relay.js | 26 + .../browser-to-browser/tests/test.spec.js | 129 + .../browser-to-browser/vite.config.js | 11 + .../examples/browser-to-server/README.md | 34 + .../examples/browser-to-server/index.html | 41 + .../examples/browser-to-server/index.js | 62 + .../examples/browser-to-server/package.json | 25 + .../browser-to-server/tests/test.spec.js | 94 + .../examples/browser-to-server/vite.config.js | 8 + .../examples/go-libp2p-server/.gitignore | 1 + .../examples/go-libp2p-server/go.mod | 117 + .../examples/go-libp2p-server/go.sum | 1174 +++++ .../examples/go-libp2p-server/main.go | 95 + packages/transport-webrtc/package.json | 95 + packages/transport-webrtc/src/error.ts | 122 + packages/transport-webrtc/src/index.ts | 31 + packages/transport-webrtc/src/maconn.ts | 85 + packages/transport-webrtc/src/muxer.ts | 165 + .../transport-webrtc/src/pb/message.proto | 20 + packages/transport-webrtc/src/pb/message.ts | 92 + .../src/private-to-private/handler.ts | 156 + .../src/private-to-private/listener.ts | 43 + .../src/private-to-private/pb/message.proto | 16 + .../src/private-to-private/pb/message.ts | 92 + .../src/private-to-private/transport.ts | 184 + .../src/private-to-private/util.ts | 59 + .../src/private-to-public/options.ts | 4 + .../src/private-to-public/sdp.ts | 162 + .../src/private-to-public/transport.ts | 280 ++ .../src/private-to-public/util.ts | 3 + packages/transport-webrtc/src/stream.ts | 273 ++ packages/transport-webrtc/src/util.ts | 8 + packages/transport-webrtc/test/basics.spec.ts | 135 + .../transport-webrtc/test/listener.spec.ts | 40 + .../test/maconn.browser.spec.ts | 34 + .../test/peer.browser.spec.ts | 132 + packages/transport-webrtc/test/sdp.spec.ts | 81 + .../test/stream.browser.spec.ts | 150 + packages/transport-webrtc/test/stream.spec.ts | 115 + .../test/transport.browser.spec.ts | 95 + packages/transport-webrtc/test/util.ts | 9 + packages/transport-webrtc/tsconfig.json | 55 + packages/transport-websockets/.aegir.js | 46 + packages/transport-websockets/CHANGELOG.md | 713 +++ packages/transport-websockets/LICENSE | 4 + packages/transport-websockets/LICENSE-APACHE | 5 + packages/transport-websockets/LICENSE-MIT | 19 + packages/transport-websockets/README.md | 130 + packages/transport-websockets/package.json | 107 + .../transport-websockets/src/constants.ts | 10 + packages/transport-websockets/src/filters.ts | 66 + packages/transport-websockets/src/index.ts | 143 + .../src/listener.browser.ts | 5 + packages/transport-websockets/src/listener.ts | 160 + .../src/socket-to-conn.ts | 71 + packages/transport-websockets/test/browser.ts | 98 + .../test/compliance.node.ts | 60 + .../test/fixtures/certificate.pem | 13 + .../test/fixtures/key.pem | 15 + packages/transport-websockets/test/node.ts | 639 +++ packages/transport-websockets/tsconfig.json | 33 + packages/transport-webtransport/.aegir.js | 61 + packages/transport-webtransport/.gitignore | 1 + packages/transport-webtransport/CHANGELOG.md | 142 + packages/transport-webtransport/LICENSE | 4 + .../transport-webtransport/LICENSE-APACHE | 5 + packages/transport-webtransport/LICENSE-MIT | 19 + packages/transport-webtransport/README.md | 93 + .../examples/fetch-file-from-kubo/.gitignore | 24 + .../examples/fetch-file-from-kubo/README.md | 121 + .../fetch-file-from-kubo/img/img1.png | Bin 0 -> 571580 bytes .../fetch-file-from-kubo/img/img2.png | Bin 0 -> 740468 bytes .../examples/fetch-file-from-kubo/index.html | 37 + .../fetch-file-from-kubo/package.json | 26 + .../fetch-file-from-kubo/src/libp2p.ts | 27 + .../examples/fetch-file-from-kubo/src/main.ts | 64 + .../fetch-file-from-kubo/src/style.css | 109 + .../fetch-file-from-kubo/src/vite-env.d.ts | 1 + .../fetch-file-from-kubo/tests/test.spec.js | 76 + .../fetch-file-from-kubo/tsconfig.json | 20 + .../fetch-file-from-kubo/vite.config.js | 8 + .../go-libp2p-webtransport-server/go.mod | 95 + .../go-libp2p-webtransport-server/go.sum | 501 ++ .../go-libp2p-webtransport-server/main.go | 48 + packages/transport-webtransport/package.json | 90 + packages/transport-webtransport/src/index.ts | 503 ++ .../transport-webtransport/test/browser.ts | 189 + packages/transport-webtransport/tsconfig.json | 33 + packages/utils/API.md | 209 + packages/utils/CHANGELOG.md | 324 ++ packages/utils/LICENSE | 4 + packages/utils/LICENSE-APACHE | 5 + packages/utils/LICENSE-MIT | 19 + packages/utils/README.md | 65 + packages/utils/package.json | 110 + packages/utils/src/address-sort.ts | 55 + packages/utils/src/array-equals.ts | 34 + packages/utils/src/index.ts | 1 + packages/utils/src/ip-port-to-multiaddr.ts | 47 + packages/utils/src/multiaddr/is-loopback.ts | 11 + packages/utils/src/multiaddr/is-private.ts | 15 + packages/utils/src/stream-to-ma-conn.ts | 94 + packages/utils/test/address-sort.spec.ts | 51 + packages/utils/test/array-equals.spec.ts | 70 + .../utils/test/ip-port-to-multiaddr.spec.ts | 47 + .../utils/test/multiaddr/is-loopback.spec.ts | 55 + .../utils/test/multiaddr/is-private.spec.ts | 66 + packages/utils/test/stream-to-ma-conn.spec.ts | 79 + packages/utils/tsconfig.json | 24 + 568 files changed, 57362 insertions(+), 2752 deletions(-) create mode 100644 interop/LICENSE create mode 100644 interop/LICENSE-APACHE create mode 100644 interop/LICENSE-MIT create mode 100644 interop/README.md create mode 100644 packages/crypto/.aegir.js create mode 100644 packages/crypto/CHANGELOG.md create mode 100644 packages/crypto/LICENSE create mode 100644 packages/crypto/LICENSE-APACHE create mode 100644 packages/crypto/LICENSE-MIT create mode 100644 packages/crypto/README.md create mode 100644 packages/crypto/benchmark/ed25519/compat.cjs create mode 100644 packages/crypto/benchmark/ed25519/index.js create mode 100644 packages/crypto/benchmark/ed25519/package.json create mode 100644 packages/crypto/benchmark/ephemeral-keys.cjs create mode 100644 packages/crypto/benchmark/key-stretcher.cjs create mode 100644 packages/crypto/benchmark/rsa.cjs create mode 100644 packages/crypto/package.json create mode 100644 packages/crypto/src/aes/cipher-mode.ts create mode 100644 packages/crypto/src/aes/ciphers-browser.ts create mode 100644 packages/crypto/src/aes/ciphers.ts create mode 100644 packages/crypto/src/aes/index.ts create mode 100644 packages/crypto/src/ciphers/aes-gcm.browser.ts create mode 100644 packages/crypto/src/ciphers/aes-gcm.ts create mode 100644 packages/crypto/src/ciphers/interface.ts create mode 100644 packages/crypto/src/hmac/index-browser.ts create mode 100644 packages/crypto/src/hmac/index.ts create mode 100644 packages/crypto/src/hmac/lengths.ts create mode 100644 packages/crypto/src/index.ts create mode 100644 packages/crypto/src/keys/ecdh-browser.ts create mode 100644 packages/crypto/src/keys/ecdh.ts create mode 100644 packages/crypto/src/keys/ed25519-browser.ts create mode 100644 packages/crypto/src/keys/ed25519-class.ts create mode 100644 packages/crypto/src/keys/ed25519.ts create mode 100644 packages/crypto/src/keys/ephemeral-keys.ts create mode 100644 packages/crypto/src/keys/exporter.ts create mode 100644 packages/crypto/src/keys/importer.ts create mode 100644 packages/crypto/src/keys/index.ts create mode 100644 packages/crypto/src/keys/interface.ts create mode 100644 packages/crypto/src/keys/jwk2pem.ts create mode 100644 packages/crypto/src/keys/key-stretcher.ts create mode 100644 packages/crypto/src/keys/keys.proto create mode 100644 packages/crypto/src/keys/keys.ts create mode 100644 packages/crypto/src/keys/rsa-browser.ts create mode 100644 packages/crypto/src/keys/rsa-class.ts create mode 100644 packages/crypto/src/keys/rsa-utils.ts create mode 100644 packages/crypto/src/keys/rsa.ts create mode 100644 packages/crypto/src/keys/secp256k1-class.ts create mode 100644 packages/crypto/src/keys/secp256k1.ts create mode 100644 packages/crypto/src/pbkdf2.ts create mode 100644 packages/crypto/src/random-bytes.ts create mode 100644 packages/crypto/src/util.ts create mode 100644 packages/crypto/src/webcrypto.ts create mode 100644 packages/crypto/stats.md create mode 100644 packages/crypto/test/aes/aes.spec.ts create mode 100644 packages/crypto/test/crypto.spec.ts create mode 100644 packages/crypto/test/fixtures/aes.ts create mode 100644 packages/crypto/test/fixtures/go-aes.ts create mode 100644 packages/crypto/test/fixtures/go-elliptic-key.ts create mode 100644 packages/crypto/test/fixtures/go-key-ed25519.ts create mode 100644 packages/crypto/test/fixtures/go-key-rsa.ts create mode 100644 packages/crypto/test/fixtures/go-key-secp256k1.ts create mode 100644 packages/crypto/test/fixtures/go-stretch-key.ts create mode 100644 packages/crypto/test/fixtures/secp256k1.ts create mode 100644 packages/crypto/test/helpers/test-garbage-error-handling.ts create mode 100644 packages/crypto/test/hmac/hmac.spec.ts create mode 100644 packages/crypto/test/keys/ed25519.spec.ts create mode 100644 packages/crypto/test/keys/ephemeral-keys.spec.ts create mode 100644 packages/crypto/test/keys/importer.spec.ts create mode 100644 packages/crypto/test/keys/key-stretcher.spec.ts create mode 100644 packages/crypto/test/keys/rsa.spec.ts create mode 100644 packages/crypto/test/keys/secp256k1.spec.ts create mode 100644 packages/crypto/test/random-bytes.spec.ts create mode 100644 packages/crypto/test/util.spec.ts create mode 100644 packages/crypto/test/workaround.spec.ts create mode 100644 packages/crypto/tsconfig.json create mode 100644 packages/kad-dht/.aegir.js create mode 100644 packages/kad-dht/CHANGELOG.md create mode 100644 packages/kad-dht/LICENSE create mode 100644 packages/kad-dht/LICENSE-APACHE create mode 100644 packages/kad-dht/LICENSE-MIT create mode 100644 packages/kad-dht/README.md create mode 100644 packages/kad-dht/package.json create mode 100644 packages/kad-dht/src/constants.ts create mode 100644 packages/kad-dht/src/content-fetching/index.ts create mode 100644 packages/kad-dht/src/content-routing/index.ts create mode 100644 packages/kad-dht/src/dual-kad-dht.ts create mode 100644 packages/kad-dht/src/index.ts create mode 100644 packages/kad-dht/src/kad-dht.ts create mode 100644 packages/kad-dht/src/message/dht.proto create mode 100644 packages/kad-dht/src/message/dht.ts create mode 100644 packages/kad-dht/src/message/index.ts create mode 100644 packages/kad-dht/src/network.ts create mode 100644 packages/kad-dht/src/peer-list/index.ts create mode 100644 packages/kad-dht/src/peer-list/peer-distance-list.ts create mode 100644 packages/kad-dht/src/peer-routing/index.ts create mode 100644 packages/kad-dht/src/providers.ts create mode 100644 packages/kad-dht/src/query-self.ts create mode 100644 packages/kad-dht/src/query/events.ts create mode 100644 packages/kad-dht/src/query/manager.ts create mode 100644 packages/kad-dht/src/query/query-path.ts create mode 100644 packages/kad-dht/src/query/types.ts create mode 100644 packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts create mode 100644 packages/kad-dht/src/routing-table/generated-prefix-list.ts create mode 100644 packages/kad-dht/src/routing-table/index.ts create mode 100644 packages/kad-dht/src/routing-table/k-bucket.ts create mode 100644 packages/kad-dht/src/routing-table/refresh.ts create mode 100644 packages/kad-dht/src/rpc/handlers/add-provider.ts create mode 100644 packages/kad-dht/src/rpc/handlers/find-node.ts create mode 100644 packages/kad-dht/src/rpc/handlers/get-providers.ts create mode 100644 packages/kad-dht/src/rpc/handlers/get-value.ts create mode 100644 packages/kad-dht/src/rpc/handlers/ping.ts create mode 100644 packages/kad-dht/src/rpc/handlers/put-value.ts create mode 100644 packages/kad-dht/src/rpc/index.ts create mode 100644 packages/kad-dht/src/topology-listener.ts create mode 100644 packages/kad-dht/src/utils.ts create mode 100644 packages/kad-dht/test/enable-server-mode.spec.ts create mode 100755 packages/kad-dht/test/fixtures/msg-1 create mode 100755 packages/kad-dht/test/fixtures/msg-2 create mode 100755 packages/kad-dht/test/fixtures/msg-3 create mode 100755 packages/kad-dht/test/fixtures/msg-4 create mode 100755 packages/kad-dht/test/fixtures/msg-5 create mode 100755 packages/kad-dht/test/fixtures/msg-6 create mode 100755 packages/kad-dht/test/fixtures/msg-7 create mode 100755 packages/kad-dht/test/fixtures/msg-8 create mode 100644 packages/kad-dht/test/generate-peers/.gitignore create mode 100644 packages/kad-dht/test/generate-peers/generate-peer.go create mode 100644 packages/kad-dht/test/generate-peers/generate-peers.node.ts create mode 100644 packages/kad-dht/test/kad-dht.spec.ts create mode 100644 packages/kad-dht/test/kad-utils.spec.ts create mode 100644 packages/kad-dht/test/message.node.ts create mode 100644 packages/kad-dht/test/message.spec.ts create mode 100644 packages/kad-dht/test/multiple-nodes.spec.ts create mode 100644 packages/kad-dht/test/network.spec.ts create mode 100644 packages/kad-dht/test/peer-distance-list.spec.ts create mode 100644 packages/kad-dht/test/peer-list.spec.ts create mode 100644 packages/kad-dht/test/providers.node.ts create mode 100644 packages/kad-dht/test/providers.spec.ts create mode 100644 packages/kad-dht/test/query-self.spec.ts create mode 100644 packages/kad-dht/test/query.spec.ts create mode 100644 packages/kad-dht/test/routing-table.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/add-provider.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/find-node.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/get-providers.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/get-value.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/ping.spec.ts create mode 100644 packages/kad-dht/test/rpc/handlers/put-value.spec.ts create mode 100644 packages/kad-dht/test/rpc/index.node.ts create mode 100644 packages/kad-dht/test/utils/create-peer-id.ts create mode 100644 packages/kad-dht/test/utils/create-values.ts create mode 100644 packages/kad-dht/test/utils/index.ts create mode 100644 packages/kad-dht/test/utils/sort-closest-peers.ts create mode 100644 packages/kad-dht/test/utils/test-dht.ts create mode 100644 packages/kad-dht/tsconfig.json create mode 100644 packages/keychain/CHANGELOG.md create mode 100644 packages/keychain/LICENSE create mode 100644 packages/keychain/LICENSE-APACHE create mode 100644 packages/keychain/LICENSE-MIT create mode 100644 packages/keychain/README.md create mode 100644 packages/keychain/doc/private-key.png create mode 100644 packages/keychain/doc/private-key.xml create mode 100644 packages/keychain/package.json create mode 100644 packages/keychain/src/errors.ts create mode 100644 packages/keychain/src/index.ts create mode 100644 packages/keychain/src/util.ts create mode 100644 packages/keychain/test/keychain.spec.ts create mode 100644 packages/keychain/test/peerid.spec.ts create mode 100644 packages/keychain/tsconfig.json create mode 100644 packages/logger/CHANGELOG.md create mode 100644 packages/logger/LICENSE create mode 100644 packages/logger/LICENSE-APACHE create mode 100644 packages/logger/LICENSE-MIT create mode 100644 packages/logger/README.md create mode 100644 packages/logger/package.json create mode 100644 packages/logger/src/index.ts create mode 100644 packages/logger/test/index.spec.ts create mode 100644 packages/logger/tsconfig.json create mode 100644 packages/metrics-prometheus/.aegir.js create mode 100644 packages/metrics-prometheus/CHANGELOG.md create mode 100644 packages/metrics-prometheus/LICENSE create mode 100644 packages/metrics-prometheus/LICENSE-APACHE create mode 100644 packages/metrics-prometheus/LICENSE-MIT create mode 100644 packages/metrics-prometheus/README.md create mode 100644 packages/metrics-prometheus/package.json create mode 100644 packages/metrics-prometheus/src/counter-group.ts create mode 100644 packages/metrics-prometheus/src/counter.ts create mode 100644 packages/metrics-prometheus/src/index.ts create mode 100644 packages/metrics-prometheus/src/metric-group.ts create mode 100644 packages/metrics-prometheus/src/metric.ts create mode 100644 packages/metrics-prometheus/src/utils.ts create mode 100644 packages/metrics-prometheus/test/counter-groups.spec.ts create mode 100644 packages/metrics-prometheus/test/counters.spec.ts create mode 100644 packages/metrics-prometheus/test/custom-registry.spec.ts create mode 100644 packages/metrics-prometheus/test/fixtures/random-metric-name.ts create mode 100644 packages/metrics-prometheus/test/metric-groups.spec.ts create mode 100644 packages/metrics-prometheus/test/metrics.spec.ts create mode 100644 packages/metrics-prometheus/test/streams.spec.ts create mode 100644 packages/metrics-prometheus/test/utils.spec.ts create mode 100644 packages/metrics-prometheus/tsconfig.json create mode 100644 packages/multistream-select/CHANGELOG.md create mode 100644 packages/multistream-select/LICENSE create mode 100644 packages/multistream-select/LICENSE-APACHE create mode 100644 packages/multistream-select/LICENSE-MIT create mode 100644 packages/multistream-select/README.md create mode 100644 packages/multistream-select/package.json create mode 100644 packages/multistream-select/src/constants.ts create mode 100644 packages/multistream-select/src/handle.ts create mode 100644 packages/multistream-select/src/index.ts create mode 100644 packages/multistream-select/src/multistream.ts create mode 100644 packages/multistream-select/src/select.ts create mode 100644 packages/multistream-select/test/dialer.spec.ts create mode 100644 packages/multistream-select/test/integration.spec.ts create mode 100644 packages/multistream-select/test/listener.spec.ts create mode 100644 packages/multistream-select/test/multistream.spec.ts create mode 100644 packages/multistream-select/tsconfig.json create mode 100644 packages/peer-collections/CHANGELOG.md create mode 100644 packages/peer-collections/LICENSE create mode 100644 packages/peer-collections/LICENSE-APACHE create mode 100644 packages/peer-collections/LICENSE-MIT create mode 100644 packages/peer-collections/README.md create mode 100644 packages/peer-collections/package.json create mode 100644 packages/peer-collections/src/index.ts create mode 100644 packages/peer-collections/src/list.ts create mode 100644 packages/peer-collections/src/map.ts create mode 100644 packages/peer-collections/src/set.ts create mode 100644 packages/peer-collections/src/util.ts create mode 100644 packages/peer-collections/test/list.spec.ts create mode 100644 packages/peer-collections/test/map.spec.ts create mode 100644 packages/peer-collections/test/set.spec.ts create mode 100644 packages/peer-collections/tsconfig.json create mode 100644 packages/peer-discovery-bootstrap/CHANGELOG.md create mode 100644 packages/peer-discovery-bootstrap/LICENSE create mode 100644 packages/peer-discovery-bootstrap/LICENSE-APACHE create mode 100644 packages/peer-discovery-bootstrap/LICENSE-MIT create mode 100644 packages/peer-discovery-bootstrap/README.md create mode 100644 packages/peer-discovery-bootstrap/package.json create mode 100644 packages/peer-discovery-bootstrap/src/index.ts create mode 100644 packages/peer-discovery-bootstrap/test/bootstrap.spec.ts create mode 100644 packages/peer-discovery-bootstrap/test/compliance.spec.ts create mode 100644 packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts create mode 100644 packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts create mode 100644 packages/peer-discovery-bootstrap/tsconfig.json create mode 100644 packages/peer-discovery-mdns/.aegir.js create mode 100644 packages/peer-discovery-mdns/CHANGELOG.md create mode 100644 packages/peer-discovery-mdns/LICENSE create mode 100644 packages/peer-discovery-mdns/LICENSE-APACHE create mode 100644 packages/peer-discovery-mdns/LICENSE-MIT create mode 100644 packages/peer-discovery-mdns/README.md create mode 100644 packages/peer-discovery-mdns/package.json create mode 100644 packages/peer-discovery-mdns/src/index.ts create mode 100644 packages/peer-discovery-mdns/src/query.ts create mode 100644 packages/peer-discovery-mdns/src/utils.ts create mode 100644 packages/peer-discovery-mdns/test/compliance.spec.ts create mode 100644 packages/peer-discovery-mdns/test/multicast-dns.spec.ts create mode 100644 packages/peer-discovery-mdns/tsconfig.json create mode 100644 packages/peer-id-factory/CHANGELOG.md create mode 100644 packages/peer-id-factory/LICENSE create mode 100644 packages/peer-id-factory/LICENSE-APACHE create mode 100644 packages/peer-id-factory/LICENSE-MIT create mode 100644 packages/peer-id-factory/README.md create mode 100644 packages/peer-id-factory/package.json create mode 100644 packages/peer-id-factory/src/index.ts create mode 100644 packages/peer-id-factory/src/proto.proto create mode 100644 packages/peer-id-factory/src/proto.ts create mode 100644 packages/peer-id-factory/test/fixtures/go-private-key.ts create mode 100644 packages/peer-id-factory/test/fixtures/sample-id.ts create mode 100644 packages/peer-id-factory/test/index.spec.ts create mode 100644 packages/peer-id-factory/tsconfig.json create mode 100644 packages/peer-id/CHANGELOG.md create mode 100644 packages/peer-id/LICENSE create mode 100644 packages/peer-id/LICENSE-APACHE create mode 100644 packages/peer-id/LICENSE-MIT create mode 100644 packages/peer-id/README.md create mode 100644 packages/peer-id/package.json create mode 100644 packages/peer-id/src/index.ts create mode 100644 packages/peer-id/test/index.spec.ts create mode 100644 packages/peer-id/tsconfig.json create mode 100644 packages/peer-record/CHANGELOG.md create mode 100644 packages/peer-record/LICENSE create mode 100644 packages/peer-record/LICENSE-APACHE create mode 100644 packages/peer-record/LICENSE-MIT create mode 100644 packages/peer-record/README.md create mode 100644 packages/peer-record/package.json create mode 100644 packages/peer-record/src/envelope/envelope.proto create mode 100644 packages/peer-record/src/envelope/envelope.ts create mode 100644 packages/peer-record/src/envelope/index.ts create mode 100644 packages/peer-record/src/errors.ts create mode 100644 packages/peer-record/src/index.ts create mode 100644 packages/peer-record/src/peer-record/consts.ts create mode 100644 packages/peer-record/src/peer-record/index.ts create mode 100644 packages/peer-record/src/peer-record/peer-record.proto create mode 100644 packages/peer-record/src/peer-record/peer-record.ts create mode 100644 packages/peer-record/test/envelope.spec.ts create mode 100644 packages/peer-record/test/peer-record.spec.ts create mode 100644 packages/peer-record/tsconfig.json create mode 100644 packages/peer-store/CHANGELOG.md create mode 100644 packages/peer-store/LICENSE create mode 100644 packages/peer-store/LICENSE-APACHE create mode 100644 packages/peer-store/LICENSE-MIT create mode 100644 packages/peer-store/README.md create mode 100644 packages/peer-store/package.json create mode 100644 packages/peer-store/src/errors.ts create mode 100644 packages/peer-store/src/index.ts create mode 100644 packages/peer-store/src/pb/peer.proto create mode 100644 packages/peer-store/src/pb/peer.ts create mode 100644 packages/peer-store/src/store.ts create mode 100644 packages/peer-store/src/utils/bytes-to-peer.ts create mode 100644 packages/peer-store/src/utils/dedupe-addresses.ts create mode 100644 packages/peer-store/src/utils/peer-data-to-datastore-peer.ts create mode 100644 packages/peer-store/src/utils/peer-id-to-datastore-key.ts create mode 100644 packages/peer-store/src/utils/to-peer-pb.ts create mode 100644 packages/peer-store/test/index.spec.ts create mode 100644 packages/peer-store/test/merge.spec.ts create mode 100644 packages/peer-store/test/patch.spec.ts create mode 100644 packages/peer-store/test/save.spec.ts create mode 100644 packages/peer-store/test/utils/dedupe-addresses.spec.ts create mode 100644 packages/peer-store/tsconfig.json create mode 100644 packages/record/CHANGELOG.md create mode 100644 packages/record/LICENSE create mode 100644 packages/record/LICENSE-APACHE create mode 100644 packages/record/LICENSE-MIT create mode 100644 packages/record/README.md create mode 100644 packages/record/package.json create mode 100644 packages/record/src/index.ts create mode 100644 packages/record/src/record.proto create mode 100644 packages/record/src/record.ts create mode 100644 packages/record/src/selectors.ts create mode 100644 packages/record/src/utils.ts create mode 100644 packages/record/src/validators.ts create mode 100644 packages/record/test/fixtures/go-key-records.ts create mode 100644 packages/record/test/fixtures/go-record.ts create mode 100644 packages/record/test/record.spec.ts create mode 100644 packages/record/test/selection.spec.ts create mode 100644 packages/record/test/utils.spec.ts create mode 100644 packages/record/test/validator.spec.ts create mode 100644 packages/record/tsconfig.json create mode 100644 packages/stream-multiplexer-mplex/.aegir.js create mode 100644 packages/stream-multiplexer-mplex/CHANGELOG.md create mode 100644 packages/stream-multiplexer-mplex/LICENSE create mode 100644 packages/stream-multiplexer-mplex/LICENSE-APACHE create mode 100644 packages/stream-multiplexer-mplex/LICENSE-MIT create mode 100644 packages/stream-multiplexer-mplex/README.md create mode 100644 packages/stream-multiplexer-mplex/benchmark/send-and-receive.js create mode 100644 packages/stream-multiplexer-mplex/examples/dialer.js create mode 100644 packages/stream-multiplexer-mplex/examples/listener.js create mode 100644 packages/stream-multiplexer-mplex/examples/util.js create mode 100644 packages/stream-multiplexer-mplex/package.json create mode 100644 packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts create mode 100644 packages/stream-multiplexer-mplex/src/alloc-unsafe.ts create mode 100644 packages/stream-multiplexer-mplex/src/decode.ts create mode 100644 packages/stream-multiplexer-mplex/src/encode.ts create mode 100644 packages/stream-multiplexer-mplex/src/index.ts create mode 100644 packages/stream-multiplexer-mplex/src/message-types.ts create mode 100644 packages/stream-multiplexer-mplex/src/mplex.ts create mode 100644 packages/stream-multiplexer-mplex/src/stream.ts create mode 100644 packages/stream-multiplexer-mplex/test/coder.spec.ts create mode 100644 packages/stream-multiplexer-mplex/test/compliance.spec.ts create mode 100644 packages/stream-multiplexer-mplex/test/fixtures/decode.ts create mode 100644 packages/stream-multiplexer-mplex/test/fixtures/utils.ts create mode 100644 packages/stream-multiplexer-mplex/test/mplex.spec.ts create mode 100644 packages/stream-multiplexer-mplex/test/restrict-size.spec.ts create mode 100644 packages/stream-multiplexer-mplex/test/stream.spec.ts create mode 100644 packages/stream-multiplexer-mplex/tsconfig.json create mode 100644 packages/topology/CHANGELOG.md create mode 100644 packages/topology/LICENSE create mode 100644 packages/topology/LICENSE-APACHE create mode 100644 packages/topology/LICENSE-MIT create mode 100644 packages/topology/README.md create mode 100644 packages/topology/package.json create mode 100644 packages/topology/src/index.ts create mode 100644 packages/topology/tsconfig.json create mode 100644 packages/tracked-map/.gitignore create mode 100644 packages/tracked-map/CHANGELOG.md create mode 100644 packages/tracked-map/LICENSE create mode 100644 packages/tracked-map/LICENSE-APACHE create mode 100644 packages/tracked-map/LICENSE-MIT create mode 100644 packages/tracked-map/README.md create mode 100644 packages/tracked-map/package.json create mode 100644 packages/tracked-map/src/index.ts create mode 100644 packages/tracked-map/test/index.spec.ts create mode 100644 packages/tracked-map/tsconfig.json create mode 100644 packages/transport-tcp/.aegir.js create mode 100644 packages/transport-tcp/CHANGELOG.md create mode 100644 packages/transport-tcp/LICENSE create mode 100644 packages/transport-tcp/LICENSE-APACHE create mode 100644 packages/transport-tcp/LICENSE-MIT create mode 100644 packages/transport-tcp/README.md create mode 100644 packages/transport-tcp/package.json create mode 100644 packages/transport-tcp/src/constants.ts create mode 100644 packages/transport-tcp/src/index.ts create mode 100644 packages/transport-tcp/src/listener.ts create mode 100644 packages/transport-tcp/src/socket-to-conn.ts create mode 100644 packages/transport-tcp/src/utils.ts create mode 100644 packages/transport-tcp/test/compliance.spec.ts create mode 100644 packages/transport-tcp/test/connection.spec.ts create mode 100644 packages/transport-tcp/test/filter.spec.ts create mode 100644 packages/transport-tcp/test/listen-dial.spec.ts create mode 100644 packages/transport-tcp/test/max-connections-close.spec.ts create mode 100644 packages/transport-tcp/test/max-connections.spec.ts create mode 100644 packages/transport-tcp/test/socket-to-conn.spec.ts create mode 100644 packages/transport-tcp/tsconfig.json create mode 100644 packages/transport-webrtc/.aegir.js create mode 100644 packages/transport-webrtc/CHANGELOG.md create mode 100644 packages/transport-webrtc/LICENSE create mode 100644 packages/transport-webrtc/LICENSE-APACHE create mode 100644 packages/transport-webrtc/LICENSE-MIT create mode 100644 packages/transport-webrtc/README.md create mode 100644 packages/transport-webrtc/examples/README.md create mode 100644 packages/transport-webrtc/examples/browser-to-browser/README.md create mode 100644 packages/transport-webrtc/examples/browser-to-browser/index.html create mode 100644 packages/transport-webrtc/examples/browser-to-browser/index.js create mode 100644 packages/transport-webrtc/examples/browser-to-browser/package.json create mode 100644 packages/transport-webrtc/examples/browser-to-browser/relay.js create mode 100644 packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js create mode 100644 packages/transport-webrtc/examples/browser-to-browser/vite.config.js create mode 100644 packages/transport-webrtc/examples/browser-to-server/README.md create mode 100644 packages/transport-webrtc/examples/browser-to-server/index.html create mode 100644 packages/transport-webrtc/examples/browser-to-server/index.js create mode 100644 packages/transport-webrtc/examples/browser-to-server/package.json create mode 100644 packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js create mode 100644 packages/transport-webrtc/examples/browser-to-server/vite.config.js create mode 100644 packages/transport-webrtc/examples/go-libp2p-server/.gitignore create mode 100644 packages/transport-webrtc/examples/go-libp2p-server/go.mod create mode 100644 packages/transport-webrtc/examples/go-libp2p-server/go.sum create mode 100644 packages/transport-webrtc/examples/go-libp2p-server/main.go create mode 100644 packages/transport-webrtc/package.json create mode 100644 packages/transport-webrtc/src/error.ts create mode 100644 packages/transport-webrtc/src/index.ts create mode 100644 packages/transport-webrtc/src/maconn.ts create mode 100644 packages/transport-webrtc/src/muxer.ts create mode 100644 packages/transport-webrtc/src/pb/message.proto create mode 100644 packages/transport-webrtc/src/pb/message.ts create mode 100644 packages/transport-webrtc/src/private-to-private/handler.ts create mode 100644 packages/transport-webrtc/src/private-to-private/listener.ts create mode 100644 packages/transport-webrtc/src/private-to-private/pb/message.proto create mode 100644 packages/transport-webrtc/src/private-to-private/pb/message.ts create mode 100644 packages/transport-webrtc/src/private-to-private/transport.ts create mode 100644 packages/transport-webrtc/src/private-to-private/util.ts create mode 100644 packages/transport-webrtc/src/private-to-public/options.ts create mode 100644 packages/transport-webrtc/src/private-to-public/sdp.ts create mode 100644 packages/transport-webrtc/src/private-to-public/transport.ts create mode 100644 packages/transport-webrtc/src/private-to-public/util.ts create mode 100644 packages/transport-webrtc/src/stream.ts create mode 100644 packages/transport-webrtc/src/util.ts create mode 100644 packages/transport-webrtc/test/basics.spec.ts create mode 100644 packages/transport-webrtc/test/listener.spec.ts create mode 100644 packages/transport-webrtc/test/maconn.browser.spec.ts create mode 100644 packages/transport-webrtc/test/peer.browser.spec.ts create mode 100644 packages/transport-webrtc/test/sdp.spec.ts create mode 100644 packages/transport-webrtc/test/stream.browser.spec.ts create mode 100644 packages/transport-webrtc/test/stream.spec.ts create mode 100644 packages/transport-webrtc/test/transport.browser.spec.ts create mode 100644 packages/transport-webrtc/test/util.ts create mode 100644 packages/transport-webrtc/tsconfig.json create mode 100644 packages/transport-websockets/.aegir.js create mode 100644 packages/transport-websockets/CHANGELOG.md create mode 100644 packages/transport-websockets/LICENSE create mode 100644 packages/transport-websockets/LICENSE-APACHE create mode 100644 packages/transport-websockets/LICENSE-MIT create mode 100644 packages/transport-websockets/README.md create mode 100644 packages/transport-websockets/package.json create mode 100644 packages/transport-websockets/src/constants.ts create mode 100644 packages/transport-websockets/src/filters.ts create mode 100644 packages/transport-websockets/src/index.ts create mode 100644 packages/transport-websockets/src/listener.browser.ts create mode 100644 packages/transport-websockets/src/listener.ts create mode 100644 packages/transport-websockets/src/socket-to-conn.ts create mode 100644 packages/transport-websockets/test/browser.ts create mode 100644 packages/transport-websockets/test/compliance.node.ts create mode 100644 packages/transport-websockets/test/fixtures/certificate.pem create mode 100644 packages/transport-websockets/test/fixtures/key.pem create mode 100644 packages/transport-websockets/test/node.ts create mode 100644 packages/transport-websockets/tsconfig.json create mode 100644 packages/transport-webtransport/.aegir.js create mode 100644 packages/transport-webtransport/.gitignore create mode 100644 packages/transport-webtransport/CHANGELOG.md create mode 100644 packages/transport-webtransport/LICENSE create mode 100644 packages/transport-webtransport/LICENSE-APACHE create mode 100644 packages/transport-webtransport/LICENSE-MIT create mode 100644 packages/transport-webtransport/README.md create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/README.md create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/img/img2.png create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/index.html create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/package.json create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json create mode 100644 packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js create mode 100644 packages/transport-webtransport/go-libp2p-webtransport-server/go.mod create mode 100644 packages/transport-webtransport/go-libp2p-webtransport-server/go.sum create mode 100644 packages/transport-webtransport/go-libp2p-webtransport-server/main.go create mode 100644 packages/transport-webtransport/package.json create mode 100644 packages/transport-webtransport/src/index.ts create mode 100644 packages/transport-webtransport/test/browser.ts create mode 100644 packages/transport-webtransport/tsconfig.json create mode 100644 packages/utils/API.md create mode 100644 packages/utils/CHANGELOG.md create mode 100644 packages/utils/LICENSE create mode 100644 packages/utils/LICENSE-APACHE create mode 100644 packages/utils/LICENSE-MIT create mode 100644 packages/utils/README.md create mode 100644 packages/utils/package.json create mode 100644 packages/utils/src/address-sort.ts create mode 100644 packages/utils/src/array-equals.ts create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/src/ip-port-to-multiaddr.ts create mode 100644 packages/utils/src/multiaddr/is-loopback.ts create mode 100644 packages/utils/src/multiaddr/is-private.ts create mode 100644 packages/utils/src/stream-to-ma-conn.ts create mode 100644 packages/utils/test/address-sort.spec.ts create mode 100644 packages/utils/test/array-equals.spec.ts create mode 100644 packages/utils/test/ip-port-to-multiaddr.spec.ts create mode 100644 packages/utils/test/multiaddr/is-loopback.spec.ts create mode 100644 packages/utils/test/multiaddr/is-private.spec.ts create mode 100644 packages/utils/test/stream-to-ma-conn.spec.ts create mode 100644 packages/utils/tsconfig.json diff --git a/.github/workflows/interop-test.yml b/.github/workflows/interop-test.yml index 5d97ba3058..8c70c23a6a 100644 --- a/.github/workflows/interop-test.yml +++ b/.github/workflows/interop-test.yml @@ -12,11 +12,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ipfs/aegir/actions/cache-node-modules@master - with: - directories: | - ./interop/node_modules - - name: Build interop - run: (cd interop && npm i && npm run build) - name: Build images run: (cd interop && make) - name: Save package-lock.json as artifact diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c18845b81e..ba70b783b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -102,7 +102,7 @@ jobs: test-webkit: needs: check - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -116,6 +116,22 @@ jobs: directory: ./.nyc_output flags: webkit + test-webkit-webworker: + needs: check + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx playwright install-deps + - run: npm run --if-present test:webkit-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: webkit-webworker + test-electron-main: needs: check runs-on: ubuntu-latest @@ -165,6 +181,8 @@ jobs: test-chrome-webworker, test-firefox, test-firefox-webworker, + test-webkit, + test-webkit-webworker, test-electron-main, test-electron-renderer, test-interop diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 15cb8a7cf1..ce2dc358a3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1,26 @@ -{"packages/interface-address-manager":"3.0.1","packages/interface-compliance-tests":"3.0.7","packages/interface-connection":"5.1.1","packages/interface-connection-compliance-tests":"2.0.9","packages/interface-connection-encrypter":"4.0.1","packages/interface-connection-encrypter-compliance-tests":"5.0.1","packages/interface-connection-gater":"3.0.1","packages/interface-connection-manager":"3.0.1","packages/interface-content-routing":"2.1.1","packages/interface-dht":"2.0.3","packages/interface-keychain":"2.0.5","packages/interface-keys":"1.0.8","packages/interface-libp2p":"3.2.0","packages/interface-metrics":"4.0.8","packages/interface-mocks":"12.0.1","packages/interface-peer-discovery":"2.0.0","packages/interface-peer-discovery-compliance-tests":"2.0.8","packages/interface-peer-id":"2.0.2","packages/interface-peer-info":"1.0.10","packages/interface-peer-routing":"1.1.1","packages/interface-peer-store":"2.0.4","packages/interface-pubsub":"4.0.1","packages/interface-pubsub-compliance-tests":"5.0.9","packages/interface-record":"2.0.7","packages/interface-record-compliance-tests":"2.0.5","packages/interface-registrar":"2.0.12","packages/interface-stream-muxer":"4.1.2","packages/interface-stream-muxer-compliance-tests":"7.0.3","packages/interface-transport":"4.0.3","packages/interface-transport-compliance-tests":"4.0.2","packages/interfaces":"3.3.2","packages/libp2p":"0.45.9"} \ No newline at end of file +{ + "packages/peer-discovery-bootstrap": "8.0.0", + "packages/crypto": "1.0.17", + "packages/interface-address-manager":"3.0.1","packages/interface-compliance-tests":"3.0.7","packages/interface-connection":"5.1.1","packages/interface-connection-compliance-tests":"2.0.9","packages/interface-connection-encrypter":"4.0.1","packages/interface-connection-encrypter-compliance-tests":"5.0.1","packages/interface-connection-gater":"3.0.1","packages/interface-connection-manager":"3.0.1","packages/interface-content-routing":"2.1.1","packages/interface-dht":"2.0.3","packages/interface-keychain":"2.0.5","packages/interface-keys":"1.0.8","packages/interface-libp2p":"3.2.0","packages/interface-metrics":"4.0.8","packages/interface-mocks":"12.0.1","packages/interface-peer-discovery":"2.0.0","packages/interface-peer-discovery-compliance-tests":"2.0.8","packages/interface-peer-id":"2.0.2","packages/interface-peer-info":"1.0.10","packages/interface-peer-routing":"1.1.1","packages/interface-peer-store":"2.0.4","packages/interface-pubsub":"4.0.1","packages/interface-pubsub-compliance-tests":"5.0.9","packages/interface-record":"2.0.7","packages/interface-record-compliance-tests":"2.0.5","packages/interface-registrar":"2.0.12","packages/interface-stream-muxer":"4.1.2","packages/interface-stream-muxer-compliance-tests":"7.0.3","packages/interface-transport":"4.0.3","packages/interface-transport-compliance-tests":"4.0.2","packages/interfaces":"3.3.2", + "packages/kad-dht": "9.3.6", + "packages/keychain": "2.0.1", + "packages/libp2p":"0.45.9", + "packages/logger":"2.1.1", + "packages/peer-discovery-mdns":"8.0.0", + "packages/stream-multiplexer-mplex":"8.0.4", + "packages/multistream-select":"3.1.9", + "packages/peer-collections":"3.0.2", + "packages/peer-id":"2.0.3", + "packages/peer-id-factory":"2.0.3", + "packages/peer-record":"5.0.4", + "packages/peer-store":"8.2.1", + "packages/metrics-prometheus":"1.1.5", + "packages/record":"3.0.4", + "packages/transport-tcp":"7.0.3", + "packages/topology":"4.0.3", + "packages/tracked-map":"3.0.3", + "packages/utils": "3.0.12", + "packages/transport-webrtc":"2.0.10", + "packages/transport-websockets":"6.0.3", + "packages/transport-webtransport":"2.0.2" +} diff --git a/.release-please.json b/.release-please.json index 83d6cd719d..9e3c14c9a1 100644 --- a/.release-please.json +++ b/.release-please.json @@ -4,6 +4,7 @@ "bump-patch-for-minor-pre-major": true, "group-pull-request-title-pattern": "chore: release ${component}", "packages": { + "packages/crypto": {}, "packages/interface-address-manager": {}, "packages/interface-compliance-tests": {}, "packages/interface-connection": {}, @@ -35,6 +36,27 @@ "packages/interface-transport": {}, "packages/interface-transport-compliance-tests": {}, "packages/interfaces": {}, - "packages/libp2p": {} + "packages/kad-dht": {}, + "packages/keychain": {}, + "packages/libp2p": {}, + "packages/logger": {}, + "packages/metrics-prometheus": {}, + "packages/multistream-select": {}, + "packages/peer-collections": {}, + "packages/peer-discovery-bootstrap": {}, + "packages/peer-discovery-mdns": {}, + "packages/peer-id": {}, + "packages/peer-id-factory": {}, + "packages/peer-record": {}, + "packages/peer-store": {}, + "packages/record": {}, + "packages/stream-multiplexer-mplex": {}, + "packages/topology": {}, + "packages/tracked-map": {}, + "packages/transport-tcp": {}, + "packages/transport-webrtc": {}, + "packages/transport-websockets": {}, + "packages/transport-webtransport": {}, + "packages/utils": {} } } diff --git a/interop/BrowserDockerfile b/interop/BrowserDockerfile index aa6f0077be..55b8613ce1 100644 --- a/interop/BrowserDockerfile +++ b/interop/BrowserDockerfile @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/playwright COPY --from=node-js-libp2p-head /app/ /app/ WORKDIR /app/interop -RUN ./node_modules/.bin/playwright install +RUN npx playwright install ARG BROWSER=chromium # Options: chromium, firefox, webkit ENV BROWSER=$BROWSER diff --git a/interop/Dockerfile b/interop/Dockerfile index 8f981e0aa1..9d28f0229e 100644 --- a/interop/Dockerfile +++ b/interop/Dockerfile @@ -3,13 +3,9 @@ FROM node:18 WORKDIR /app COPY package.json . COPY ./node_modules ./node_modules -COPY ./packages/libp2p ./packages/libp2p +COPY ./packages ./packages +COPY ./interop ./interop WORKDIR /app/interop -COPY ./interop/node_modules ./node_modules -COPY ./interop/dist ./dist -COPY ./interop/package.json . -COPY ./interop/.aegir.js . -COPY ./interop/relay.js . ENTRYPOINT [ "npm", "test", "--", "--build", "false", "--types", "false", "-t", "node" ] diff --git a/interop/LICENSE b/interop/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/interop/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/interop/LICENSE-APACHE b/interop/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/interop/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/interop/LICENSE-MIT b/interop/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/interop/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/interop/README.md b/interop/README.md new file mode 100644 index 0000000000..b4d301f10f --- /dev/null +++ b/interop/README.md @@ -0,0 +1,36 @@ +# @libp2p/multidim-interop + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Multidimensional Interop Test + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/multidim-interop +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/interop/package.json b/interop/package.json index 78108d265b..896306d3bf 100644 --- a/interop/package.json +++ b/interop/package.json @@ -1,34 +1,60 @@ { - "name": "multidim-interop", - "private": true, + "name": "@libp2p/multidim-interop", "version": "1.0.0", - "description": "Multidimension Interop Test", - "type": "module", - "main": "index.js", + "description": "Multidimensional Interop Test", "author": "Glen De Cauwsemaecker / @marcopolo", - "license": "MIT", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/interop#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, "scripts": { "start": "node index.js", "build": "aegir build", "test": "aegir test" }, "dependencies": { - "@chainsafe/libp2p-noise": "^12.0.0", + "@chainsafe/libp2p-noise": "^12.0.1", "@chainsafe/libp2p-yamux": "^4.0.1", - "@libp2p/mplex": "^8.0.1", - "@libp2p/tcp": "^7.0.1", - "@libp2p/webrtc": "^2.0.7", - "@libp2p/websockets": "^6.0.1", - "@libp2p/webtransport": "^2.0.1", + "@libp2p/mplex": "^8.0.0", + "@libp2p/tcp": "^7.0.0", + "@libp2p/webrtc": "^2.0.0", + "@libp2p/websockets": "^6.0.0", + "@libp2p/webtransport": "^2.0.0", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.3", - "libp2p": "../packages/libp2p", + "libp2p": "^0.45.0", "redis": "4.5.1" }, + "devDependencies": { + "aegir": "^39.0.10" + }, "browser": { "@libp2p/tcp": false }, - "devDependencies": { - "aegir": "^39.0.5" - } + "private": true } diff --git a/interop/test/ping.spec.ts b/interop/test/ping.spec.ts index 391c75745b..083ad7df81 100644 --- a/interop/test/ping.spec.ts +++ b/interop/test/ping.spec.ts @@ -46,6 +46,12 @@ describe('ping test', () => { services: { ping: pingService(), identify: identifyService() + }, + connectionManager: { + // disable auto-dial + minConnections: 0, + // slow CI is slow + dialTimeout: 60 * 1000 * 5 } } diff --git a/interop/tsconfig.json b/interop/tsconfig.json index 55b334a3e5..5be3797bc7 100644 --- a/interop/tsconfig.json +++ b/interop/tsconfig.json @@ -1,10 +1,30 @@ { - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ] -} \ No newline at end of file + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../packages/libp2p" + }, + { + "path": "../packages/stream-multiplexer-mplex" + }, + { + "path": "../packages/transport-tcp" + }, + { + "path": "../packages/transport-webrtc" + }, + { + "path": "../packages/transport-websockets" + }, + { + "path": "../packages/transport-webtransport" + } + ] +} diff --git a/package.json b/package.json index db12d779e4..11e16af991 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ ] }, "workspaces": [ + "interop", "packages/*" ] } diff --git a/packages/crypto/.aegir.js b/packages/crypto/.aegir.js new file mode 100644 index 0000000000..3cda4fd788 --- /dev/null +++ b/packages/crypto/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir/types').PartialOptions} */ +export default { + build: { + bundlesizeMax: '70kB' + } +} diff --git a/packages/crypto/CHANGELOG.md b/packages/crypto/CHANGELOG.md new file mode 100644 index 0000000000..2185a39d35 --- /dev/null +++ b/packages/crypto/CHANGELOG.md @@ -0,0 +1,738 @@ +## [1.0.17](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.16...v1.0.17) (2023-05-05) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#320](https://github.com/libp2p/js-libp2p-crypto/issues/320)) ([f0b4c06](https://github.com/libp2p/js-libp2p-crypto/commit/f0b4c068a23d78b1376865c6adf6cce21ab91196)) + +## [1.0.16](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.15...v1.0.16) (2023-05-05) + + +### Bug Fixes + +* Remove unreliable webkit-linux useragent check ([#319](https://github.com/libp2p/js-libp2p-crypto/issues/319)) ([8f8df5c](https://github.com/libp2p/js-libp2p-crypto/commit/8f8df5ccb99cf49f8d7f85a4834c505738c3bf63)), closes [#318](https://github.com/libp2p/js-libp2p-crypto/issues/318) + +## [1.0.15](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.14...v1.0.15) (2023-04-04) + + +### Bug Fixes + +* deriveKey in webkit linux (workaround) ([#313](https://github.com/libp2p/js-libp2p-crypto/issues/313)) ([4905944](https://github.com/libp2p/js-libp2p-crypto/commit/4905944fdb0578a77a36fd5c097be459a501ad34)) + +## [1.0.14](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.13...v1.0.14) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#297](https://github.com/libp2p/js-libp2p-crypto/issues/297)) ([7e61930](https://github.com/libp2p/js-libp2p-crypto/commit/7e61930f0416bac16dfd66f825a60a49acfbf780)) + +## [1.0.13](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.12...v1.0.13) (2023-03-10) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b66007c](https://github.com/libp2p/js-libp2p-crypto/commit/b66007c8a092789490789dce596b4e157fa855e1)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([110063c](https://github.com/libp2p/js-libp2p-crypto/commit/110063cbea5d868923a2df2b9676e13a127b81f5)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([c789b0c](https://github.com/libp2p/js-libp2p-crypto/commit/c789b0c9f18f76df84a4038e9ed893a516babb43)) + + +### Dependencies + +* **dev:** upgrade aegir to `38.1.2` ([#302](https://github.com/libp2p/js-libp2p-crypto/issues/302)) ([9d60e39](https://github.com/libp2p/js-libp2p-crypto/commit/9d60e394d5e52167e9197067d3fa7953b30a990b)) + +## [1.0.12](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.11...v1.0.12) (2023-02-08) + + +### Bug Fixes + +* derive ed25519 public key from private key using node crypto ([#300](https://github.com/libp2p/js-libp2p-crypto/issues/300)) ([874f820](https://github.com/libp2p/js-libp2p-crypto/commit/874f8201c157a76a5fd9d3828c1f44358bce2f48)), closes [#295](https://github.com/libp2p/js-libp2p-crypto/issues/295) + + +### Trivial Changes + +* replace err-code with CodeError ([#293](https://github.com/libp2p/js-libp2p-crypto/issues/293)) ([4398cf6](https://github.com/libp2p/js-libp2p-crypto/commit/4398cf6c719fc7cb051cb3071cad9fc86b858752)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) + +## [1.0.11](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.10...v1.0.11) (2023-01-06) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#292](https://github.com/libp2p/js-libp2p-crypto/issues/292)) ([f2d78f8](https://github.com/libp2p/js-libp2p-crypto/commit/f2d78f8012b459da0a62bb4a7c63c396f56d4976)), closes [#234](https://github.com/libp2p/js-libp2p-crypto/issues/234) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) [#234](https://github.com/libp2p/js-libp2p-crypto/issues/234) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) [#226](https://github.com/libp2p/js-libp2p-crypto/issues/226) + + +### Trivial Changes + +* update bundlesize ([c2a1954](https://github.com/libp2p/js-libp2p-crypto/commit/c2a195431ba94fdeedfeb89e406b80a60f960dd7)) + +## [1.0.10](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.9...v1.0.10) (2022-12-16) + + +### Documentation + +* publish api docs ([#291](https://github.com/libp2p/js-libp2p-crypto/issues/291)) ([5ae970f](https://github.com/libp2p/js-libp2p-crypto/commit/5ae970f0190853470a24509c3e396cc949294d8e)) + +## [1.0.9](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.8...v1.0.9) (2022-12-01) + + +### Bug Fixes + +* use node crypto for ed25519 signing and verification ([#289](https://github.com/libp2p/js-libp2p-crypto/issues/289)) ([1c623e7](https://github.com/libp2p/js-libp2p-crypto/commit/1c623e7d55ddfafbad0b65b261f55bbc6957df28)) + +## [1.0.8](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.7...v1.0.8) (2022-12-01) + + +### Dependencies + +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#287](https://github.com/libp2p/js-libp2p-crypto/issues/287)) ([3aa22cd](https://github.com/libp2p/js-libp2p-crypto/commit/3aa22cdfdef028b3b451e62f9f5784000b2b0690)) + +## [1.0.7](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.6...v1.0.7) (2022-10-14) + + +### Dependencies + +* update protons-runtime and protons ([#283](https://github.com/libp2p/js-libp2p-crypto/issues/283)) ([2c4650c](https://github.com/libp2p/js-libp2p-crypto/commit/2c4650cff5222344b2573be6db4f791415dc276c)) + +## [1.0.6](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.5...v1.0.6) (2022-10-13) + + +### Dependencies + +* bump uint8arrays from 3.1.1 to 4.0.2 ([#281](https://github.com/libp2p/js-libp2p-crypto/issues/281)) ([2e87325](https://github.com/libp2p/js-libp2p-crypto/commit/2e8732549814bead1f0da1f2afc5b591f1dfc765)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.4...v1.0.5) (2022-10-13) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([05c2246](https://github.com/libp2p/js-libp2p-crypto/commit/05c22463b3334ef9dd2c74a29a5f2a5fca96222a)) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([#279](https://github.com/libp2p/js-libp2p-crypto/issues/279)) ([699c812](https://github.com/libp2p/js-libp2p-crypto/commit/699c812e07e35cf81b199698ea362d507777fbd9)) +* **dev:** bump @types/mocha from 9.1.1 to 10.0.0 ([#276](https://github.com/libp2p/js-libp2p-crypto/issues/276)) ([bb22d31](https://github.com/libp2p/js-libp2p-crypto/commit/bb22d314c1c1ea6d1e4d4d0bfcbd76969e1f3ab1)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.3...v1.0.4) (2022-08-26) + + +### Dependencies + +* **dev:** bump wherearewe from 1.0.2 to 2.0.1 ([#273](https://github.com/libp2p/js-libp2p-crypto/issues/273)) ([612e8ae](https://github.com/libp2p/js-libp2p-crypto/commit/612e8aef341d1f3392eed047fdc66d9c329cbf1f)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.2...v1.0.3) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#272](https://github.com/libp2p/js-libp2p-crypto/issues/272)) ([23a031a](https://github.com/libp2p/js-libp2p-crypto/commit/23a031ab554677ee0b7c5d632e5457edf5615954)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.1...v1.0.2) (2022-08-03) + + +### Bug Fixes + +* remove iso-random-stream dependency ([#262](https://github.com/libp2p/js-libp2p-crypto/issues/262)) ([c5cb096](https://github.com/libp2p/js-libp2p-crypto/commit/c5cb0969a614fb9cab73a5e12acc5e9e8ce9993d)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-crypto/compare/v1.0.0...v1.0.1) (2022-07-31) + + +### Trivial Changes + +* update project config ([#265](https://github.com/libp2p/js-libp2p-crypto/issues/265)) ([10ca181](https://github.com/libp2p/js-libp2p-crypto/commit/10ca18126bacf97e34283cc82c4bc8500716a4e3)) + + +### Dependencies + +* update protons to support no-copy ops ([#268](https://github.com/libp2p/js-libp2p-crypto/issues/268)) ([920b081](https://github.com/libp2p/js-libp2p-crypto/commit/920b081bb88afe094cbedd84a2113cf29ef9f1e8)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.14...v1.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#260](https://github.com/libp2p/js-libp2p-crypto/issues/260)) ([60e0789](https://github.com/libp2p/js-libp2p-crypto/commit/60e078968de7d03a61fde6cfd4ab5a3e378c127f)) + +### [0.22.14](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.13...v0.22.14) (2022-05-23) + + +### Bug Fixes + +* Added .js extension to import in src/keys/jwk2pem.ts ([#257](https://github.com/libp2p/js-libp2p-crypto/issues/257)) ([9e8f376](https://github.com/libp2p/js-libp2p-crypto/commit/9e8f3767f5f051edc09ae7be77c833817fed7279)), closes [#256](https://github.com/libp2p/js-libp2p-crypto/issues/256) + + +### Trivial Changes + +* **deps:** bump @libp2p/interfaces from 1.3.32 to 2.0.1 ([#258](https://github.com/libp2p/js-libp2p-crypto/issues/258)) ([f7d30bc](https://github.com/libp2p/js-libp2p-crypto/commit/f7d30bce64a74711e56b962412989164530a45d3)) + +### [0.22.13](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.12...v0.22.13) (2022-05-10) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 13.0.2 to 14.0.0 ([#253](https://github.com/libp2p/js-libp2p-crypto/issues/253)) ([f999025](https://github.com/libp2p/js-libp2p-crypto/commit/f999025ef6f9a2a5ad86e60a6ff95cc4bcef9f06)) + +### [0.22.12](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.11...v0.22.12) (2022-05-10) + + +### Bug Fixes + +* encode protobuf enums correctly ([#254](https://github.com/libp2p/js-libp2p-crypto/issues/254)) ([a27dc64](https://github.com/libp2p/js-libp2p-crypto/commit/a27dc64a902d947d58fd87ed7c532a66f81eaedd)) + +### [0.22.11](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.10...v0.22.11) (2022-04-14) + + +### Bug Fixes + +* add return types to key methods ([#252](https://github.com/libp2p/js-libp2p-crypto/issues/252)) ([8363d28](https://github.com/libp2p/js-libp2p-crypto/commit/8363d28eda3a827ff03a6fc29df263b8c474cfb9)) + +### [0.22.10](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.9...v0.22.10) (2022-04-11) + + +### Bug Fixes + +* use protons ([#251](https://github.com/libp2p/js-libp2p-crypto/issues/251)) ([54a1424](https://github.com/libp2p/js-libp2p-crypto/commit/54a14249ca5e3b7395c243a762fbe6bb4c96e24f)) + +### [0.22.9](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.8...v0.22.9) (2022-03-10) + + +### Bug Fixes + +* fix broken return type ([#247](https://github.com/libp2p/js-libp2p-crypto/issues/247)) ([afa5b6d](https://github.com/libp2p/js-libp2p-crypto/commit/afa5b6d5a4a5e34e4fcac07803439f32555cb748)) + +### [0.22.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.7...v0.22.8) (2022-02-21) + + +### Bug Fixes + +* bump noble-ed25519 to an audited version ([#245](https://github.com/libp2p/js-libp2p-crypto/issues/245)) ([a104a4f](https://github.com/libp2p/js-libp2p-crypto/commit/a104a4f0a0fa5b2ecefd8928c3963dfa1019355b)) + +### [0.22.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.6...v0.22.7) (2022-02-03) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 12.0.1 to 13.0.1 ([#243](https://github.com/libp2p/js-libp2p-crypto/issues/243)) ([a484751](https://github.com/libp2p/js-libp2p-crypto/commit/a484751847ca6ed1889602b378a1436a84483973)) + +### [0.22.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.5...v0.22.6) (2022-01-17) + + +### Trivial Changes + +* update benchmark deps ([#238](https://github.com/libp2p/js-libp2p-crypto/issues/238)) ([21ffe04](https://github.com/libp2p/js-libp2p-crypto/commit/21ffe0428fa040088f1b91f4e454418f436ed690)) + +### [0.22.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.4...v0.22.5) (2022-01-15) + + +### Trivial Changes + +* update project config ([#237](https://github.com/libp2p/js-libp2p-crypto/issues/237)) ([0f28984](https://github.com/libp2p/js-libp2p-crypto/commit/0f28984dee75d446d03911e271d242f1dbc2e8f9)) + +### [0.22.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.3...v0.22.4) (2022-01-08) + + +### Trivial Changes + +* change min node support to 16 ([#230](https://github.com/libp2p/js-libp2p-crypto/issues/230)) ([9aabfe6](https://github.com/libp2p/js-libp2p-crypto/commit/9aabfe6c885b132366c2772e834f8396c96081e9)), closes [#221](https://github.com/libp2p/js-libp2p-crypto/issues/221) +* **deps:** bump node-forge from 0.10.0 to 1.1.0 ([#231](https://github.com/libp2p/js-libp2p-crypto/issues/231)) ([d33eea1](https://github.com/libp2p/js-libp2p-crypto/commit/d33eea1137c538668f1a54b3bc493e5fb4d1c035)) + +### [0.22.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.22.2...v0.22.3) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#232](https://github.com/libp2p/js-libp2p-crypto/issues/232)) ([d1b9961](https://github.com/libp2p/js-libp2p-crypto/commit/d1b99617da775ffed7a7e1a20de740338c0c6159)) +* uncomment renderer build ([#226](https://github.com/libp2p/js-libp2p-crypto/issues/226)) ([cecad66](https://github.com/libp2p/js-libp2p-crypto/commit/cecad669aa7dda5e5eed178099ae0191a500e543)) +* update build ([#227](https://github.com/libp2p/js-libp2p-crypto/issues/227)) ([1642c1b](https://github.com/libp2p/js-libp2p-crypto/commit/1642c1b4386a6d28ed945ffb96ce68ef8a2aed35)) +* update build ([#229](https://github.com/libp2p/js-libp2p-crypto/issues/229)) ([3544d9c](https://github.com/libp2p/js-libp2p-crypto/commit/3544d9c63a7fba0b39e7cbf23bc44b4e196ad52b)) + +# [0.22.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.21.0...v0.22.0) (2022-01-04) + + +### Features + +* convert to typescript ([#222](https://github.com/libp2p/js-libp2p-crypto/issues/222)) ([7875906](https://github.com/libp2p/js-libp2p-crypto/commit/7875906c71fa9fe695889d57061937dd6494b15a)) + + + +# [0.21.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.20.0...v0.21.0) (2021-12-01) + + +### Features + +* replace keypair and ursa-optional with node crypto ([#219](https://github.com/libp2p/js-libp2p-crypto/issues/219)) ([759535c](https://github.com/libp2p/js-libp2p-crypto/commit/759535cef65f0141c540524b77c72783288cc0a0)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.20.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.7...v0.20.0) (2021-10-21) + + +### Bug Fixes + +* demand up to date keypair ([#201](https://github.com/libp2p/js-libp2p-crypto/issues/201)) ([2e40aea](https://github.com/libp2p/js-libp2p-crypto/commit/2e40aeaaa6735a4f2bea58f568d937b60561868b)) + + +### Features + +* use noble-secp256k1 and noble-ed25519 ([#202](https://github.com/libp2p/js-libp2p-crypto/issues/202)) ([167eace](https://github.com/libp2p/js-libp2p-crypto/commit/167eaceb61a779904ff006602ce58d7065d126b7)) + + +### BREAKING CHANGES + +* keys function hashAndVerify returns boolean false when fail, instead of throwing error + + + +## [0.19.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.6...v0.19.7) (2021-08-18) + + + +## [0.19.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.5...v0.19.6) (2021-07-15) + + + +## [0.19.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.4...v0.19.5) (2021-07-07) + + + +## [0.19.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.3...v0.19.4) (2021-04-20) + + +### Bug Fixes + +* specify pbjs root ([#188](https://github.com/libp2p/js-libp2p-crypto/issues/188)) ([a3da59b](https://github.com/libp2p/js-libp2p-crypto/commit/a3da59b90e50d514d1578236e1c86f05c8cb0805)) + + + +## [0.19.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.7...v0.19.3) (2021-04-11) + + +### Bug Fixes + +* ed25519 key ID generation ([bc33769](https://github.com/libp2p/js-libp2p-crypto/commit/bc337698b6124e3461c8dc4be2f264ea98351c70)) +* ed25519 PeerID generation ([#186](https://github.com/libp2p/js-libp2p-crypto/issues/186)) ([1c16dd3](https://github.com/libp2p/js-libp2p-crypto/commit/1c16dd3dec8a641f55187bd9fbb6c03ba5fafdaa)), closes [ipfs/js-ipfs#3591](https://github.com/ipfs/js-ipfs/issues/3591) [libp2p/js-libp2p-crypto#185](https://github.com/libp2p/js-libp2p-crypto/issues/185) +* go ed25519 interop ([2f18a07](https://github.com/libp2p/js-libp2p-crypto/commit/2f18a077b47ee84c450431f7431ecdfc913c8543)) +* remove rendundant public key ([#181](https://github.com/libp2p/js-libp2p-crypto/issues/181)) ([afcffc8](https://github.com/libp2p/js-libp2p-crypto/commit/afcffc8115c8833edfe2a942d05547f418be5585)) +* replace node buffers with uint8arrays ([#180](https://github.com/libp2p/js-libp2p-crypto/issues/180)) ([a0f387a](https://github.com/libp2p/js-libp2p-crypto/commit/a0f387aeab5dff45368341d0d80a5d1a25e9f849)) + + +### Features + +* add exporting/importing of non rsa keys in libp2p-key format ([#179](https://github.com/libp2p/js-libp2p-crypto/issues/179)) ([7273739](https://github.com/libp2p/js-libp2p-crypto/commit/7273739f045b33a46aae45f5003dd09f7ea6e37e)) + + +### BREAKING CHANGES + +* The private ed25519 key will no longer include the redundant public key + +* chore: fix lint + + + + +## [0.19.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.1...v0.19.2) (2021-03-17) + + +### Bug Fixes + +* ed25519 PeerID generation ([#186](https://github.com/libp2p/js-libp2p-crypto/issues/186)) ([1c16dd3](https://github.com/libp2p/js-libp2p-crypto/commit/1c16dd3)), closes [ipfs/js-ipfs#3591](https://github.com/ipfs/js-ipfs/issues/3591) [libp2p/js-libp2p-crypto#185](https://github.com/libp2p/js-libp2p-crypto/issues/185) + + + + +## [0.19.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.19.0...v0.19.1) (2021-03-15) + + +### Bug Fixes + +* ed25519 key ID generation ([bc33769](https://github.com/libp2p/js-libp2p-crypto/commit/bc33769)) + + + + +# [0.19.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.18.0...v0.19.0) (2021-01-15) + + + + +# [0.18.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.9...v0.18.0) (2020-08-07) + + +### Bug Fixes + +* remove rendundant public key ([#181](https://github.com/libp2p/js-libp2p-crypto/issues/181)) ([afcffc8](https://github.com/libp2p/js-libp2p-crypto/commit/afcffc8)) +* replace node buffers with uint8arrays ([#180](https://github.com/libp2p/js-libp2p-crypto/issues/180)) ([a0f387a](https://github.com/libp2p/js-libp2p-crypto/commit/a0f387a)) + + +### BREAKING CHANGES + +* The private ed25519 key will no longer include the redundant public key + +* chore: fix lint +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove commented code + + + + +## [0.17.9](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.8...v0.17.9) (2020-08-05) + + +### Features + +* add exporting/importing of non rsa keys in libp2p-key format ([#179](https://github.com/libp2p/js-libp2p-crypto/issues/179)) ([7273739](https://github.com/libp2p/js-libp2p-crypto/commit/7273739)) + + + + +## [0.17.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.7...v0.17.8) (2020-07-20) + + +### Bug Fixes + +* go ed25519 interop ([2f18a07](https://github.com/libp2p/js-libp2p-crypto/commit/2f18a07)) + + + + +## [0.17.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.6...v0.17.7) (2020-06-09) + + + + +## [0.17.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.5...v0.17.6) (2020-04-07) + + +### Bug Fixes + +* add buffer and update deps ([#25](https://github.com/libp2p/js-libp2p-crypto/issues/25)) ([35f196e](https://github.com/libp2p/js-libp2p-crypto/commit/35f196e)) +* **unmarshal:** provide only one arg to callback ([#17](https://github.com/libp2p/js-libp2p-crypto/issues/17)) ([3bb8451](https://github.com/libp2p/js-libp2p-crypto/commit/3bb8451)) +* circular circular dep -> DI ([0dcf1a6](https://github.com/libp2p/js-libp2p-crypto/commit/0dcf1a6)) +* update deps and repo setup ([cfdcbe0](https://github.com/libp2p/js-libp2p-crypto/commit/cfdcbe0)) + + +### Features + +* add `id()` method to Secp256k1PrivateKey ([f4dbd62](https://github.com/libp2p/js-libp2p-crypto/commit/f4dbd62)) +* initial implementation ([4c36aeb](https://github.com/libp2p/js-libp2p-crypto/commit/4c36aeb)) +* next libp2p-crypto ([#4](https://github.com/libp2p/js-libp2p-crypto/issues/4)) ([4ee48a7](https://github.com/libp2p/js-libp2p-crypto/commit/4ee48a7)) +* use async await ([#18](https://github.com/libp2p/js-libp2p-crypto/issues/18)) ([1974eb9](https://github.com/libp2p/js-libp2p-crypto/commit/1974eb9)) + + +### BREAKING CHANGES + +* Callback support has been dropped in favor of async/await. + +* feat: use async/await + +This PR changes this module to remove callbacks and use async/await. The API is unchanged aside from the obvious removal of the `callback` parameter. + +refs https://github.com/ipfs/js-ipfs/issues/1670 + +* fix: use latest multihashing-async as it is all promises now + + + + +## [0.17.5](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.4...v0.17.5) (2020-03-24) + + + + +## [0.17.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.3...v0.17.4) (2020-03-23) + + +### Bug Fixes + +* add buffer, cleanup, reduce size ([#170](https://github.com/libp2p/js-libp2p-crypto/issues/170)) ([c956d1a](https://github.com/libp2p/js-libp2p-crypto/commit/c956d1a)) + + + + +## [0.17.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.2...v0.17.3) (2020-02-26) + + +### Performance Improvements + +* remove asn1.js and use node-forge ([#166](https://github.com/libp2p/js-libp2p-crypto/issues/166)) ([00477e3](https://github.com/libp2p/js-libp2p-crypto/commit/00477e3)) +* remove jwk2privPem and jwk2pubPem ([#162](https://github.com/libp2p/js-libp2p-crypto/issues/162)) ([cc20949](https://github.com/libp2p/js-libp2p-crypto/commit/cc20949)) + + +### BREAKING CHANGES + +* removes unused jwk2pem methods `jwk2pubPem` and `jwk2privPem`. These methods are not being used in any js libp2p modules, so only users referencing these directly will be impacted. + + + + +## [0.17.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.1...v0.17.2) (2020-01-17) + + +### Features + +* add typescript types + linting/tests ([#161](https://github.com/libp2p/js-libp2p-crypto/issues/161)) ([e01977c](https://github.com/libp2p/js-libp2p-crypto/commit/e01977c)) + + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.17.0...v0.17.1) (2019-10-25) + + +### Bug Fixes + +* better error for missing web crypto ([a5e0560](https://github.com/libp2p/js-libp2p-crypto/commit/a5e0560)) +* browser rsa enc/dec ([b8e2414](https://github.com/libp2p/js-libp2p-crypto/commit/b8e2414)) +* jwk var naming ([8b8d0c1](https://github.com/libp2p/js-libp2p-crypto/commit/8b8d0c1)) +* lint ([2c294b5](https://github.com/libp2p/js-libp2p-crypto/commit/2c294b5)) +* padding error ([2c1bac5](https://github.com/libp2p/js-libp2p-crypto/commit/2c1bac5)) +* use direct buffers instead of converting to hex ([027a5a9](https://github.com/libp2p/js-libp2p-crypto/commit/027a5a9)) + + +### Features + +* add (rsa)pubKey.encrypt and (rsa)privKey.decrypt ([34c5f5c](https://github.com/libp2p/js-libp2p-crypto/commit/34c5f5c)) +* browser enc/dec ([9f747a1](https://github.com/libp2p/js-libp2p-crypto/commit/9f747a1)) +* use forge to convert jwk2forge ([b998f63](https://github.com/libp2p/js-libp2p-crypto/commit/b998f63)) + + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.16.1...v0.17.0) (2019-07-11) + + +### Bug Fixes + +* **deps:** update to ursa-optiona@0.10 ([26b6217](https://github.com/libp2p/js-libp2p-crypto/commit/26b6217)) +* fix links in README ([#148](https://github.com/libp2p/js-libp2p-crypto/issues/148)) ([5cd0e8c](https://github.com/libp2p/js-libp2p-crypto/commit/5cd0e8c)) +* put optional args last for key export ([#154](https://github.com/libp2p/js-libp2p-crypto/issues/154)) ([d675670](https://github.com/libp2p/js-libp2p-crypto/commit/d675670)) + + +### Features + +* refactor to use async/await ([#131](https://github.com/libp2p/js-libp2p-crypto/issues/131)) ([ad71072](https://github.com/libp2p/js-libp2p-crypto/commit/ad71072)) + + +### BREAKING CHANGES + +* key export arguments are now swapped so that the optional format is last +* API refactored to use async/await + +feat: WIP use async await +fix: passing tests +chore: update travis node.js versions +fix: skip ursa optional tests on windows +fix: benchmarks +docs: update docs +fix: remove broken and intested private key decrypt +chore: update deps + + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.16.0...v0.16.1) (2019-02-26) + + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.15.0...v0.16.0) (2019-01-08) + + +### Bug Fixes + +* clean up, bundle size reduction ([8d8294d](https://github.com/libp2p/js-libp2p-crypto/commit/8d8294d)) + + +### BREAKING CHANGES + +* getRandomValues method exported from src/keys/rsa-browser.js and src/keys/rsa.js signature has changed from accepting an array to a number for random byte length + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.14.1...v0.15.0) (2019-01-03) + + +### Features + +* nextTick instead of setImmediate, and fix sync in async ([#136](https://github.com/libp2p/js-libp2p-crypto/issues/136)) ([c54ea20](https://github.com/libp2p/js-libp2p-crypto/commit/c54ea20)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.14.0...v0.14.1) (2018-11-05) + + +### Bug Fixes + +* dont setimmediate when its not needed ([9e57786](https://github.com/libp2p/js-libp2p-crypto/commit/9e57786)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.13.0...v0.14.0) (2018-09-17) + + +### Bug Fixes + +* windows build ([c7e0409](https://github.com/libp2p/js-libp2p-crypto/commit/c7e0409)) +* **lint:** use ~ for ursa-optional version ([e8cbf13](https://github.com/libp2p/js-libp2p-crypto/commit/e8cbf13)) + + +### Features + +* use ursa-optional for lightning fast key generation ([b05e77f](https://github.com/libp2p/js-libp2p-crypto/commit/b05e77f)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.12.1...v0.13.0) (2018-04-05) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.12.0...v0.12.1) (2018-02-12) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.11.0...v0.12.0) (2018-01-27) + + +### Features + +* improve perf ([#117](https://github.com/libp2p/js-libp2p-crypto/issues/117)) ([cdcca5f](https://github.com/libp2p/js-libp2p-crypto/commit/cdcca5f)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.4...v0.11.0) (2017-12-20) + + +### Features + +* key exchange with jsrsasign ([#115](https://github.com/libp2p/js-libp2p-crypto/issues/115)) ([b342128](https://github.com/libp2p/js-libp2p-crypto/commit/b342128)) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.3...v0.10.4) (2017-12-01) + + +### Bug Fixes + +* catch error when unmarshaling instead of crashing ([#113](https://github.com/libp2p/js-libp2p-crypto/issues/113)) ([7608fdd](https://github.com/libp2p/js-libp2p-crypto/commit/7608fdd)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.2...v0.10.3) (2017-09-07) + + +### Features + +* switch protocol-buffers to protons ([#110](https://github.com/libp2p/js-libp2p-crypto/issues/110)) ([3a91ae2](https://github.com/libp2p/js-libp2p-crypto/commit/3a91ae2)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.1...v0.10.2) (2017-09-06) + + +### Bug Fixes + +* use regular protocol-buffers until protobufjs is fixed ([#109](https://github.com/libp2p/js-libp2p-crypto/issues/109)) ([957fdd3](https://github.com/libp2p/js-libp2p-crypto/commit/957fdd3)) + + +### Features + +* **deps:** upgrade to aegir@12 and browserify-aes@1.0.8 ([83257bc](https://github.com/libp2p/js-libp2p-crypto/commit/83257bc)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.10.0...v0.10.1) (2017-09-05) + + +### Bug Fixes + +* switch to protobufjs ([#107](https://github.com/libp2p/js-libp2p-crypto/issues/107)) ([dc2793f](https://github.com/libp2p/js-libp2p-crypto/commit/dc2793f)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.4...v0.10.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#106](https://github.com/libp2p/js-libp2p-crypto/issues/106)) ([9e977c7](https://github.com/libp2p/js-libp2p-crypto/commit/9e977c7)) +* skip nextTick in nodeify ([#103](https://github.com/libp2p/js-libp2p-crypto/issues/103)) ([f20267b](https://github.com/libp2p/js-libp2p-crypto/commit/f20267b)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.3...v0.9.4) (2017-07-22) + + +### Bug Fixes + +* circular circular dep -> DI ([bc554d1](https://github.com/libp2p/js-libp2p-crypto/commit/bc554d1)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.2...v0.9.3) (2017-07-22) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.1...v0.9.2) (2017-07-22) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-crypto/compare/v0.9.0...v0.9.1) (2017-07-22) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.8...v0.9.0) (2017-07-22) + + + + +## [0.8.8](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.7...v0.8.8) (2017-04-11) + + +### Bug Fixes + +* **ecdh:** allow base64 to be left-0-padded, needed for JWK format ([be64372](https://github.com/libp2p/js-libp2p-crypto/commit/be64372)), closes [#97](https://github.com/libp2p/js-libp2p-crypto/issues/97) + + + + +## [0.8.7](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.6...v0.8.7) (2017-03-21) + + + + +## [0.8.6](https://github.com/libp2p/js-libp2p-crypto/compare/v0.8.5...v0.8.6) (2017-03-03) + + +### Bug Fixes + +* **package:** update tweetnacl to version 1.0.0-rc.1 ([4e56e17](https://github.com/libp2p/js-libp2p-crypto/commit/4e56e17)) + + +### Features + +* **keys:** implement generateKeyPairFromSeed for ed25519 ([e5b7c1f](https://github.com/libp2p/js-libp2p-crypto/commit/e5b7c1f)) diff --git a/packages/crypto/LICENSE b/packages/crypto/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/crypto/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/crypto/LICENSE-APACHE b/packages/crypto/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/crypto/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/crypto/LICENSE-MIT b/packages/crypto/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/crypto/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/crypto/README.md b/packages/crypto/README.md new file mode 100644 index 0000000000..a1ab936c74 --- /dev/null +++ b/packages/crypto/README.md @@ -0,0 +1,330 @@ +# @libp2p/crypto + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Crypto primitives for libp2p + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +This repo contains the JavaScript implementation of the crypto primitives needed for libp2p. This is based on this [go implementation](https://github.com/libp2p/go-libp2p-crypto). + +## Lead Maintainer + +[Jacob Heun](https://github.com/jacobheun/) + +## Usage + +```js +const crypto = require('libp2p-crypto') + +// Now available to you: +// +// crypto.aes +// crypto.hmac +// crypto.keys +// etc. +// +// See full API details below... +``` + +### Web Crypto API + +The `libp2p-crypto` library depends on the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in the browser. Web Crypto is available in all modern browsers, however browsers restrict its usage to [Secure Contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + +**This means you will not be able to use some `libp2p-crypto` functions in the browser when the page is served over HTTP.** To enable the Web Crypto API and allow `libp2p-crypto` to work fully, please serve your page over HTTPS. + +## API + +### `crypto.aes` + +Exposes an interface to AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197. + +This uses `CTR` mode. + +#### `crypto.aes.create(key, iv)` + +- `key: Uint8Array` The key, if length `16` then `AES 128` is used. For length `32`, `AES 256` is used. +- `iv: Uint8Array` Must have length `16`. + +Returns `Promise<{decrypt, encrypt}>` + +##### `decrypt(data)` + +- `data: Uint8Array` + +Returns `Promise` + +##### `encrypt(data)` + +- `data: Uint8Array` + +Returns `Promise` + +```js +const crypto = require('libp2p-crypto') + +// Setting up Key and IV + +// A 16 bytes array, 128 Bits, AES-128 is chosen +const key128 = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) + +// A 16 bytes array, 128 Bits, +const IV = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) + +async function main () { + const decryptedMessage = 'Hello, world!' + + // Encrypting + const cipher = await crypto.aes.create(key128, IV) + const encryptedBuffer = await cipher.encrypt(Uint8Array.from(decryptedMessage)) + console.log(encryptedBuffer) + // prints: + + // Decrypting + const decipher = await crypto.aes.create(key128, IV) + const decryptedBuffer = await cipher.decrypt(encryptedBuffer) + + console.log(decryptedBuffer) + // prints: + + console.log(decryptedBuffer.toString('utf-8')) + // prints: Hello, world! +} + +main() +``` + +### `crypto.hmac` + +Exposes an interface to the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198. An HMAC is a cryptographic hash that uses a key to sign a message. The receiver verifies the hash by recomputing it using the same key. + +#### `crypto.hmac.create(hash, secret)` + +- `hash: String` +- `secret: Uint8Array` + +Returns `Promise<{digest}>` + +##### `digest(data)` + +- `data: Uint8Array` + +Returns `Promise` + +Example: + +```js +const crypto = require('libp2p-crypto') + +async function main () { + const hash = 'SHA1' // 'SHA256' || 'SHA512' + const hmac = await crypto.hmac.create(hash, uint8ArrayFromString('secret')) + const sig = await hmac.digest(uint8ArrayFromString('hello world')) + console.log(sig) +} + +main() +``` + +### `crypto.keys` + +**Supported Key Types** + +The [`generateKeyPair`](#generatekeypairtype-bits), [`marshalPublicKey`](#marshalpublickeykey-type), and [`marshalPrivateKey`](#marshalprivatekeykey-type) functions accept a string `type` argument. + +Currently the `'RSA'`, `'ed25519'`, and `secp256k1` types are supported, although ed25519 and secp256k1 keys support only signing and verification of messages. For encryption / decryption support, RSA keys should be used. + +### `crypto.keys.generateKeyPair(type, bits)` + +- `type: String`, see [Supported Key Types](#supported-key-types) above. +- `bits: Number` Minimum of 1024 + +Returns `Promise<{privateKey, publicKey}>` + +Generates a keypair of the given type and bitsize. + +### `crypto.keys.generateEphemeralKeyPair(curve)` + +- `curve: String`, one of `'P-256'`, `'P-384'`, `'P-521'` is currently supported + +Returns `Promise` + +Generates an ephemeral public key and returns a function that will compute the shared secret key. + +Focuses only on ECDH now, but can be made more general in the future. + +Resolves to an object of the form: + +```js +{ + key: Uint8Array, + genSharedKey: Function +} +``` + +### `crypto.keys.keyStretcher(cipherType, hashType, secret)` + +- `cipherType: String`, one of `'AES-128'`, `'AES-256'`, `'Blowfish'` +- `hashType: String`, one of `'SHA1'`, `SHA256`, `SHA512` +- `secret: Uint8Array` + +Returns `Promise` + +Generates a set of keys for each party by stretching the shared key. + +Resolves to an object of the form: + +```js +{ + k1: { + iv: Uint8Array, + cipherKey: Uint8Array, + macKey: Uint8Array + }, + k2: { + iv: Uint8Array, + cipherKey: Uint8Array, + macKey: Uint8Array + } +} +``` + +### `crypto.keys.marshalPublicKey(key, [type])` + +- `key: keys.rsa.RsaPublicKey | keys.ed25519.Ed25519PublicKey | keys.secp256k1.Secp256k1PublicKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. Defaults to 'rsa'. + +Returns `Uint8Array` + +Converts a public key object into a protobuf serialized public key. + +### `crypto.keys.unmarshalPublicKey(buf)` + +- `buf: Uint8Array` + +Returns `RsaPublicKey|Ed25519PublicKey|Secp256k1PublicKey` + +Converts a protobuf serialized public key into its representative object. + +### `crypto.keys.marshalPrivateKey(key, [type])` + +- `key: keys.rsa.RsaPrivateKey | keys.ed25519.Ed25519PrivateKey | keys.secp256k1.Secp256k1PrivateKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. + +Returns `Uint8Array` + +Converts a private key object into a protobuf serialized private key. + +### `crypto.keys.unmarshalPrivateKey(buf)` + +- `buf: Uint8Array` + +Returns `Promise` + +Converts a protobuf serialized private key into its representative object. + +### `crypto.keys.import(encryptedKey, password)` + +- `encryptedKey: string` +- `password: string` + +Returns `Promise` + +Converts an exported private key into its representative object. Supported formats are 'pem' (RSA only) and 'libp2p-key'. + +### `privateKey.export(password, format)` + +- `password: string` +- `format: string` the format to export to: 'pem' (rsa only), 'libp2p-key' + +Returns `string` + +Exports the password protected `PrivateKey`. RSA keys will be exported as password protected PEM by default. Ed25519 and Secp256k1 keys will be exported as password protected AES-GCM base64 encoded strings ('libp2p-key' format). + +### `crypto.randomBytes(number)` + +- `number: Number` + +Returns `Uint8Array` + +Generates a Uint8Array with length `number` populated by random bytes. + +### `crypto.pbkdf2(password, salt, iterations, keySize, hash)` + +- `password: String` +- `salt: String` +- `iterations: Number` +- `keySize: Number` in bytes +- `hash: String` the hashing algorithm ('sha1', 'sha2-512', ...) + +Computes the Password Based Key Derivation Function 2; returning a new password. + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-crypto/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/crypto/benchmark/ed25519/compat.cjs b/packages/crypto/benchmark/ed25519/compat.cjs new file mode 100644 index 0000000000..e8e31e4a47 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/compat.cjs @@ -0,0 +1,201 @@ +/* eslint-disable no-console */ +// @ts-expect-error types are missing +const forge = require('node-forge/lib/forge') + +/* + * Make sure that every Ed25519 implementation can use keys generated + * by every other implementation to sign and verify messages signed + * by themselves and by every other implementation using those keys. + * + * Nb. some modules return different structures from their key generation + * routine - we normalise to `{ privateKey: seed, publicKey }`. + * + * Most implementations return the seed as the private key but supercop.wasm + * returns a hash of the seed. We ignore supercop's private key in favour + * of the seed here, since we can re-create it using the createKeyPair + * function because key generation is deterministic for a given seed. + */ + +const { concat } = require('uint8arrays/concat') +const { fromString } = require('uint8arrays/from-string') + +const native = require('ed25519') +const noble = require('@noble/ed25519') +const { randomBytes } = noble.utils +const { subtle } = require('crypto').webcrypto +require('node-forge/lib/ed25519') +const stable = require('@stablelib/ed25519') +const supercopWasm = require('supercop.wasm') + +const ALGORITHM = 'NODE-ED25519' +const ED25519_PKCS8_PREFIX = fromString('302e020100300506032b657004220420', 'hex') + +const implementations = [{ + name: '@noble/ed25519', + before: () => {}, + generateKeyPair: async () => { + const privateKey = noble.utils.randomPrivateKey() + const publicKey = await noble.getPublicKey(privateKey) + + return { + privateKey, + publicKey + } + }, + sign: (message, keyPair) => noble.sign(message, keyPair.privateKey), + verify: (message, signature, keyPair) => noble.verify(signature, message, keyPair.publicKey) +}, { + name: '@stablelib/ed25519', + before: () => {}, + generateKeyPair: async () => { + const key = stable.generateKeyPair() + + return { + privateKey: key.secretKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => stable.sign(concat([keyPair.privateKey, keyPair.publicKey]), message), + verify: (message, signature, keyPair) => stable.verify(keyPair.publicKey, message, signature) +}, { + name: 'node-forge/ed25519', + before: () => {}, + generateKeyPair: async () => { + const seed = randomBytes(32) + const key = await forge.pki.ed25519.generateKeyPair({ seed }) + + return { + privateKey: key.privateKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => forge.pki.ed25519.sign({ message, privateKey: keyPair.privateKey }), + verify: (message, signature, keyPair) => forge.pki.ed25519.verify({ signature, message, publicKey: keyPair.publicKey }) +}, { + name: 'supercop.wasm', + before: () => { + return new Promise(resolve => { + supercopWasm.ready(() => { + resolve() + }) + }) + }, + generateKeyPair: async () => { + const seed = supercopWasm.createSeed() + const key = supercopWasm.createKeyPair(seed) + + return { + privateKey: seed, + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => { + const key = supercopWasm.createKeyPair(keyPair.privateKey) + + return supercopWasm.sign(message, key.publicKey, key.secretKey) + }, + verify: (message, signature, keyPair) => { + return supercopWasm.verify(signature, message, keyPair.publicKey) + } +}, { + name: 'native Ed25519', + generateKeyPair: async () => { + const seed = randomBytes(32) + const key = native.MakeKeypair(seed) + + return { + privateKey: key.privateKey.subarray(0, 32), + publicKey: key.publicKey + } + }, + sign: (message, keyPair) => native.Sign(message, keyPair.privateKey), + verify: (message, signature, keyPair) => native.Verify(message, signature, keyPair.publicKey) +}, { + name: 'node.js web crypto', + generateKeyPair: async () => { + const key = await subtle.generateKey({ + name: 'NODE-ED25519', + namedCurve: 'NODE-ED25519' + }, true, ['sign', 'verify']) + const jwk = await subtle.exportKey('jwk', key.privateKey) + + return { + privateKey: fromString(jwk.d, 'base64url'), + publicKey: fromString(jwk.x, 'base64url') + } + }, + sign: async (message, keyPair) => { + const pkcs8 = concat([ + ED25519_PKCS8_PREFIX, + keyPair.privateKey + ], ED25519_PKCS8_PREFIX.length + 32) + const cryptoKey = await subtle.importKey('pkcs8', pkcs8, { + name: ALGORITHM, + namedCurve: ALGORITHM + }, true, ['sign']) + + const signature = await subtle.sign(ALGORITHM, cryptoKey, message) + + return new Uint8Array(signature) + }, + verify: async (message, signature, keyPair) => { + const cryptoKey = await subtle.importKey('raw', keyPair.publicKey, { + name: ALGORITHM, + namedCurve: ALGORITHM, + public: true + }, true, ['verify']) + + return subtle.verify(ALGORITHM, cryptoKey, signature, message) + } +}] + +async function test (a, b) { + console.info(`test ${a.name} against ${b.name}`) + const message = Buffer.from('hello world ' + Math.random()) + + const keyPair = await a.generateKeyPair() + + if (keyPair.privateKey.length !== 32) { + throw new Error('Private key not 32 bytes') + } + + if (keyPair.publicKey.length !== 32) { + throw new Error('Public key not 32 bytes') + } + + // make sure we can sign and verify with keys created by the other implementation + const pairs = [[a, a], [a, b], [b, a], [b, b]] + + for (const [a, b] of pairs) { + console.info('test', a.name, 'against', b.name) + const signature = await a.sign(message, keyPair) + const isSigned = await b.verify(message, signature, keyPair) + + if (!isSigned) { + console.error(`${b.name} could not verify signature created by ${a.name}`) + } + } +} + +async function main () { + for (const a of implementations) { + if (a.before) { + await a.before() + } + + for (const b of implementations) { + if (b.before) { + await b.before() + } + + await test(a, b) + await test(b, a) + } + } +} + +main() + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/packages/crypto/benchmark/ed25519/index.js b/packages/crypto/benchmark/ed25519/index.js new file mode 100644 index 0000000000..c0f554e8a8 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/index.js @@ -0,0 +1,138 @@ +/* eslint-disable no-console */ +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import Benchmark from 'benchmark' +import native from 'ed25519' +import * as noble from '@noble/ed25519' +import 'node-forge/lib/ed25519.js' +import stable from '@stablelib/ed25519' +import supercopWasm from 'supercop.wasm' +import ed25519WasmPro from 'ed25519-wasm-pro' +import * as libp2pCrypto from '../../dist/src/index.js' + +const { randomBytes } = noble.utils + +const suite = new Benchmark.Suite('ed25519 implementations') + +suite.add('@libp2p/crypto', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + + const key = await libp2pCrypto.keys.generateKeyPair('Ed25519') + + const signature = await key.sign(message) + const res = await key.public.verify(message, signature) + + if (!res) { + throw new Error('could not verify @libp2p/crypto signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('@noble/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const privateKey = noble.utils.randomPrivateKey() + const publicKey = await noble.getPublicKey(privateKey) + const signature = await noble.sign(message, privateKey) + const isSigned = await noble.verify(signature, message, publicKey) + + if (!isSigned) { + throw new Error('could not verify noble signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('@stablelib/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const key = stable.generateKeyPair() + const signature = await stable.sign(key.secretKey, message) + const isSigned = await stable.verify(key.publicKey, message, signature) + + if (!isSigned) { + throw new Error('could not verify stablelib signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('node-forge/ed25519', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = randomBytes(32) + const key = await forge.pki.ed25519.generateKeyPair({ seed }) + const signature = await forge.pki.ed25519.sign({ message, privateKey: key.privateKey }) + const res = await forge.pki.ed25519.verify({ signature, message, publicKey: key.publicKey }) + + if (!res) { + throw new Error('could not verify node-forge signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('supercop.wasm', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = supercopWasm.createSeed() + const keys = supercopWasm.createKeyPair(seed) + const signature = supercopWasm.sign(message, keys.publicKey, keys.secretKey) + const isSigned = await supercopWasm.verify(signature, message, keys.publicKey) + + if (!isSigned) { + throw new Error('could not verify supercop.wasm signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('ed25519-wasm-pro', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = ed25519WasmPro.createSeed() + const keys = ed25519WasmPro.createKeyPair(seed) + const signature = ed25519WasmPro.sign(message, keys.publicKey, keys.secretKey) + const isSigned = await ed25519WasmPro.verify(signature, message, keys.publicKey) + + if (!isSigned) { + throw new Error('could not verify ed25519-wasm-pro signature') + } + + d.resolve() +}, { defer: true }) + +suite.add('ed25519 (native module)', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + const seed = randomBytes(32) + const key = native.MakeKeypair(seed) + const signature = native.Sign(message, key) + const res = native.Verify(message, signature, key.publicKey) + + if (!res) { + throw new Error('could not verify native signature') + } + + d.resolve() +}, { defer: true }) + +async function main () { + await Promise.all([ + new Promise((resolve) => { + supercopWasm.ready(() => resolve()) + }), + new Promise((resolve) => { + ed25519WasmPro.ready(() => resolve()) + }) + ]) + noble.utils.precompute(8) + + suite + .on('cycle', (event) => console.log(String(event.target))) + .on('complete', function () { + console.log('fastest is ' + this.filter('fastest').map('name')) + }) + .run({ async: true }) +} + +main() + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/packages/crypto/benchmark/ed25519/package.json b/packages/crypto/benchmark/ed25519/package.json new file mode 100644 index 0000000000..530a3d9c44 --- /dev/null +++ b/packages/crypto/benchmark/ed25519/package.json @@ -0,0 +1,20 @@ +{ + "name": "libp2p-crypto-ed25519-benchmarks", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node .", + "compat": "node compat.js" + }, + "license": "MIT", + "dependencies": { + "@noble/ed25519": "^1.3.0", + "@stablelib/ed25519": "^1.0.2", + "benchmark": "^2.1.4", + "ed25519": "^0.0.5", + "ed25519-wasm-pro": "^1.1.1", + "node-forge": "^1.0.0", + "supercop.wasm": "^5.0.1" + } +} diff --git a/packages/crypto/benchmark/ephemeral-keys.cjs b/packages/crypto/benchmark/ephemeral-keys.cjs new file mode 100644 index 0000000000..b438489ea6 --- /dev/null +++ b/packages/crypto/benchmark/ephemeral-keys.cjs @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('ephemeral-keys') + +const secrets = [] +const curves = ['P-256', 'P-384', 'P-521'] + +curves.forEach((curve) => { + suite.add(`ephemeral key with secrect ${curve}`, async (d) => { + const res = await crypto.keys.generateEphemeralKeyPair('P-256') + const secret = await res.genSharedKey(res.key) + secrets.push(secret) + d.resolve() + }, { defer: true }) +}) + +suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) diff --git a/packages/crypto/benchmark/key-stretcher.cjs b/packages/crypto/benchmark/key-stretcher.cjs new file mode 100644 index 0000000000..0ff11e50ea --- /dev/null +++ b/packages/crypto/benchmark/key-stretcher.cjs @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('key-stretcher') + +const keys = [] + +const ciphers = ['AES-128', 'AES-256', 'Blowfish'] +const hashes = ['SHA1', 'SHA256', 'SHA512'] + +;(async () => { + const res = await crypto.keys.generateEphemeralKeyPair('P-256') + const secret = await res.genSharedKey(res.key) + + ciphers.forEach((cipher) => hashes.forEach((hash) => { + setup(cipher, hash, secret) + })) + + suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) +})() + +function setup (cipher, hash, secret) { + suite.add(`keyStretcher ${cipher} ${hash}`, async (d) => { + const k = await crypto.keys.keyStretcher(cipher, hash, secret) + keys.push(k) + d.resolve() + }, { defer: true }) +} diff --git a/packages/crypto/benchmark/rsa.cjs b/packages/crypto/benchmark/rsa.cjs new file mode 100644 index 0000000000..7101a40fa7 --- /dev/null +++ b/packages/crypto/benchmark/rsa.cjs @@ -0,0 +1,36 @@ +/* eslint-disable no-console */ +const crypto = require('../dist/src/index.js') + +const Benchmark = require('benchmark') + +const suite = new Benchmark.Suite('rsa') + +const keys = [] +const bits = [1024, 2048, 4096] + +bits.forEach((bit) => { + suite.add(`generateKeyPair ${bit}bits`, async (d) => { + const key = await crypto.keys.generateKeyPair('RSA', bit) + keys.push(key) + d.resolve() + }, { + defer: true + }) +}) + +suite.add('sign and verify', async (d) => { + const key = keys[0] + const text = key.genSecret() + + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + + if (res !== true) { throw new Error('failed to verify') } + d.resolve() +}, { + defer: true +}) + +suite + .on('cycle', (event) => console.log(String(event.target))) + .run({ async: true }) diff --git a/packages/crypto/package.json b/packages/crypto/package.json new file mode 100644 index 0000000000..f9d7831dec --- /dev/null +++ b/packages/crypto/package.json @@ -0,0 +1,119 @@ +{ + "name": "@libp2p/crypto", + "version": "1.0.17", + "description": "Crypto primitives for libp2p", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/crypto#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "crypto", + "libp2p", + "rsa", + "secp256k1" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./aes": { + "types": "./dist/src/aes/index.d.ts", + "import": "./dist/src/aes/index.js" + }, + "./ciphers": { + "types": "./dist/src/ciphers/index.d.ts", + "import": "./dist/src/ciphers/index.js" + }, + "./hmac": { + "types": "./dist/src/hmac/index.d.ts", + "import": "./dist/src/hmac/index.js" + }, + "./keys": { + "types": "./dist/src/keys/index.d.ts", + "import": "./dist/src/keys/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/*.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:webkit": "bash -c '[ \"${CI}\" == \"true\" ] && playwright install-deps'; aegir test -t browser -- --browser webkit", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "generate": "protons ./src/keys/keys.proto" + }, + "dependencies": { + "@libp2p/interface-keys": "^1.0.0", + "@libp2p/interfaces": "^3.0.0", + "@noble/ed25519": "^1.6.0", + "@noble/secp256k1": "^1.5.4", + "multiformats": "^11.0.2", + "node-forge": "^1.1.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/mocha": "^10.0.0", + "aegir": "^39.0.10", + "benchmark": "^2.1.4", + "protons": "^7.0.2" + }, + "browser": { + "./dist/src/aes/ciphers.js": "./dist/src/aes/ciphers-browser.js", + "./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js", + "./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js", + "./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js", + "./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js", + "./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/crypto/src/aes/cipher-mode.ts b/packages/crypto/src/aes/cipher-mode.ts new file mode 100644 index 0000000000..b420deed54 --- /dev/null +++ b/packages/crypto/src/aes/cipher-mode.ts @@ -0,0 +1,15 @@ +import { CodeError } from '@libp2p/interfaces/errors' + +const CIPHER_MODES = { + 16: 'aes-128-ctr', + 32: 'aes-256-ctr' +} + +export function cipherMode (key: Uint8Array): string { + if (key.length === 16 || key.length === 32) { + return CIPHER_MODES[key.length] + } + + const modes = Object.entries(CIPHER_MODES).map(([k, v]) => `${k} (${v})`).join(' / ') + throw new CodeError(`Invalid key length ${key.length} bytes. Must be ${modes}`, 'ERR_INVALID_KEY_LENGTH') +} diff --git a/packages/crypto/src/aes/ciphers-browser.ts b/packages/crypto/src/aes/ciphers-browser.ts new file mode 100644 index 0000000000..88d18d7d95 --- /dev/null +++ b/packages/crypto/src/aes/ciphers-browser.ts @@ -0,0 +1,32 @@ + +import 'node-forge/lib/aes.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +export interface Cipher { + update: (data: Uint8Array) => Uint8Array +} + +export function createCipheriv (mode: any, key: Uint8Array, iv: Uint8Array): Cipher { + const cipher2 = forge.cipher.createCipher('AES-CTR', uint8ArrayToString(key, 'ascii')) + cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) + return { + update: (data: Uint8Array) => { + cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) + return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') + } + } +} + +export function createDecipheriv (mode: any, key: Uint8Array, iv: Uint8Array): Cipher { + const cipher2 = forge.cipher.createDecipher('AES-CTR', uint8ArrayToString(key, 'ascii')) + cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) + return { + update: (data: Uint8Array) => { + cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) + return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') + } + } +} diff --git a/packages/crypto/src/aes/ciphers.ts b/packages/crypto/src/aes/ciphers.ts new file mode 100644 index 0000000000..c1a2cd74a5 --- /dev/null +++ b/packages/crypto/src/aes/ciphers.ts @@ -0,0 +1,4 @@ +import crypto from 'crypto' + +export const createCipheriv = crypto.createCipheriv +export const createDecipheriv = crypto.createDecipheriv diff --git a/packages/crypto/src/aes/index.ts b/packages/crypto/src/aes/index.ts new file mode 100644 index 0000000000..0023ca5496 --- /dev/null +++ b/packages/crypto/src/aes/index.ts @@ -0,0 +1,25 @@ +import { cipherMode } from './cipher-mode.js' +import * as ciphers from './ciphers.js' + +export interface AESCipher { + encrypt: (data: Uint8Array) => Promise + decrypt: (data: Uint8Array) => Promise +} + +export async function create (key: Uint8Array, iv: Uint8Array): Promise { // eslint-disable-line require-await + const mode = cipherMode(key) + const cipher = ciphers.createCipheriv(mode, key, iv) + const decipher = ciphers.createDecipheriv(mode, key, iv) + + const res: AESCipher = { + async encrypt (data) { // eslint-disable-line require-await + return cipher.update(data) + }, + + async decrypt (data) { // eslint-disable-line require-await + return decipher.update(data) + } + } + + return res +} diff --git a/packages/crypto/src/ciphers/aes-gcm.browser.ts b/packages/crypto/src/ciphers/aes-gcm.browser.ts new file mode 100644 index 0000000000..e238e20be7 --- /dev/null +++ b/packages/crypto/src/ciphers/aes-gcm.browser.ts @@ -0,0 +1,109 @@ +import { concat } from 'uint8arrays/concat' +import { fromString } from 'uint8arrays/from-string' +import webcrypto from '../webcrypto.js' +import type { CreateOptions, AESCipher } from './interface.js' + +// WebKit on Linux does not support deriving a key from an empty PBKDF2 key. +// So, as a workaround, we provide the generated key as a constant. We test that +// this generated key is accurate in test/workaround.spec.ts +// Generated via: +// await crypto.subtle.exportKey('jwk', +// await crypto.subtle.deriveKey( +// { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } }, +// await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']), +// { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']) +// ) +export const derivedEmptyPasswordKey = { alg: 'A128GCM', ext: true, k: 'scm9jmO_4BJAgdwWGVulLg', key_ops: ['encrypt', 'decrypt'], kty: 'oct' } + +// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples + +export function create (opts?: CreateOptions): AESCipher { + const algorithm = opts?.algorithm ?? 'AES-GCM' + let keyLength = opts?.keyLength ?? 16 + const nonceLength = opts?.nonceLength ?? 12 + const digest = opts?.digest ?? 'SHA-256' + const saltLength = opts?.saltLength ?? 16 + const iterations = opts?.iterations ?? 32767 + + const crypto = webcrypto.get() + keyLength *= 8 // Browser crypto uses bits instead of bytes + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to encrypt the data. + */ + async function encrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + const salt = crypto.getRandomValues(new Uint8Array(saltLength)) + const nonce = crypto.getRandomValues(new Uint8Array(nonceLength)) + const aesGcm = { name: algorithm, iv: nonce } + + if (typeof password === 'string') { + password = fromString(password) + } + + let cryptoKey: CryptoKey + if (password.length === 0) { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt']) + try { + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['encrypt']) + } catch { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt']) + } + } else { + // Derive a key using PBKDF2. + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt']) + } + + // Encrypt the string. + const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data) + return concat([salt, aesGcm.iv, new Uint8Array(ciphertext)]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to decrypt the data. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + */ + async function decrypt (data: Uint8Array, password: string | Uint8Array): Promise { + const salt = data.subarray(0, saltLength) + const nonce = data.subarray(saltLength, saltLength + nonceLength) + const ciphertext = data.subarray(saltLength + nonceLength) + const aesGcm = { name: algorithm, iv: nonce } + + if (typeof password === 'string') { + password = fromString(password) + } + + let cryptoKey: CryptoKey + if (password.length === 0) { + try { + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['decrypt']) + } catch { + cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['decrypt']) + } + } else { + // Derive the key using PBKDF2. + const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } + const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey']) + cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt']) + } + + // Decrypt the string. + const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext) + return new Uint8Array(plaintext) + } + + const cipher: AESCipher = { + encrypt, + decrypt + } + + return cipher +} diff --git a/packages/crypto/src/ciphers/aes-gcm.ts b/packages/crypto/src/ciphers/aes-gcm.ts new file mode 100644 index 0000000000..0f217d4cb9 --- /dev/null +++ b/packages/crypto/src/ciphers/aes-gcm.ts @@ -0,0 +1,102 @@ +import crypto from 'crypto' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { CreateOptions, AESCipher } from './interface.js' + +// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples + +export function create (opts?: CreateOptions): AESCipher { + const algorithm = opts?.algorithm ?? 'aes-128-gcm' + const keyLength = opts?.keyLength ?? 16 + const nonceLength = opts?.nonceLength ?? 12 + const digest = opts?.digest ?? 'sha256' + const saltLength = opts?.saltLength ?? 16 + const iterations = opts?.iterations ?? 32767 + const algorithmTagLength = opts?.algorithmTagLength ?? 16 + + async function encryptWithKey (data: Uint8Array, key: Uint8Array): Promise { // eslint-disable-line require-await + const nonce = crypto.randomBytes(nonceLength) + + // Create the cipher instance. + const cipher = crypto.createCipheriv(algorithm, key, nonce) + + // Encrypt and prepend nonce. + const ciphertext = uint8ArrayConcat([cipher.update(data), cipher.final()]) + + // @ts-expect-error getAuthTag is not a function + return uint8ArrayConcat([nonce, ciphertext, cipher.getAuthTag()]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to encrypt the data. + */ + async function encrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + // Generate a 128-bit salt using a CSPRNG. + const salt = crypto.randomBytes(saltLength) + + if (typeof password === 'string') { + password = uint8ArrayFromString(password) + } + + // Derive a key using PBKDF2. + const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + + // Encrypt and prepend salt. + return uint8ArrayConcat([salt, await encryptWithKey(Uint8Array.from(data), key)]) + } + + /** + * Decrypts the given cipher text with the provided key. The `key` should + * be a cryptographically safe key and not a plaintext password. To use + * a plaintext password, use `decrypt`. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + */ + async function decryptWithKey (ciphertextAndNonce: Uint8Array, key: Uint8Array): Promise { // eslint-disable-line require-await + // Create Uint8Arrays of nonce, ciphertext and tag. + const nonce = ciphertextAndNonce.subarray(0, nonceLength) + const ciphertext = ciphertextAndNonce.subarray(nonceLength, ciphertextAndNonce.length - algorithmTagLength) + const tag = ciphertextAndNonce.subarray(ciphertext.length + nonceLength) + + // Create the cipher instance. + const cipher = crypto.createDecipheriv(algorithm, key, nonce) + + // Decrypt and return result. + // @ts-expect-error getAuthTag is not a function + cipher.setAuthTag(tag) + return uint8ArrayConcat([cipher.update(ciphertext), cipher.final()]) + } + + /** + * Uses the provided password to derive a pbkdf2 key. The key + * will then be used to decrypt the data. The options used to create + * this decryption cipher must be the same as those used to create + * the encryption cipher. + * + * @param {Uint8Array} data - The data to decrypt + * @param {string|Uint8Array} password - A plain password + */ + async function decrypt (data: Uint8Array, password: string | Uint8Array): Promise { // eslint-disable-line require-await + // Create Uint8Arrays of salt and ciphertextAndNonce. + const salt = data.subarray(0, saltLength) + const ciphertextAndNonce = data.subarray(saltLength) + + if (typeof password === 'string') { + password = uint8ArrayFromString(password) + } + + // Derive the key using PBKDF2. + const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + + // Decrypt and return result. + return decryptWithKey(ciphertextAndNonce, key) + } + + const cipher: AESCipher = { + encrypt, + decrypt + } + + return cipher +} diff --git a/packages/crypto/src/ciphers/interface.ts b/packages/crypto/src/ciphers/interface.ts new file mode 100644 index 0000000000..08ce7b6b86 --- /dev/null +++ b/packages/crypto/src/ciphers/interface.ts @@ -0,0 +1,15 @@ + +export interface CreateOptions { + algorithm?: string + nonceLength?: number + keyLength?: number + digest?: string + saltLength?: number + iterations?: number + algorithmTagLength?: number +} + +export interface AESCipher { + encrypt: (data: Uint8Array, password: string | Uint8Array) => Promise + decrypt: (data: Uint8Array, password: string | Uint8Array) => Promise +} diff --git a/packages/crypto/src/hmac/index-browser.ts b/packages/crypto/src/hmac/index-browser.ts new file mode 100644 index 0000000000..0a5d91fd5c --- /dev/null +++ b/packages/crypto/src/hmac/index-browser.ts @@ -0,0 +1,35 @@ +import webcrypto from '../webcrypto.js' +import lengths from './lengths.js' + +const hashTypes = { + SHA1: 'SHA-1', + SHA256: 'SHA-256', + SHA512: 'SHA-512' +} + +const sign = async (key: CryptoKey, data: Uint8Array): Promise => { + const buf = await webcrypto.get().subtle.sign({ name: 'HMAC' }, key, data) + return new Uint8Array(buf, 0, buf.byteLength) +} + +export async function create (hashType: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise<{ digest: (data: Uint8Array) => Promise, length: number }> { + const hash = hashTypes[hashType] + + const key = await webcrypto.get().subtle.importKey( + 'raw', + secret, + { + name: 'HMAC', + hash: { name: hash } + }, + false, + ['sign'] + ) + + return { + async digest (data: Uint8Array) { // eslint-disable-line require-await + return sign(key, data) + }, + length: lengths[hashType] + } +} diff --git a/packages/crypto/src/hmac/index.ts b/packages/crypto/src/hmac/index.ts new file mode 100644 index 0000000000..a0a802f13c --- /dev/null +++ b/packages/crypto/src/hmac/index.ts @@ -0,0 +1,20 @@ +import crypto from 'crypto' +import lengths from './lengths.js' + +export interface HMAC { + digest: (data: Uint8Array) => Promise + length: number +} + +export async function create (hash: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise { + const res = { + async digest (data: Uint8Array) { // eslint-disable-line require-await + const hmac = crypto.createHmac(hash.toLowerCase(), secret) + hmac.update(data) + return hmac.digest() + }, + length: lengths[hash] + } + + return res +} diff --git a/packages/crypto/src/hmac/lengths.ts b/packages/crypto/src/hmac/lengths.ts new file mode 100644 index 0000000000..396bafc780 --- /dev/null +++ b/packages/crypto/src/hmac/lengths.ts @@ -0,0 +1,6 @@ + +export default { + SHA1: 20, + SHA256: 32, + SHA512: 64 +} diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts new file mode 100644 index 0000000000..9e368358eb --- /dev/null +++ b/packages/crypto/src/index.ts @@ -0,0 +1,11 @@ +import * as aes from './aes/index.js' +import * as hmac from './hmac/index.js' +import * as keys from './keys/index.js' +import pbkdf2 from './pbkdf2.js' +import randomBytes from './random-bytes.js' + +export { aes } +export { hmac } +export { keys } +export { randomBytes } +export { pbkdf2 } diff --git a/packages/crypto/src/keys/ecdh-browser.ts b/packages/crypto/src/keys/ecdh-browser.ts new file mode 100644 index 0000000000..c371429f33 --- /dev/null +++ b/packages/crypto/src/keys/ecdh-browser.ts @@ -0,0 +1,137 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { base64urlToBuffer } from '../util.js' +import webcrypto from '../webcrypto.js' +import type { ECDHKey, ECDHKeyPair, JWKEncodedPrivateKey, JWKEncodedPublicKey } from './interface.js' + +const bits = { + 'P-256': 256, + 'P-384': 384, + 'P-521': 521 +} + +const curveTypes = Object.keys(bits) +const names = curveTypes.join(' / ') + +export async function generateEphmeralKeyPair (curve: string): Promise { + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const pair = await webcrypto.get().subtle.generateKey( + { + name: 'ECDH', + namedCurve: curve + }, + true, + ['deriveBits'] + ) + + // forcePrivate is used for testing only + const genSharedKey = async (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair): Promise => { + let privateKey + + if (forcePrivate != null) { + privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + unmarshalPrivateKey(curve, forcePrivate), + { + name: 'ECDH', + namedCurve: curve + }, + false, + ['deriveBits'] + ) + } else { + privateKey = pair.privateKey + } + + const key = await webcrypto.get().subtle.importKey( + 'jwk', + unmarshalPublicKey(curve, theirPub), + { + name: 'ECDH', + namedCurve: curve + }, + false, + [] + ) + + const buffer = await webcrypto.get().subtle.deriveBits( + { + name: 'ECDH', + // @ts-expect-error namedCurve is missing from the types + namedCurve: curve, + public: key + }, + privateKey, + bits[curve] + ) + + return new Uint8Array(buffer, 0, buffer.byteLength) + } + + const publicKey = await webcrypto.get().subtle.exportKey('jwk', pair.publicKey) + + const ecdhKey: ECDHKey = { + key: marshalPublicKey(publicKey), + genSharedKey + } + + return ecdhKey +} + +const curveLengths = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} + +// Marshal converts a jwk encoded ECDH public key into the +// form specified in section 4.3.6 of ANSI X9.62. (This is the format +// go-ipfs uses) +function marshalPublicKey (jwk: JsonWebKey): Uint8Array { + if (jwk.crv == null || jwk.x == null || jwk.y == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + if (jwk.crv !== 'P-256' && jwk.crv !== 'P-384' && jwk.crv !== 'P-521') { + throw new CodeError(`Unknown curve: ${jwk.crv}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const byteLen = curveLengths[jwk.crv] + + return uint8ArrayConcat([ + Uint8Array.from([4]), // uncompressed point + base64urlToBuffer(jwk.x, byteLen), + base64urlToBuffer(jwk.y, byteLen) + ], 1 + byteLen * 2) +} + +// Unmarshal converts a point, serialized by Marshal, into an jwk encoded key +function unmarshalPublicKey (curve: string, key: Uint8Array): JWKEncodedPublicKey { + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const byteLen = curveLengths[curve] + + if (!uint8ArrayEquals(key.subarray(0, 1), Uint8Array.from([4]))) { + throw new CodeError('Cannot unmarshal public key - invalid key format', 'ERR_INVALID_KEY_FORMAT') + } + + return { + kty: 'EC', + crv: curve, + x: uint8ArrayToString(key.subarray(1, byteLen + 1), 'base64url'), + y: uint8ArrayToString(key.subarray(1 + byteLen), 'base64url'), + ext: true + } +} + +const unmarshalPrivateKey = (curve: string, key: ECDHKeyPair): JWKEncodedPrivateKey => ({ + ...unmarshalPublicKey(curve, key.public), + d: uint8ArrayToString(key.private, 'base64url') +}) diff --git a/packages/crypto/src/keys/ecdh.ts b/packages/crypto/src/keys/ecdh.ts new file mode 100644 index 0000000000..5e139f4e08 --- /dev/null +++ b/packages/crypto/src/keys/ecdh.ts @@ -0,0 +1,33 @@ +import crypto from 'crypto' +import { CodeError } from '@libp2p/interfaces/errors' +import type { ECDHKey, ECDHKeyPair } from './interface.js' + +const curves = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1' +} + +const curveTypes = Object.keys(curves) +const names = curveTypes.join(' / ') + +export async function generateEphmeralKeyPair (curve: string): Promise { // eslint-disable-line require-await + if (curve !== 'P-256' && curve !== 'P-384' && curve !== 'P-521') { + throw new CodeError(`Unknown curve: ${curve}. Must be ${names}`, 'ERR_INVALID_CURVE') + } + + const ecdh = crypto.createECDH(curves[curve]) + ecdh.generateKeys() + + return { + key: ecdh.getPublicKey() as Uint8Array, + + async genSharedKey (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair): Promise { // eslint-disable-line require-await + if (forcePrivate != null) { + ecdh.setPrivateKey(forcePrivate.private) + } + + return ecdh.computeSecret(theirPub) + } + } +} diff --git a/packages/crypto/src/keys/ed25519-browser.ts b/packages/crypto/src/keys/ed25519-browser.ts new file mode 100644 index 0000000000..8d2f6de154 --- /dev/null +++ b/packages/crypto/src/keys/ed25519-browser.ts @@ -0,0 +1,64 @@ +import * as ed from '@noble/ed25519' +import type { Uint8ArrayKeyPair } from './interface' + +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 + +export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +export async function generateKey (): Promise { + // the actual private key (32 bytes) + const privateKeyRaw = ed.utils.randomPrivateKey() + const publicKey = await ed.getPublicKey(privateKeyRaw) + + // concatenated the public key to the private key + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +/** + * Generate keypair from a 32 byte uint8array + */ +export async function generateKeyFromSeed (seed: Uint8Array): Promise { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const privateKeyRaw = seed + const publicKey = await ed.getPublicKey(privateKeyRaw) + + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array): Promise { + const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH) + + return ed.sign(msg, privateKeyRaw) +} + +export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + return ed.verify(sig, msg, publicKey) +} + +function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array { + const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey +} diff --git a/packages/crypto/src/keys/ed25519-class.ts b/packages/crypto/src/keys/ed25519-class.ts new file mode 100644 index 0000000000..baae1bc188 --- /dev/null +++ b/packages/crypto/src/keys/ed25519-class.ts @@ -0,0 +1,146 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { base58btc } from 'multiformats/bases/base58' +import { identity } from 'multiformats/hashes/identity' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import * as crypto from './ed25519.js' +import { exporter } from './exporter.js' +import * as pbm from './keys.js' +import type { Multibase } from 'multiformats' + +export class Ed25519PublicKey { + private readonly _key: Uint8Array + + constructor (key: Uint8Array) { + this._key = ensureKey(key, crypto.publicKeyLength) + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return pbm.PublicKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class Ed25519PrivateKey { + private readonly _key: Uint8Array + private readonly _publicKey: Uint8Array + + // key - 64 byte Uint8Array containing private key + // publicKey - 32 byte Uint8Array containing public key + constructor (key: Uint8Array, publicKey: Uint8Array) { + this._key = ensureKey(key, crypto.privateKeyLength) + this._publicKey = ensureKey(publicKey, crypto.publicKeyLength) + } + + async sign (message: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndSign(this._key, message) + } + + get public (): Ed25519PublicKey { + return new Ed25519PublicKey(this._publicKey) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return pbm.PrivateKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the identity multihash containing its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @returns {Promise} + */ + async id (): Promise { + const encoding = identity.digest(this.public.bytes) + return base58btc.encode(encoding.bytes).substring(1) + } + + /** + * Exports the key into a password protected `format` + */ + async export (password: string, format = 'libp2p-key'): Promise> { + if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export function unmarshalEd25519PrivateKey (bytes: Uint8Array): Ed25519PrivateKey { + // Try the old, redundant public key version + if (bytes.length > crypto.privateKeyLength) { + bytes = ensureKey(bytes, crypto.privateKeyLength + crypto.publicKeyLength) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.privateKeyLength, bytes.length) + return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) + } + + bytes = ensureKey(bytes, crypto.privateKeyLength) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.publicKeyLength) + return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) +} + +export function unmarshalEd25519PublicKey (bytes: Uint8Array): Ed25519PublicKey { + bytes = ensureKey(bytes, crypto.publicKeyLength) + return new Ed25519PublicKey(bytes) +} + +export async function generateKeyPair (): Promise { + const { privateKey, publicKey } = await crypto.generateKey() + return new Ed25519PrivateKey(privateKey, publicKey) +} + +export async function generateKeyPairFromSeed (seed: Uint8Array): Promise { + const { privateKey, publicKey } = await crypto.generateKeyFromSeed(seed) + return new Ed25519PrivateKey(privateKey, publicKey) +} + +function ensureKey (key: Uint8Array, length: number): Uint8Array { + key = Uint8Array.from(key ?? []) + if (key.length !== length) { + throw new CodeError(`Key must be a Uint8Array of length ${length}, got ${key.length}`, 'ERR_INVALID_KEY_TYPE') + } + return key +} diff --git a/packages/crypto/src/keys/ed25519.ts b/packages/crypto/src/keys/ed25519.ts new file mode 100644 index 0000000000..0de68a526d --- /dev/null +++ b/packages/crypto/src/keys/ed25519.ts @@ -0,0 +1,137 @@ +import crypto from 'crypto' +import { promisify } from 'util' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { toString as uint8arrayToString } from 'uint8arrays/to-string' +import type { Uint8ArrayKeyPair } from './interface.js' + +const keypair = promisify(crypto.generateKeyPair) + +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 +const SIGNATURE_BYTE_LENGTH = 64 + +export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +function derivePublicKey (privateKey: Uint8Array): Uint8Array { + const keyObject = crypto.createPrivateKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + x: '', + d: uint8arrayToString(privateKey, 'base64url'), + kty: 'OKP' + } + }) + const jwk = keyObject.export({ + format: 'jwk' + }) + + if (jwk.x == null || jwk.x === '') { + throw new Error('Could not export JWK public key') + } + + return uint8arrayFromString(jwk.x, 'base64url') +} + +export async function generateKey (): Promise { + const key = await keypair('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'jwk' }, + privateKeyEncoding: { type: 'pkcs8', format: 'jwk' } + }) + + // @ts-expect-error node types are missing jwk as a format + const privateKeyRaw = uint8arrayFromString(key.privateKey.d, 'base64url') + // @ts-expect-error node types are missing jwk as a format + const publicKeyRaw = uint8arrayFromString(key.privateKey.x, 'base64url') + + return { + privateKey: concatKeys(privateKeyRaw, publicKeyRaw), + publicKey: publicKeyRaw + } +} + +/** + * Generate keypair from a 32 byte uint8array + */ +export async function generateKeyFromSeed (seed: Uint8Array): Promise { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const publicKeyRaw = derivePublicKey(seed) + + return { + privateKey: concatKeys(seed, publicKeyRaw), + publicKey: publicKeyRaw + } +} + +export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise { + if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + let privateKey: Uint8Array + let publicKey: Uint8Array + + if (key.byteLength === PRIVATE_KEY_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = key.subarray(32) + } else if (key.byteLength === KEYS_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = derivePublicKey(privateKey) + } else { + throw new TypeError('"key" must be 64 or 32 bytes in length.') + } + + const obj = crypto.createPrivateKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + d: uint8arrayToString(privateKey, 'base64url'), + x: uint8arrayToString(publicKey, 'base64url'), + kty: 'OKP' + } + }) + + return crypto.sign(null, msg, obj) +} + +export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + if (key.byteLength !== PUBLIC_KEY_BYTE_LENGTH) { + throw new TypeError('"key" must be 32 bytes in length.') + } else if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + if (sig.byteLength !== SIGNATURE_BYTE_LENGTH) { + throw new TypeError('"sig" must be 64 bytes in length.') + } else if (!(sig instanceof Uint8Array)) { + throw new TypeError('"sig" must be a node.js Buffer, or Uint8Array.') + } + + const obj = crypto.createPublicKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + x: uint8arrayToString(key, 'base64url'), + kty: 'OKP' + } + }) + + return crypto.verify(null, msg, obj, sig) +} + +function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array { + const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey +} diff --git a/packages/crypto/src/keys/ephemeral-keys.ts b/packages/crypto/src/keys/ephemeral-keys.ts new file mode 100644 index 0000000000..f60cc0f1a8 --- /dev/null +++ b/packages/crypto/src/keys/ephemeral-keys.ts @@ -0,0 +1,9 @@ +import { generateEphmeralKeyPair } from './ecdh.js' + +/** + * Generates an ephemeral public key and returns a function that will compute + * the shared secret key. + * + * Focuses only on ECDH now, but can be made more general in the future. + */ +export default generateEphmeralKeyPair diff --git a/packages/crypto/src/keys/exporter.ts b/packages/crypto/src/keys/exporter.ts new file mode 100644 index 0000000000..db62943f31 --- /dev/null +++ b/packages/crypto/src/keys/exporter.ts @@ -0,0 +1,14 @@ +import { base64 } from 'multiformats/bases/base64' +import * as ciphers from '../ciphers/aes-gcm.js' +import type { Multibase } from 'multiformats' + +/** + * Exports the given PrivateKey as a base64 encoded string. + * The PrivateKey is encrypted via a password derived PBKDF2 key + * leveraging the aes-gcm cipher algorithm. + */ +export async function exporter (privateKey: Uint8Array, password: string): Promise> { + const cipher = ciphers.create() + const encryptedKey = await cipher.encrypt(privateKey, password) + return base64.encode(encryptedKey) +} diff --git a/packages/crypto/src/keys/importer.ts b/packages/crypto/src/keys/importer.ts new file mode 100644 index 0000000000..d26b0226c7 --- /dev/null +++ b/packages/crypto/src/keys/importer.ts @@ -0,0 +1,13 @@ +import { base64 } from 'multiformats/bases/base64' +import * as ciphers from '../ciphers/aes-gcm.js' + +/** + * Attempts to decrypt a base64 encoded PrivateKey string + * with the given password. The privateKey must have been exported + * using the same password and underlying cipher (aes-gcm) + */ +export async function importer (privateKey: string, password: string): Promise { + const encryptedKey = base64.decode(privateKey) + const cipher = ciphers.create() + return cipher.decrypt(encryptedKey, password) +} diff --git a/packages/crypto/src/keys/index.ts b/packages/crypto/src/keys/index.ts new file mode 100644 index 0000000000..7a15d0d480 --- /dev/null +++ b/packages/crypto/src/keys/index.ts @@ -0,0 +1,129 @@ +import 'node-forge/lib/asn1.js' +import 'node-forge/lib/pbe.js' +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as Ed25519 from './ed25519-class.js' +import generateEphemeralKeyPair from './ephemeral-keys.js' +import { importer } from './importer.js' +import { keyStretcher } from './key-stretcher.js' +import * as keysPBM from './keys.js' +import * as RSA from './rsa-class.js' +import * as Secp256k1 from './secp256k1-class.js' +import type { PrivateKey, PublicKey } from '@libp2p/interface-keys' + +export { keyStretcher } +export { generateEphemeralKeyPair } +export { keysPBM } + +export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1' + +export const supportedKeys = { + rsa: RSA, + ed25519: Ed25519, + secp256k1: Secp256k1 +} + +function unsupportedKey (type: string): CodeError> { + const supported = Object.keys(supportedKeys).join(' / ') + return new CodeError(`invalid or unsupported key type ${type}. Must be ${supported}`, 'ERR_UNSUPPORTED_KEY_TYPE') +} + +function typeToKey (type: string): typeof RSA | typeof Ed25519 | typeof Secp256k1 { + type = type.toLowerCase() + + if (type === 'rsa' || type === 'ed25519' || type === 'secp256k1') { + return supportedKeys[type] + } + + throw unsupportedKey(type) +} + +// Generates a keypair of the given type and bitsize +export async function generateKeyPair (type: KeyTypes, bits?: number): Promise { // eslint-disable-line require-await + return typeToKey(type).generateKeyPair(bits ?? 2048) +} + +// Generates a keypair of the given type and bitsize +// seed is a 32 byte uint8array +export async function generateKeyPairFromSeed (type: KeyTypes, seed: Uint8Array, bits?: number): Promise { // eslint-disable-line require-await + if (type.toLowerCase() !== 'ed25519') { + throw new CodeError('Seed key derivation is unimplemented for RSA or secp256k1', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE') + } + + return Ed25519.generateKeyPairFromSeed(seed) +} + +// Converts a protobuf serialized public key into its +// representative object +export function unmarshalPublicKey (buf: Uint8Array): PublicKey { + const decoded = keysPBM.PublicKey.decode(buf) + const data = decoded.Data ?? new Uint8Array() + + switch (decoded.Type) { + case keysPBM.KeyType.RSA: + return supportedKeys.rsa.unmarshalRsaPublicKey(data) + case keysPBM.KeyType.Ed25519: + return supportedKeys.ed25519.unmarshalEd25519PublicKey(data) + case keysPBM.KeyType.Secp256k1: + return supportedKeys.secp256k1.unmarshalSecp256k1PublicKey(data) + default: + throw unsupportedKey(decoded.Type ?? 'RSA') + } +} + +// Converts a public key object into a protobuf serialized public key +export function marshalPublicKey (key: { bytes: Uint8Array }, type?: string): Uint8Array { + type = (type ?? 'rsa').toLowerCase() + typeToKey(type) // check type + return key.bytes +} + +// Converts a protobuf serialized private key into its +// representative object +export async function unmarshalPrivateKey (buf: Uint8Array): Promise { // eslint-disable-line require-await + const decoded = keysPBM.PrivateKey.decode(buf) + const data = decoded.Data ?? new Uint8Array() + + switch (decoded.Type) { + case keysPBM.KeyType.RSA: + return supportedKeys.rsa.unmarshalRsaPrivateKey(data) + case keysPBM.KeyType.Ed25519: + return supportedKeys.ed25519.unmarshalEd25519PrivateKey(data) + case keysPBM.KeyType.Secp256k1: + return supportedKeys.secp256k1.unmarshalSecp256k1PrivateKey(data) + default: + throw unsupportedKey(decoded.Type ?? 'RSA') + } +} + +// Converts a private key object into a protobuf serialized private key +export function marshalPrivateKey (key: { bytes: Uint8Array }, type?: string): Uint8Array { + type = (type ?? 'rsa').toLowerCase() + typeToKey(type) // check type + return key.bytes +} + +/** + * + * @param {string} encryptedKey + * @param {string} password + */ +export async function importKey (encryptedKey: string, password: string): Promise { // eslint-disable-line require-await + try { + const key = await importer(encryptedKey, password) + return await unmarshalPrivateKey(key) + } catch (_) { + // Ignore and try the old pem decrypt + } + + // Only rsa supports pem right now + const key = forge.pki.decryptRsaPrivateKey(encryptedKey, password) + if (key === null) { + throw new CodeError('Cannot read the key, most likely the password is wrong or not a RSA key', 'ERR_CANNOT_DECRYPT_PEM') + } + let der = forge.asn1.toDer(forge.pki.privateKeyToAsn1(key)) + der = uint8ArrayFromString(der.getBytes(), 'ascii') + return supportedKeys.rsa.unmarshalRsaPrivateKey(der) +} diff --git a/packages/crypto/src/keys/interface.ts b/packages/crypto/src/keys/interface.ts new file mode 100644 index 0000000000..30bceb1dd2 --- /dev/null +++ b/packages/crypto/src/keys/interface.ts @@ -0,0 +1,35 @@ + +export interface JWKKeyPair { + privateKey: JsonWebKey + publicKey: JsonWebKey +} + +export interface Uint8ArrayKeyPair { + privateKey: Uint8Array + publicKey: Uint8Array +} + +export interface ECDHKeyPair { + private: Uint8Array + public: Uint8Array +} + +export interface ECDHKey { + key: Uint8Array + genSharedKey: (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair) => Promise +} + +export interface JWKEncodedPublicKey { kty: string, crv: 'P-256' | 'P-384' | 'P-521', x: string, y: string, ext: boolean } + +export interface JWKEncodedPrivateKey extends JWKEncodedPublicKey { d: string} + +export interface EnhancedKey { + iv: Uint8Array + cipherKey: Uint8Array + macKey: Uint8Array +} + +export interface EnhancedKeyPair { + k1: EnhancedKey + k2: EnhancedKey +} diff --git a/packages/crypto/src/keys/jwk2pem.ts b/packages/crypto/src/keys/jwk2pem.ts new file mode 100644 index 0000000000..64feebc188 --- /dev/null +++ b/packages/crypto/src/keys/jwk2pem.ts @@ -0,0 +1,21 @@ +import 'node-forge/lib/rsa.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { base64urlToBigInteger } from '../util.js' + +export interface JWK { + encrypt: (msg: string) => string + decrypt: (msg: string) => string +} + +function convert (key: any, types: string[]): Array { + return types.map(t => base64urlToBigInteger(key[t])) +} + +export function jwk2priv (key: JsonWebKey): JWK { + return forge.pki.setRsaPrivateKey(...convert(key, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'])) +} + +export function jwk2pub (key: JsonWebKey): JWK { + return forge.pki.setRsaPublicKey(...convert(key, ['n', 'e'])) +} diff --git a/packages/crypto/src/keys/key-stretcher.ts b/packages/crypto/src/keys/key-stretcher.ts new file mode 100644 index 0000000000..c1feefcf0b --- /dev/null +++ b/packages/crypto/src/keys/key-stretcher.ts @@ -0,0 +1,78 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as hmac from '../hmac/index.js' +import type { EnhancedKey, EnhancedKeyPair } from './interface.js' + +const cipherMap = { + 'AES-128': { + ivSize: 16, + keySize: 16 + }, + 'AES-256': { + ivSize: 16, + keySize: 32 + }, + Blowfish: { + ivSize: 8, + keySize: 32 + } +} + +/** + * Generates a set of keys for each party by stretching the shared key. + * (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey) + */ +export async function keyStretcher (cipherType: 'AES-128' | 'AES-256' | 'Blowfish', hash: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise { + const cipher = cipherMap[cipherType] + + if (cipher == null) { + const allowed = Object.keys(cipherMap).join(' / ') + throw new CodeError(`unknown cipher type '${cipherType}'. Must be ${allowed}`, 'ERR_INVALID_CIPHER_TYPE') + } + + if (hash == null) { + throw new CodeError('missing hash type', 'ERR_MISSING_HASH_TYPE') + } + + const cipherKeySize = cipher.keySize + const ivSize = cipher.ivSize + const hmacKeySize = 20 + const seed = uint8ArrayFromString('key expansion') + const resultLength = 2 * (ivSize + cipherKeySize + hmacKeySize) + + const m = await hmac.create(hash, secret) + let a = await m.digest(seed) + + const result = [] + let j = 0 + + while (j < resultLength) { + const b = await m.digest(uint8ArrayConcat([a, seed])) + let todo = b.length + + if (j + todo > resultLength) { + todo = resultLength - j + } + + result.push(b) + j += todo + a = await m.digest(a) + } + + const half = resultLength / 2 + const resultBuffer = uint8ArrayConcat(result) + const r1 = resultBuffer.subarray(0, half) + const r2 = resultBuffer.subarray(half, resultLength) + + const createKey = (res: Uint8Array): EnhancedKey => ({ + iv: res.subarray(0, ivSize), + cipherKey: res.subarray(ivSize, ivSize + cipherKeySize), + macKey: res.subarray(ivSize + cipherKeySize) + }) + + return { + k1: createKey(r1), + k2: createKey(r2) + } +} diff --git a/packages/crypto/src/keys/keys.proto b/packages/crypto/src/keys/keys.proto new file mode 100644 index 0000000000..7f49971654 --- /dev/null +++ b/packages/crypto/src/keys/keys.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +enum KeyType { + RSA = 0; + Ed25519 = 1; + Secp256k1 = 2; +} +message PublicKey { + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional KeyType Type = 1; + + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional bytes Data = 2; +} +message PrivateKey { + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional KeyType Type = 1; + + // the proto2 version of this field is "required" which means it will have + // no default value. the default for proto3 is "singluar" which omits the + // value on the wire if it's the default so for proto3 we make it "optional" + // to ensure a value is always written on to the wire + optional bytes Data = 2; +} diff --git a/packages/crypto/src/keys/keys.ts b/packages/crypto/src/keys/keys.ts new file mode 100644 index 0000000000..0d9fcd41ed --- /dev/null +++ b/packages/crypto/src/keys/keys.ts @@ -0,0 +1,156 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export enum KeyType { + RSA = 'RSA', + Ed25519 = 'Ed25519', + Secp256k1 = 'Secp256k1' +} + +enum __KeyTypeValues { + RSA = 0, + Ed25519 = 1, + Secp256k1 = 2 +} + +export namespace KeyType { + export const codec = (): Codec => { + return enumeration(__KeyTypeValues) + } +} +export interface PublicKey { + Type?: KeyType + Data?: Uint8Array +} + +export namespace PublicKey { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.Type != null) { + w.uint32(8) + KeyType.codec().encode(obj.Type, w) + } + + if (obj.Data != null) { + w.uint32(18) + w.bytes(obj.Data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.Type = KeyType.codec().decode(reader) + break + case 2: + obj.Data = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PublicKey.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PublicKey => { + return decodeMessage(buf, PublicKey.codec()) + } +} + +export interface PrivateKey { + Type?: KeyType + Data?: Uint8Array +} + +export namespace PrivateKey { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.Type != null) { + w.uint32(8) + KeyType.codec().encode(obj.Type, w) + } + + if (obj.Data != null) { + w.uint32(18) + w.bytes(obj.Data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.Type = KeyType.codec().decode(reader) + break + case 2: + obj.Data = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PrivateKey.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PrivateKey => { + return decodeMessage(buf, PrivateKey.codec()) + } +} diff --git a/packages/crypto/src/keys/rsa-browser.ts b/packages/crypto/src/keys/rsa-browser.ts new file mode 100644 index 0000000000..a17eae884c --- /dev/null +++ b/packages/crypto/src/keys/rsa-browser.ts @@ -0,0 +1,157 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import randomBytes from '../random-bytes.js' +import webcrypto from '../webcrypto.js' +import { jwk2pub, jwk2priv } from './jwk2pem.js' +import * as utils from './rsa-utils.js' +import type { JWKKeyPair } from './interface.js' + +export { utils } + +export async function generateKey (bits: number): Promise { + const pair = await webcrypto.get().subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: bits, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-256' } + }, + true, + ['sign', 'verify'] + ) + + const keys = await exportKey(pair) + + return { + privateKey: keys[0], + publicKey: keys[1] + } +} + +// Takes a jwk key +export async function unmarshalPrivateKey (key: JsonWebKey): Promise { + const privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + true, + ['sign'] + ) + + const pair = [ + privateKey, + await derivePublicFromPrivate(key) + ] + + const keys = await exportKey({ + privateKey: pair[0], + publicKey: pair[1] + }) + + return { + privateKey: keys[0], + publicKey: keys[1] + } +} + +export { randomBytes as getRandomValues } + +export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise { + const privateKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + false, + ['sign'] + ) + + const sig = await webcrypto.get().subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + privateKey, + Uint8Array.from(msg) + ) + + return new Uint8Array(sig, 0, sig.byteLength) +} + +export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise { + const publicKey = await webcrypto.get().subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + false, + ['verify'] + ) + + return webcrypto.get().subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + publicKey, + sig, + msg + ) +} + +async function exportKey (pair: CryptoKeyPair): Promise<[JsonWebKey, JsonWebKey]> { + if (pair.privateKey == null || pair.publicKey == null) { + throw new CodeError('Private and public key are required', 'ERR_INVALID_PARAMETERS') + } + + return Promise.all([ + webcrypto.get().subtle.exportKey('jwk', pair.privateKey), + webcrypto.get().subtle.exportKey('jwk', pair.publicKey) + ]) +} + +async function derivePublicFromPrivate (jwKey: JsonWebKey): Promise { + return webcrypto.get().subtle.importKey( + 'jwk', + { + kty: jwKey.kty, + n: jwKey.n, + e: jwKey.e + }, + { + name: 'RSASSA-PKCS1-v1_5', + hash: { name: 'SHA-256' } + }, + true, + ['verify'] + ) +} + +/* + +RSA encryption/decryption for the browser with webcrypto workaround +"bloody dark magic. webcrypto's why." + +Explanation: + - Convert JWK to nodeForge + - Convert msg Uint8Array to nodeForge buffer: ByteBuffer is a "binary-string backed buffer", so let's make our Uint8Array a binary string + - Convert resulting nodeForge buffer to Uint8Array: it returns a binary string, turn that into a Uint8Array + +*/ + +function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array, handle: (msg: string, key: { encrypt: (msg: string) => string, decrypt: (msg: string) => string }) => string): Uint8Array { + const fkey = pub ? jwk2pub(key) : jwk2priv(key) + const fmsg = uint8ArrayToString(Uint8Array.from(msg), 'ascii') + const fomsg = handle(fmsg, fkey) + return uint8ArrayFromString(fomsg, 'ascii') +} + +export function encrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array { + return convertKey(key, true, msg, (msg, key) => key.encrypt(msg)) +} + +export function decrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array { + return convertKey(key, false, msg, (msg, key) => key.decrypt(msg)) +} diff --git a/packages/crypto/src/keys/rsa-class.ts b/packages/crypto/src/keys/rsa-class.ts new file mode 100644 index 0000000000..dd25257037 --- /dev/null +++ b/packages/crypto/src/keys/rsa-class.ts @@ -0,0 +1,156 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import 'node-forge/lib/sha512.js' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { exporter } from './exporter.js' +import * as pbm from './keys.js' +import * as crypto from './rsa.js' +import type { Multibase } from 'multiformats' + +export class RsaPublicKey { + private readonly _key: JsonWebKey + + constructor (key: JsonWebKey) { + this._key = key + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return crypto.utils.jwkToPkix(this._key) + } + + get bytes (): Uint8Array { + return pbm.PublicKey.encode({ + Type: pbm.KeyType.RSA, + Data: this.marshal() + }).subarray() + } + + encrypt (bytes: Uint8Array): Uint8Array { + return crypto.encrypt(this._key, bytes) + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class RsaPrivateKey { + private readonly _key: JsonWebKey + private readonly _publicKey: JsonWebKey + + constructor (key: JsonWebKey, publicKey: JsonWebKey) { + this._key = key + this._publicKey = publicKey + } + + genSecret (): Uint8Array { + return crypto.getRandomValues(16) + } + + async sign (message: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.hashAndSign(this._key, message) + } + + get public (): RsaPublicKey { + if (this._publicKey == null) { + throw new CodeError('public key not provided', 'ERR_PUBKEY_NOT_PROVIDED') + } + + return new RsaPublicKey(this._publicKey) + } + + decrypt (bytes: Uint8Array): Uint8Array { + return crypto.decrypt(this._key, bytes) + } + + marshal (): Uint8Array { + return crypto.utils.jwkToPkcs1(this._key) + } + + get bytes (): Uint8Array { + return pbm.PrivateKey.encode({ + Type: pbm.KeyType.RSA, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + */ + async id (): Promise { + const hash = await this.public.hash() + return uint8ArrayToString(hash, 'base58btc') + } + + /** + * Exports the key into a password protected PEM format + */ + async export (password: string, format = 'pkcs-8'): Promise> { // eslint-disable-line require-await + if (format === 'pkcs-8') { + const buffer = new forge.util.ByteBuffer(this.marshal()) + const asn1 = forge.asn1.fromDer(buffer) + const privateKey = forge.pki.privateKeyFromAsn1(asn1) + + const options = { + algorithm: 'aes256', + count: 10000, + saltSize: 128 / 8, + prfAlgorithm: 'sha512' + } + return forge.pki.encryptRsaPrivateKey(privateKey, password, options) + } else if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export async function unmarshalRsaPrivateKey (bytes: Uint8Array): Promise { + const jwk = crypto.utils.pkcs1ToJwk(bytes) + const keys = await crypto.unmarshalPrivateKey(jwk) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} + +export function unmarshalRsaPublicKey (bytes: Uint8Array): RsaPublicKey { + const jwk = crypto.utils.pkixToJwk(bytes) + return new RsaPublicKey(jwk) +} + +export async function fromJwk (jwk: JsonWebKey): Promise { + const keys = await crypto.unmarshalPrivateKey(jwk) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} + +export async function generateKeyPair (bits: number): Promise { + const keys = await crypto.generateKey(bits) + return new RsaPrivateKey(keys.privateKey, keys.publicKey) +} diff --git a/packages/crypto/src/keys/rsa-utils.ts b/packages/crypto/src/keys/rsa-utils.ts new file mode 100644 index 0000000000..c818362379 --- /dev/null +++ b/packages/crypto/src/keys/rsa-utils.ts @@ -0,0 +1,74 @@ +import 'node-forge/lib/asn1.js' +import 'node-forge/lib/rsa.js' +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { bigIntegerToUintBase64url, base64urlToBigInteger } from './../util.js' + +// Convert a PKCS#1 in ASN1 DER format to a JWK key +export function pkcs1ToJwk (bytes: Uint8Array): JsonWebKey { + const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) + const privateKey = forge.pki.privateKeyFromAsn1(asn1) + + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + return { + kty: 'RSA', + n: bigIntegerToUintBase64url(privateKey.n), + e: bigIntegerToUintBase64url(privateKey.e), + d: bigIntegerToUintBase64url(privateKey.d), + p: bigIntegerToUintBase64url(privateKey.p), + q: bigIntegerToUintBase64url(privateKey.q), + dp: bigIntegerToUintBase64url(privateKey.dP), + dq: bigIntegerToUintBase64url(privateKey.dQ), + qi: bigIntegerToUintBase64url(privateKey.qInv), + alg: 'RS256' + } +} + +// Convert a JWK key into PKCS#1 in ASN1 DER format +export function jwkToPkcs1 (jwk: JsonWebKey): Uint8Array { + if (jwk.n == null || jwk.e == null || jwk.d == null || jwk.p == null || jwk.q == null || jwk.dp == null || jwk.dq == null || jwk.qi == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + const asn1 = forge.pki.privateKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e), + d: base64urlToBigInteger(jwk.d), + p: base64urlToBigInteger(jwk.p), + q: base64urlToBigInteger(jwk.q), + dP: base64urlToBigInteger(jwk.dp), + dQ: base64urlToBigInteger(jwk.dq), + qInv: base64urlToBigInteger(jwk.qi) + }) + + return uint8ArrayFromString(forge.asn1.toDer(asn1).getBytes(), 'ascii') +} + +// Convert a PKCIX in ASN1 DER format to a JWK key +export function pkixToJwk (bytes: Uint8Array): JsonWebKey { + const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) + const publicKey = forge.pki.publicKeyFromAsn1(asn1) + + return { + kty: 'RSA', + n: bigIntegerToUintBase64url(publicKey.n), + e: bigIntegerToUintBase64url(publicKey.e) + } +} + +// Convert a JWK key to PKCIX in ASN1 DER format +export function jwkToPkix (jwk: JsonWebKey): Uint8Array { + if (jwk.n == null || jwk.e == null) { + throw new CodeError('JWK was missing components', 'ERR_INVALID_PARAMETERS') + } + + const asn1 = forge.pki.publicKeyToAsn1({ + n: base64urlToBigInteger(jwk.n), + e: base64urlToBigInteger(jwk.e) + }) + + return uint8ArrayFromString(forge.asn1.toDer(asn1).getBytes(), 'ascii') +} diff --git a/packages/crypto/src/keys/rsa.ts b/packages/crypto/src/keys/rsa.ts new file mode 100644 index 0000000000..619874bcd3 --- /dev/null +++ b/packages/crypto/src/keys/rsa.ts @@ -0,0 +1,69 @@ +import crypto from 'crypto' +import { promisify } from 'util' +import { CodeError } from '@libp2p/interfaces/errors' +import randomBytes from '../random-bytes.js' +import * as utils from './rsa-utils.js' +import type { JWKKeyPair } from './interface.js' + +const keypair = promisify(crypto.generateKeyPair) + +export { utils } + +export async function generateKey (bits: number): Promise { // eslint-disable-line require-await + // @ts-expect-error node types are missing jwk as a format + const key = await keypair('rsa', { + modulusLength: bits, + publicKeyEncoding: { type: 'pkcs1', format: 'jwk' }, + privateKeyEncoding: { type: 'pkcs1', format: 'jwk' } + }) + + return { + // @ts-expect-error node types are missing jwk as a format + privateKey: key.privateKey, + // @ts-expect-error node types are missing jwk as a format + publicKey: key.publicKey + } +} + +// Takes a jwk key +export async function unmarshalPrivateKey (key: JsonWebKey): Promise { // eslint-disable-line require-await + if (key == null) { + throw new CodeError('Missing key parameter', 'ERR_MISSING_KEY') + } + return { + privateKey: key, + publicKey: { + kty: key.kty, + n: key.n, + e: key.e + } + } +} + +export { randomBytes as getRandomValues } + +export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise { + return crypto.createSign('RSA-SHA256') + .update(msg) + // @ts-expect-error node types are missing jwk as a format + .sign({ format: 'jwk', key }) +} + +export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise { // eslint-disable-line require-await + return crypto.createVerify('RSA-SHA256') + .update(msg) + // @ts-expect-error node types are missing jwk as a format + .verify({ format: 'jwk', key }, sig) +} + +const padding = crypto.constants.RSA_PKCS1_PADDING + +export function encrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array { + // @ts-expect-error node types are missing jwk as a format + return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes) +} + +export function decrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array { + // @ts-expect-error node types are missing jwk as a format + return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes) +} diff --git a/packages/crypto/src/keys/secp256k1-class.ts b/packages/crypto/src/keys/secp256k1-class.ts new file mode 100644 index 0000000000..8df62b487c --- /dev/null +++ b/packages/crypto/src/keys/secp256k1-class.ts @@ -0,0 +1,119 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { exporter } from './exporter.js' +import * as keysProtobuf from './keys.js' +import * as crypto from './secp256k1.js' +import type { Multibase } from 'multiformats' + +export class Secp256k1PublicKey { + private readonly _key: Uint8Array + + constructor (key: Uint8Array) { + crypto.validatePublicKey(key) + this._key = key + } + + async verify (data: Uint8Array, sig: Uint8Array): Promise { + return crypto.hashAndVerify(this._key, sig, data) + } + + marshal (): Uint8Array { + return crypto.compressPublicKey(this._key) + } + + get bytes (): Uint8Array { + return keysProtobuf.PublicKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } +} + +export class Secp256k1PrivateKey { + private readonly _key: Uint8Array + private readonly _publicKey: Uint8Array + + constructor (key: Uint8Array, publicKey?: Uint8Array) { + this._key = key + this._publicKey = publicKey ?? crypto.computePublicKey(key) + crypto.validatePrivateKey(this._key) + crypto.validatePublicKey(this._publicKey) + } + + async sign (message: Uint8Array): Promise { + return crypto.hashAndSign(this._key, message) + } + + get public (): Secp256k1PublicKey { + return new Secp256k1PublicKey(this._publicKey) + } + + marshal (): Uint8Array { + return this._key + } + + get bytes (): Uint8Array { + return keysProtobuf.PrivateKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).subarray() + } + + equals (key: any): boolean { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + async hash (): Promise { + const { bytes } = await sha256.digest(this.bytes) + + return bytes + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + */ + async id (): Promise { + const hash = await this.public.hash() + return uint8ArrayToString(hash, 'base58btc') + } + + /** + * Exports the key into a password protected `format` + */ + async export (password: string, format = 'libp2p-key'): Promise> { + if (format === 'libp2p-key') { + return exporter(this.bytes, password) + } else { + throw new CodeError(`export format '${format}' is not supported`, 'ERR_INVALID_EXPORT_FORMAT') + } + } +} + +export function unmarshalSecp256k1PrivateKey (bytes: Uint8Array): Secp256k1PrivateKey { + return new Secp256k1PrivateKey(bytes) +} + +export function unmarshalSecp256k1PublicKey (bytes: Uint8Array): Secp256k1PublicKey { + return new Secp256k1PublicKey(bytes) +} + +export async function generateKeyPair (): Promise { + const privateKeyBytes = crypto.generateKey() + return new Secp256k1PrivateKey(privateKeyBytes) +} diff --git a/packages/crypto/src/keys/secp256k1.ts b/packages/crypto/src/keys/secp256k1.ts new file mode 100644 index 0000000000..a41207a89b --- /dev/null +++ b/packages/crypto/src/keys/secp256k1.ts @@ -0,0 +1,69 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import * as secp from '@noble/secp256k1' +import { sha256 } from 'multiformats/hashes/sha2' + +const PRIVATE_KEY_BYTE_LENGTH = 32 + +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +export function generateKey (): Uint8Array { + return secp.utils.randomPrivateKey() +} + +/** + * Hash and sign message with private key + */ +export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise { + const { digest } = await sha256.digest(msg) + try { + return await secp.sign(digest, key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + } +} + +/** + * Hash message and verify signature with public key + */ +export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise { + try { + const { digest } = await sha256.digest(msg) + return secp.verify(sig, digest, key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_INPUT') + } +} + +export function compressPublicKey (key: Uint8Array): Uint8Array { + const point = secp.Point.fromHex(key).toRawBytes(true) + return point +} + +export function decompressPublicKey (key: Uint8Array): Uint8Array { + const point = secp.Point.fromHex(key).toRawBytes(false) + return point +} + +export function validatePrivateKey (key: Uint8Array): void { + try { + secp.getPublicKey(key, true) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY') + } +} + +export function validatePublicKey (key: Uint8Array): void { + try { + secp.Point.fromHex(key) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PUBLIC_KEY') + } +} + +export function computePublicKey (privateKey: Uint8Array): Uint8Array { + try { + return secp.getPublicKey(privateKey, true) + } catch (err) { + throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY') + } +} diff --git a/packages/crypto/src/pbkdf2.ts b/packages/crypto/src/pbkdf2.ts new file mode 100644 index 0000000000..fe2add7dcc --- /dev/null +++ b/packages/crypto/src/pbkdf2.ts @@ -0,0 +1,39 @@ +import { CodeError } from '@libp2p/interfaces/errors' +// @ts-expect-error types are missing +import forgePbkdf2 from 'node-forge/lib/pbkdf2.js' +// @ts-expect-error types are missing +import forgeUtil from 'node-forge/lib/util.js' + +/** + * Maps an IPFS hash name to its node-forge equivalent. + * + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @private + */ +const hashName = { + sha1: 'sha1', + 'sha2-256': 'sha256', + 'sha2-512': 'sha512' +} + +/** + * Computes the Password-Based Key Derivation Function 2. + */ +export default function pbkdf2 (password: string, salt: string, iterations: number, keySize: number, hash: string): string { + if (hash !== 'sha1' && hash !== 'sha2-256' && hash !== 'sha2-512') { + const types = Object.keys(hashName).join(' / ') + throw new CodeError(`Hash '${hash}' is unknown or not supported. Must be ${types}`, 'ERR_UNSUPPORTED_HASH_TYPE') + } + + const hasher = hashName[hash] + const dek = forgePbkdf2( + password, + salt, + iterations, + keySize, + hasher + ) + + return forgeUtil.encode64(dek, null) +} diff --git a/packages/crypto/src/random-bytes.ts b/packages/crypto/src/random-bytes.ts new file mode 100644 index 0000000000..7352dfe071 --- /dev/null +++ b/packages/crypto/src/random-bytes.ts @@ -0,0 +1,9 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { utils } from '@noble/secp256k1' + +export default function randomBytes (length: number): Uint8Array { + if (isNaN(length) || length <= 0) { + throw new CodeError('random bytes length must be a Number bigger than 0', 'ERR_INVALID_LENGTH') + } + return utils.randomBytes(length) +} diff --git a/packages/crypto/src/util.ts b/packages/crypto/src/util.ts new file mode 100644 index 0000000000..e0bab8c5ff --- /dev/null +++ b/packages/crypto/src/util.ts @@ -0,0 +1,42 @@ +import 'node-forge/lib/util.js' +import 'node-forge/lib/jsbn.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +export function bigIntegerToUintBase64url (num: { abs: () => any }, len?: number): string { + // Call `.abs()` to convert to unsigned + let buf = Uint8Array.from(num.abs().toByteArray()) // toByteArray converts to big endian + + // toByteArray() gives us back a signed array, which will include a leading 0 + // byte if the most significant bit of the number is 1: + // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-integer + // Our number will always be positive so we should remove the leading padding. + buf = buf[0] === 0 ? buf.subarray(1) : buf + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = uint8ArrayConcat([new Uint8Array(len - buf.length), buf]) + } + + return uint8ArrayToString(buf, 'base64url') +} + +// Convert a base64url encoded string to a BigInteger +export function base64urlToBigInteger (str: string): typeof forge.jsbn.BigInteger { + const buf = base64urlToBuffer(str) + return new forge.jsbn.BigInteger(uint8ArrayToString(buf, 'base16'), 16) +} + +export function base64urlToBuffer (str: string, len?: number): Uint8Array { + let buf = uint8ArrayFromString(str, 'base64urlpad') + + if (len != null) { + if (buf.length > len) throw new Error('byte array longer than desired length') + buf = uint8ArrayConcat([new Uint8Array(len - buf.length), buf]) + } + + return buf +} diff --git a/packages/crypto/src/webcrypto.ts b/packages/crypto/src/webcrypto.ts new file mode 100644 index 0000000000..bbf03ffe98 --- /dev/null +++ b/packages/crypto/src/webcrypto.ts @@ -0,0 +1,24 @@ +/* eslint-env browser */ + +// Check native crypto exists and is enabled (In insecure context `self.crypto` +// exists but `self.crypto.subtle` does not). +export default { + get (win = globalThis) { + const nativeCrypto = win.crypto + + if (nativeCrypto == null || nativeCrypto.subtle == null) { + throw Object.assign( + new Error( + 'Missing Web Crypto API. ' + + 'The most likely cause of this error is that this page is being accessed ' + + 'from an insecure context (i.e. not HTTPS). For more information and ' + + 'possible resolutions see ' + + 'https://github.com/libp2p/js-libp2p-crypto/blob/master/README.md#web-crypto-api' + ), + { code: 'ERR_MISSING_WEB_CRYPTO' } + ) + } + + return nativeCrypto + } +} diff --git a/packages/crypto/stats.md b/packages/crypto/stats.md new file mode 100644 index 0000000000..8c4eff33e6 --- /dev/null +++ b/packages/crypto/stats.md @@ -0,0 +1,153 @@ +# Stats + +## Size + +| | non-minified | minified | +|-------|--------------|----------| +|before | `1.8M` | `949K` | +|after | `606K` | `382K` | + +## Performance + +### RSA + +#### Before + +##### Node `6.6.0` + +``` +generateKeyPair 1024bits x 3.51 ops/sec ±29.45% (22 runs sampled) +generateKeyPair 2048bits x 0.17 ops/sec ±145.40% (5 runs sampled) +generateKeyPair 4096bits x 0.02 ops/sec ±96.53% (5 runs sampled) +sign and verify x 95.98 ops/sec ±1.51% (71 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +generateKeyPair 1024bits x 3.56 ops/sec ±27.16% (23 runs sampled) +generateKeyPair 2048bits x 0.49 ops/sec ±69.32% (8 runs sampled) +generateKeyPair 4096bits x 0.03 ops/sec ±77.11% (5 runs sampled) +sign and verify x 109 ops/sec ±2.00% (53 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +generateKeyPair 1024bits x 42.45 ops/sec ±9.87% (52 runs sampled) +generateKeyPair 2048bits x 7.46 ops/sec ±23.80% (16 runs sampled) +generateKeyPair 4096bits x 1.50 ops/sec ±58.59% (13 runs sampled) +sign and verify x 1,080 ops/sec ±2.23% (74 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +generateKeyPair 1024bits x 5.89 ops/sec ±18.94% (19 runs sampled) +generateKeyPair 2048bits x 1.32 ops/sec ±36.84% (10 runs sampled) +generateKeyPair 4096bits x 0.20 ops/sec ±62.49% (5 runs sampled) +sign and verify x 608 ops/sec ±6.75% (56 runs sampled) +``` + +### Key Stretcher + + +#### Before + +##### Node `6.6.0` + +``` +keyStretcher AES-128 SHA1 x 3,863 ops/sec ±3.80% (70 runs sampled) +keyStretcher AES-128 SHA256 x 3,862 ops/sec ±5.33% (64 runs sampled) +keyStretcher AES-128 SHA512 x 3,369 ops/sec ±1.73% (73 runs sampled) +keyStretcher AES-256 SHA1 x 3,008 ops/sec ±4.81% (67 runs sampled) +keyStretcher AES-256 SHA256 x 2,900 ops/sec ±7.01% (64 runs sampled) +keyStretcher AES-256 SHA512 x 2,553 ops/sec ±4.45% (73 runs sampled) +keyStretcher Blowfish SHA1 x 28,045 ops/sec ±7.32% (61 runs sampled) +keyStretcher Blowfish SHA256 x 18,860 ops/sec ±5.36% (67 runs sampled) +keyStretcher Blowfish SHA512 x 12,142 ops/sec ±12.44% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +keyStretcher AES-128 SHA1 x 4,168 ops/sec ±4.08% (49 runs sampled) +keyStretcher AES-128 SHA256 x 4,239 ops/sec ±6.36% (48 runs sampled) +keyStretcher AES-128 SHA512 x 3,600 ops/sec ±5.15% (51 runs sampled) +keyStretcher AES-256 SHA1 x 3,009 ops/sec ±6.82% (48 runs sampled) +keyStretcher AES-256 SHA256 x 3,086 ops/sec ±9.56% (19 runs sampled) +keyStretcher AES-256 SHA512 x 2,470 ops/sec ±2.22% (54 runs sampled) +keyStretcher Blowfish SHA1 x 7,143 ops/sec ±15.17% (9 runs sampled) +keyStretcher Blowfish SHA256 x 17,846 ops/sec ±4.74% (46 runs sampled) +keyStretcher Blowfish SHA512 x 7,726 ops/sec ±1.81% (50 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +keyStretcher AES-128 SHA1 x 6,680 ops/sec ±3.62% (65 runs sampled) +keyStretcher AES-128 SHA256 x 8,124 ops/sec ±4.37% (66 runs sampled) +keyStretcher AES-128 SHA512 x 11,683 ops/sec ±4.56% (66 runs sampled) +keyStretcher AES-256 SHA1 x 5,531 ops/sec ±4.69% (68 runs sampled) +keyStretcher AES-256 SHA256 x 6,725 ops/sec ±4.87% (66 runs sampled) +keyStretcher AES-256 SHA512 x 9,042 ops/sec ±3.87% (64 runs sampled) +keyStretcher Blowfish SHA1 x 40,757 ops/sec ±5.38% (60 runs sampled) +keyStretcher Blowfish SHA256 x 41,845 ops/sec ±4.89% (64 runs sampled) +keyStretcher Blowfish SHA512 x 42,345 ops/sec ±4.86% (63 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +keyStretcher AES-128 SHA1 x 479 ops/sec ±2.12% (54 runs sampled) +keyStretcher AES-128 SHA256 x 668 ops/sec ±2.02% (53 runs sampled) +keyStretcher AES-128 SHA512 x 1,112 ops/sec ±1.61% (54 runs sampled) +keyStretcher AES-256 SHA1 x 460 ops/sec ±1.37% (54 runs sampled) +keyStretcher AES-256 SHA256 x 596 ops/sec ±1.56% (54 runs sampled) +keyStretcher AES-256 SHA512 x 808 ops/sec ±3.27% (52 runs sampled) +keyStretcher Blowfish SHA1 x 3,015 ops/sec ±3.51% (52 runs sampled) +keyStretcher Blowfish SHA256 x 2,755 ops/sec ±3.82% (53 runs sampled) +keyStretcher Blowfish SHA512 x 2,955 ops/sec ±5.35% (51 runs sampled) +``` + +### Ephemeral Keys + +#### Before + +##### Node `6.6.0` + +``` +ephemeral key with secrect P-256 x 89.93 ops/sec ±39.45% (72 runs sampled) +ephemeral key with secrect P-384 x 110 ops/sec ±1.28% (71 runs sampled) +ephemeral key with secrect P-521 x 112 ops/sec ±1.70% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +ephemeral key with secrect P-256 x 6.27 ops/sec ±15.89% (35 runs sampled) +ephemeral key with secrect P-384 x 6.84 ops/sec ±1.21% (35 runs sampled) +ephemeral key with secrect P-521 x 6.60 ops/sec ±1.84% (34 runs sampled) +``` + +#### After + +##### Node `6.6.0` + +``` +ephemeral key with secrect P-256 x 555 ops/sec ±1.61% (75 runs sampled) +ephemeral key with secrect P-384 x 547 ops/sec ±4.40% (68 runs sampled) +ephemeral key with secrect P-521 x 583 ops/sec ±4.84% (72 runs sampled) +``` + +##### Browser (Chrome `53.0.2785.116`) + +``` +ephemeral key with secrect P-256 x 796 ops/sec ±2.36% (53 runs sampled) +ephemeral key with secrect P-384 x 788 ops/sec ±2.66% (53 runs sampled) +ephemeral key with secrect P-521 x 808 ops/sec ±1.83% (54 runs sampled) +``` diff --git a/packages/crypto/test/aes/aes.spec.ts b/packages/crypto/test/aes/aes.spec.ts new file mode 100644 index 0000000000..0b41c1d2dd --- /dev/null +++ b/packages/crypto/test/aes/aes.spec.ts @@ -0,0 +1,105 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-disable valid-jsdoc */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from './../fixtures/aes.js' +import goFixtures from './../fixtures/go-aes.js' +import type { AESCipher } from '../../src/aes/index.js' + +const bytes = [{ + length: 16, + hash: 'AES-128' +}, { + length: 32, + hash: 'AES-256' +}] + +describe('AES-CTR', () => { + bytes.forEach(({ length, hash }) => { + it(`${hash} - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + await encryptAndDecrypt(cipher) + }) + }) + + bytes.forEach(({ length, hash }) => { + it(`${hash} - fixed - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + // @ts-expect-error cannot index fixtures like this + const fixture = fixtures[length] + + for (let i = 0; i < fixture.inputs.length; i++) { + const input = fixture.inputs[i] + const output = fixture.outputs[i] + const encrypted = await cipher.encrypt(input) + expect(encrypted).to.have.length(output.length) + expect(encrypted).to.eql(output) + const decrypted = await cipher.decrypt(encrypted) + expect(decrypted).to.eql(input) + } + }) + }) + + bytes.forEach(({ length, hash }) => { + // @ts-expect-error cannot index fixtures like this + if (goFixtures[length] == null) { + return + } + + it(`${hash} - go interop - encrypt and decrypt`, async () => { + const key = new Uint8Array(length) + key.fill(5) + + const iv = new Uint8Array(16) + iv.fill(1) + + const cipher = await crypto.aes.create(key, iv) + // @ts-expect-error cannot index fixtures like this + const fixture = goFixtures[length] + + for (let i = 0; i < fixture.inputs.length; i++) { + const input = fixture.inputs[i] + const output = fixture.outputs[i] + const encrypted = await cipher.encrypt(input) + expect(encrypted).to.have.length(output.length) + expect(encrypted).to.eql(output) + const decrypted = await cipher.decrypt(encrypted) + expect(decrypted).to.eql(input) + } + }) + }) + + it('checks key length', () => { + const key = new Uint8Array(5) + const iv = new Uint8Array(16) + return expect(crypto.aes.create(key, iv)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_LENGTH') + }) +}) + +async function encryptAndDecrypt (cipher: AESCipher): Promise { + const data = new Uint8Array(100) + data.fill(Math.ceil(Math.random() * 100)) + + const encrypted = await cipher.encrypt(data) + const decrypted = await cipher.decrypt(encrypted) + + expect(decrypted).to.be.eql(data) +} diff --git a/packages/crypto/test/crypto.spec.ts b/packages/crypto/test/crypto.spec.ts new file mode 100644 index 0000000000..e6f3b186ce --- /dev/null +++ b/packages/crypto/test/crypto.spec.ts @@ -0,0 +1,155 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import * as crypto from '../src/index.js' +import { RsaPrivateKey, RsaPublicKey } from '../src/keys/rsa-class.js' +import fixtures from './fixtures/go-key-rsa.js' + +describe('libp2p-crypto', function () { + this.timeout(20 * 1000) + let key: RsaPrivateKey + before(async () => { + const generated = await crypto.keys.generateKeyPair('RSA', 512) + + if (!(generated instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + key = generated + }) + + it('marshalPublicKey and unmarshalPublicKey', () => { + const key2 = crypto.keys.unmarshalPublicKey(crypto.keys.marshalPublicKey(key.public)) + + if (!(key2 instanceof RsaPublicKey)) { + throw new Error('Wrong key type unmarshalled') + } + + expect(key2.equals(key.public)).to.be.eql(true) + + expect(() => { + crypto.keys.marshalPublicKey(key.public, 'invalid-key-type') + }).to.throw() + }) + + it('marshalPrivateKey and unmarshalPrivateKey', async () => { + expect(() => { + crypto.keys.marshalPrivateKey(key, 'invalid-key-type') + }).to.throw() + + const key2 = await crypto.keys.unmarshalPrivateKey(crypto.keys.marshalPrivateKey(key)) + + if (!(key2 instanceof RsaPrivateKey)) { + throw new Error('Wrong key type unmarshalled') + } + + expect(key2.equals(key)).to.be.eql(true) + expect(key2.public.equals(key.public)).to.be.eql(true) + }) + + it('generateKeyPair', () => { + // @ts-expect-error key type is invalid + return expect(crypto.keys.generateKeyPair('invalid-key-type', 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_TYPE') + }) + + it('generateKeyPairFromSeed', () => { + const seed = crypto.randomBytes(32) + + // @ts-expect-error key type is invalid + return expect(crypto.keys.generateKeyPairFromSeed('invalid-key-type', seed, 512)).to.eventually.be.rejected.with.property('code', 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE') + }) + + // https://github.com/libp2p/js-libp2p-crypto/issues/314 + function isSafari (): boolean { + return typeof navigator !== 'undefined' && navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome') && navigator.userAgent.includes('Mac') + } + + // marshalled keys seem to be slightly different + // unsure as to if this is just a difference in encoding + // or a bug + describe('go interop', () => { + it('unmarshals private key', async () => { + if (isSafari()) { + // eslint-disable-next-line no-console + console.warn('Skipping test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314') + return + } + + const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key) + const hash = fixtures.private.hash + expect(fixtures.private.key).to.eql(key.bytes) + const digest = await key.hash() + expect(digest).to.eql(hash) + }) + + it('unmarshals public key', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.public.key) + const hash = fixtures.public.hash + expect(crypto.keys.marshalPublicKey(key)).to.eql(fixtures.public.key) + const digest = await key.hash() + expect(digest).to.eql(hash) + }) + + it('unmarshal -> marshal, private key', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.private.key) + const marshalled = crypto.keys.marshalPrivateKey(key) + if (isSafari()) { + // eslint-disable-next-line no-console + console.warn('Running differnt test in Safari. Known bug: https://github.com/libp2p/js-libp2p-crypto/issues/314') + const key2 = await crypto.keys.unmarshalPrivateKey(marshalled) + expect(key2.bytes).to.eql(key.bytes) + return + } + expect(marshalled).to.eql(fixtures.private.key) + }) + + it('unmarshal -> marshal, public key', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.public.key) + const marshalled = crypto.keys.marshalPublicKey(key) + expect(uint8ArrayEquals(fixtures.public.key, marshalled)).to.eql(true) + }) + }) + + describe('pbkdf2', () => { + it('generates a derived password using sha1', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha1') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates a derived password using sha2-512', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'sha2-512') + expect(p1).to.exist() + expect(p1).to.be.a('string') + }) + + it('generates the same derived password with the same options', () => { + const p1 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p2 = crypto.pbkdf2('password', 'at least 16 character salt', 10, 512 / 8, 'sha1') + const p3 = crypto.pbkdf2('password', 'at least 16 character salt', 11, 512 / 8, 'sha1') + expect(p2).to.equal(p1) + expect(p3).to.not.equal(p2) + }) + + it('throws on invalid hash name', () => { + const fn = (): string => crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'shaX-xxx') + expect(fn).to.throw().with.property('code', 'ERR_UNSUPPORTED_HASH_TYPE') + }) + }) + + describe('randomBytes', () => { + it('throws with invalid number passed', () => { + expect(() => { + crypto.randomBytes(-1) + }).to.throw() + }) + + it('generates different random things', () => { + const buf1 = crypto.randomBytes(10) + expect(buf1.length).to.equal(10) + const buf2 = crypto.randomBytes(10) + expect(buf1).to.not.eql(buf2) + }) + }) +}) diff --git a/packages/crypto/test/fixtures/aes.ts b/packages/crypto/test/fixtures/aes.ts new file mode 100644 index 0000000000..f696dd6330 --- /dev/null +++ b/packages/crypto/test/fixtures/aes.ts @@ -0,0 +1,36 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +export default { + 16: { + inputs: [ + uint8ArrayFromString('Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLw', 'base64'), + uint8ArrayFromString('GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGA', 'base64'), + uint8ArrayFromString('BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw', 'base64'), + uint8ArrayFromString('GRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQ', 'base64'), + uint8ArrayFromString('MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMA', 'base64') + ], + outputs: [ + uint8ArrayFromString('eWgAlzmJFu/qlzoPZBbkblX4+Q+RgN8ZwK+EqWLL52rgZs70HdUkAhrVXh2G24hJ1LAhX8ZblIuE/LZzdKCSwgBhtQDBlRUz+GEgPlaZ7kMmIjePRsFjax9DWmE3P0XLIelK7Q', 'base64'), + uint8ArrayFromString('bax/s27eT9tXTEbMpm645VoxoPxOOkkzmNoDyAp8mHWJKBd/ODnLH2XjH8bfVmJ4ZFZ0kI5/RK/56BZTFkQ85pIWmcFDBTP979JQH/5nuZF7Y82vnJC/Qx/sK2LF6x8yReRkQA', 'base64'), + uint8ArrayFromString('v11+v7QqdpAcvNO/04KqmYbws1NLFypEnsh7mzmpmIUhclodg1tGaVMtKC9NYGEIKAFu9WqsmJIFcoQAsx8sThNtXMfiJAxKtPHga1MNpxv7ZcFiMVrhxUvVkDTrglz324vRhA', 'base64'), + uint8ArrayFromString('v241vvosvogW0YPIcB89t/dfBPlFk+5KEkfFc43iZlxbgLWsQ20RpTQKNx834f2MmiNoPndnxZh9hoy1qkxLcsO8RMUcL3RSIoDoeg7leqEk1KGkkVbX6d4yj1mDIILEbSTM/g', 'base64'), + uint8ArrayFromString('YY4IJUWtfLRhRi1bxH64h9Voq1nnPysqB/VLpc22GDKq2YBwctfRkYfrs9QFUY7HNd0n76cV7aiR+fpsAvdZSeTj/5t5nc1gKyBw0a1gjyvcjBrNIiI1nSmnfevzVQ0OXW3pug', 'base64') + ] + }, + 32: { + inputs: [ + uint8ArrayFromString('RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERA', 'base64'), + uint8ArrayFromString('CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg', 'base64'), + uint8ArrayFromString('S0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSw', 'base64'), + uint8ArrayFromString('IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw', 'base64'), + uint8ArrayFromString('SEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISA', 'base64') + ], + outputs: [ + uint8ArrayFromString('xjtba97WH+ZwAceuEeDC4YpBOqAf+WYQSi5VHuj5eGwmpWkbDGJibIKjEIl0aJm8Bfuj9AA6Ac8ZTrzSzd0whEjRC0MOKtpHlPx+tzwgXSR9Z791UvG+sAGBdnCLmbI4PVs0xg', 'base64'), + uint8ArrayFromString('zvFjePijOR7YVv/AWcGwEU4+UJW96xudr/gHE95Ab8fMoxoQX91GIO8EOqL97QgzXgOlut/SdGXkUmdMSiwzdb2MhOa88xielV2T4nHDHxgJExuEtJgaQX2QVqcpkJ7MTC61bg', 'base64'), + uint8ArrayFromString('maHJQlcu8leI7pfGgXo+zY78bbvutz9f8GHc0SWQ7VT7lZjeWcjQd9UmQRMC/MF9Xky2xvN5/RAt/lIvks4paf7t7I121sXkO30tyD0YbhrrXK//VXc5oJFrzhw+CqZZBxT28w', 'base64'), + uint8ArrayFromString('T9cWHYSC1vjtfOo2104A0/beNls1AjEoAMq8Gbh5pOu9YQ4AU6ZYPjcxT5mIwYXOrGPPSfbYwGsyzqfyGbQ/uMk9WvLfwA2MH/BwnfpajgMoDGo/SSpPUhQpu60XVTv91L9tLg', 'base64'), + uint8ArrayFromString('yeA4QKfgfDETM9Di2DIMSQ//nGxis5BuIZcrQOOZeCcVlyk99RQfF23VbTcjKHptKQogsBm4W7Cxhor8oAJsK97vrgKRSiKD7dbrZhrMfEBlhrotNx00N6tfrFbyZY2Z3qGAUw', 'base64') + ] + } +} diff --git a/packages/crypto/test/fixtures/go-aes.ts b/packages/crypto/test/fixtures/go-aes.ts new file mode 100644 index 0000000000..ca653e97c1 --- /dev/null +++ b/packages/crypto/test/fixtures/go-aes.ts @@ -0,0 +1,19 @@ + +export default { + 16: { + inputs: [ + Uint8Array.from([47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]), + Uint8Array.from([24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24]), + Uint8Array.from([7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]), + Uint8Array.from([25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25]), + Uint8Array.from([48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48]) + ], + outputs: [ + Uint8Array.from([121, 104, 0, 151, 57, 137, 22, 239, 234, 151, 58, 15, 100, 22, 228, 110, 85, 248, 249, 15, 145, 128, 223, 25, 192, 175, 132, 169, 98, 203, 231, 106, 224, 102, 206, 244, 29, 213, 36, 2, 26, 213, 94, 29, 134, 219, 136, 73, 212, 176, 33, 95, 198, 91, 148, 139, 132, 252, 182, 115, 116, 160, 146, 194, 0, 97, 181, 0, 193, 149, 21, 51, 248, 97, 32, 62, 86, 153, 238, 67, 38, 34, 55, 143, 70, 193, 99, 107, 31, 67, 90, 97, 55, 63, 69, 203, 33, 233, 74, 237]), + Uint8Array.from([109, 172, 127, 179, 110, 222, 79, 219, 87, 76, 70, 204, 166, 110, 184, 229, 90, 49, 160, 252, 78, 58, 73, 51, 152, 218, 3, 200, 10, 124, 152, 117, 137, 40, 23, 127, 56, 57, 203, 31, 101, 227, 31, 198, 223, 86, 98, 120, 100, 86, 116, 144, 142, 127, 68, 175, 249, 232, 22, 83, 22, 68, 60, 230, 146, 22, 153, 193, 67, 5, 51, 253, 239, 210, 80, 31, 254, 103, 185, 145, 123, 99, 205, 175, 156, 144, 191, 67, 31, 236, 43, 98, 197, 235, 31, 50, 69, 228, 100, 64]), + Uint8Array.from([191, 93, 126, 191, 180, 42, 118, 144, 28, 188, 211, 191, 211, 130, 170, 153, 134, 240, 179, 83, 75, 23, 42, 68, 158, 200, 123, 155, 57, 169, 152, 133, 33, 114, 90, 29, 131, 91, 70, 105, 83, 45, 40, 47, 77, 96, 97, 8, 40, 1, 110, 245, 106, 172, 152, 146, 5, 114, 132, 0, 179, 31, 44, 78, 19, 109, 92, 199, 226, 36, 12, 74, 180, 241, 224, 107, 83, 13, 167, 27, 251, 101, 193, 98, 49, 90, 225, 197, 75, 213, 144, 52, 235, 130, 92, 247, 219, 139, 209, 132]), + Uint8Array.from([191, 110, 53, 190, 250, 44, 190, 136, 22, 209, 131, 200, 112, 31, 61, 183, 247, 95, 4, 249, 69, 147, 238, 74, 18, 71, 197, 115, 141, 226, 102, 92, 91, 128, 181, 172, 67, 109, 17, 165, 52, 10, 55, 31, 55, 225, 253, 140, 154, 35, 104, 62, 119, 103, 197, 152, 125, 134, 140, 181, 170, 76, 75, 114, 195, 188, 68, 197, 28, 47, 116, 82, 34, 128, 232, 122, 14, 229, 122, 161, 36, 212, 161, 164, 145, 86, 215, 233, 222, 50, 143, 89, 131, 32, 130, 196, 109, 36, 204, 254]), + Uint8Array.from([97, 142, 8, 37, 69, 173, 124, 180, 97, 70, 45, 91, 196, 126, 184, 135, 213, 104, 171, 89, 231, 63, 43, 42, 7, 245, 75, 165, 205, 182, 24, 50, 170, 217, 128, 112, 114, 215, 209, 145, 135, 235, 179, 212, 5, 81, 142, 199, 53, 221, 39, 239, 167, 21, 237, 168, 145, 249, 250, 108, 2, 247, 89, 73, 228, 227, 255, 155, 121, 157, 205, 96, 43, 32, 112, 209, 173, 96, 143, 43, 220, 140, 26, 205, 34, 34, 53, 157, 41, 167, 125, 235, 243, 85, 13, 14, 93, 109, 233, 186]) + ] + } +} diff --git a/packages/crypto/test/fixtures/go-elliptic-key.ts b/packages/crypto/test/fixtures/go-elliptic-key.ts new file mode 100644 index 0000000000..d5435775b8 --- /dev/null +++ b/packages/crypto/test/fixtures/go-elliptic-key.ts @@ -0,0 +1,12 @@ + +export default { + curve: 'P-256', + bob: { + private: Uint8Array.from([ + 181, 217, 162, 151, 225, 36, 53, 253, 107, 66, 27, 27, 232, 72, 0, 0, 103, 167, 84, 62, 203, 91, 97, 137, 131, 193, 230, 126, 98, 242, 216, 170 + ]), + public: Uint8Array.from([ + 4, 53, 59, 128, 56, 162, 250, 72, 141, 206, 117, 232, 57, 96, 39, 39, 247, 7, 27, 57, 251, 232, 120, 186, 21, 239, 176, 139, 195, 129, 125, 85, 11, 188, 191, 32, 227, 0, 6, 163, 101, 68, 208, 1, 43, 131, 124, 112, 102, 91, 104, 79, 16, 119, 152, 208, 4, 147, 155, 83, 20, 146, 104, 55, 90 + ]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-ed25519.ts b/packages/crypto/test/fixtures/go-key-ed25519.ts new file mode 100644 index 0000000000..e758d788e1 --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-ed25519.ts @@ -0,0 +1,41 @@ + +export default { + // Generation code from https://github.com/libp2p/js-libp2p-crypto/issues/175#issuecomment-634467463 + // + // package main + // + // import ( + // "crypto/rand" + // "fmt" + // "strings" + + // "github.com/libp2p/go-libp2p-core/crypto" + // ) + + // func main() { + // priv, pub, _ := crypto.GenerateEd25519Key(rand.Reader) + // pubkeyBytes, _ := pub.Bytes() + // privkeyBytes, _ := priv.Bytes() + // data := []byte("hello! and welcome to some awesome crypto primitives") + // sig, _ := priv.Sign(data) + // fmt.Println("{\n publicKey: Uint8Array.from(", strings.Replace(fmt.Sprint(pubkeyBytes), " ", ",", -1), "),") + // fmt.Println(" privateKey: Uint8Array.from(", strings.Replace(fmt.Sprint(privkeyBytes), " ", ",", -1), "),") + // fmt.Println(" data: Uint8Array.from(", strings.Replace(fmt.Sprint(data), " ", ",", -1), "),") + // fmt.Println(" signature: Uint8Array.from(", strings.Replace(fmt.Sprint(sig), " ", ",", -1), ")\n}") + // } + // + + // The legacy key unnecessarily appends the publickey. (It's already included) See https://github.com/libp2p/js-libp2p-crypto/issues/175 + redundantPubKey: { + privateKey: Uint8Array.from([8, 1, 18, 96, 201, 208, 1, 110, 176, 16, 230, 37, 66, 184, 149, 252, 78, 56, 206, 136, 2, 38, 118, 152, 226, 197, 117, 200, 54, 189, 156, 218, 184, 7, 118, 57, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + publicKey: Uint8Array.from([8, 1, 18, 32, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + data: Uint8Array.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]), + signature: Uint8Array.from([7, 230, 175, 164, 228, 58, 78, 208, 62, 243, 73, 142, 83, 195, 176, 217, 166, 62, 41, 165, 168, 164, 75, 179, 163, 86, 102, 32, 18, 84, 150, 237, 39, 207, 213, 20, 134, 237, 50, 41, 176, 183, 229, 133, 38, 255, 42, 228, 68, 186, 100, 14, 175, 156, 243, 118, 125, 125, 120, 212, 124, 103, 252, 12]) + }, + verify: { + publicKey: Uint8Array.from([8, 1, 18, 32, 163, 176, 195, 47, 254, 208, 49, 5, 192, 102, 32, 63, 58, 202, 171, 153, 146, 164, 25, 212, 25, 91, 146, 26, 117, 165, 148, 6, 207, 90, 217, 126]), + privateKey: Uint8Array.from([8, 1, 18, 64, 232, 56, 175, 20, 240, 160, 19, 47, 92, 88, 115, 221, 164, 13, 36, 162, 158, 136, 247, 31, 29, 231, 76, 143, 12, 91, 193, 4, 88, 33, 67, 23, 163, 176, 195, 47, 254, 208, 49, 5, 192, 102, 32, 63, 58, 202, 171, 153, 146, 164, 25, 212, 25, 91, 146, 26, 117, 165, 148, 6, 207, 90, 217, 126]), + data: Uint8Array.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]), + signature: Uint8Array.from([160, 125, 30, 62, 213, 189, 239, 92, 87, 76, 205, 169, 251, 149, 187, 57, 96, 85, 175, 213, 22, 132, 229, 60, 196, 18, 117, 194, 12, 174, 135, 31, 39, 168, 174, 103, 78, 55, 37, 222, 37, 172, 222, 239, 153, 63, 197, 152, 67, 167, 191, 215, 161, 212, 216, 163, 81, 77, 45, 228, 151, 79, 101, 1]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-rsa.ts b/packages/crypto/test/fixtures/go-key-rsa.ts new file mode 100644 index 0000000000..bfdfd1b3ca --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-rsa.ts @@ -0,0 +1,30 @@ + +export default { + private: { + hash: Uint8Array.from([ + 18, 32, 168, 125, 165, 65, 34, 157, 209, 4, 24, 158, 80, 196, 125, 86, 103, 0, 228, 145, 109, 252, 153, 7, 189, 9, 16, 37, 239, 36, 48, 78, 214, 212 + ]), + key: Uint8Array.from([ + 8, 0, 18, 192, 2, 48, 130, 1, 60, 2, 1, 0, 2, 65, 0, 230, 157, 160, 242, 74, 222, 87, 0, 77, 180, 91, 175, 217, 166, 2, 95, 193, 239, 195, 140, 224, 57, 84, 207, 46, 172, 113, 196, 20, 133, 117, 205, 45, 7, 224, 41, 40, 195, 254, 124, 14, 84, 223, 147, 67, 198, 48, 36, 53, 161, 112, 46, 153, 90, 19, 123, 94, 247, 5, 116, 1, 238, 32, 15, 2, 3, 1, 0, 1, 2, 65, 0, 191, 59, 140, 255, 254, 23, 123, 91, 148, 19, 240, 71, 213, 26, 181, 51, 68, 181, 150, 153, 214, 65, 148, 83, 45, 103, 239, 250, 225, 237, 125, 173, 111, 244, 37, 124, 87, 178, 86, 10, 14, 207, 63, 105, 213, 37, 81, 23, 230, 4, 222, 179, 144, 40, 252, 163, 190, 7, 241, 221, 28, 54, 225, 209, 2, 33, 0, 235, 132, 229, 150, 99, 182, 176, 194, 198, 65, 210, 160, 184, 70, 82, 49, 235, 199, 14, 11, 92, 66, 237, 45, 220, 72, 235, 1, 244, 145, 205, 57, 2, 33, 0, 250, 171, 146, 180, 188, 194, 14, 152, 52, 64, 38, 52, 158, 86, 46, 109, 66, 100, 122, 43, 88, 167, 143, 98, 104, 143, 160, 60, 171, 185, 31, 135, 2, 33, 0, 206, 47, 255, 203, 100, 170, 137, 31, 75, 240, 78, 84, 212, 95, 4, 16, 158, 73, 27, 27, 136, 255, 50, 163, 166, 169, 211, 204, 87, 111, 217, 201, 2, 33, 0, 177, 51, 194, 213, 3, 175, 7, 84, 47, 115, 189, 206, 106, 180, 47, 195, 203, 48, 110, 112, 224, 14, 43, 189, 124, 127, 51, 222, 79, 226, 225, 87, 2, 32, 67, 23, 190, 222, 106, 22, 115, 139, 217, 244, 178, 53, 153, 99, 5, 176, 72, 77, 193, 61, 67, 134, 37, 238, 69, 66, 159, 28, 39, 5, 238, 125 + ]) + }, + public: { + hash: Uint8Array.from([ + 18, 32, 112, 151, 163, 167, 204, 243, 175, 123, 208, 162, 90, 84, 199, 174, 202, 110, 0, 119, 27, 202, 7, 149, 161, 251, 215, 168, 163, 54, 93, 54, 195, 20 + ]), + key: Uint8Array.from([ + 8, 0, 18, 94, 48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 230, 157, 160, 242, 74, 222, 87, 0, 77, 180, 91, 175, 217, 166, 2, 95, 193, 239, 195, 140, 224, 57, 84, 207, 46, 172, 113, 196, 20, 133, 117, 205, 45, 7, 224, 41, 40, 195, 254, 124, 14, 84, 223, 147, 67, 198, 48, 36, 53, 161, 112, 46, 153, 90, 19, 123, 94, 247, 5, 116, 1, 238, 32, 15, 2, 3, 1, 0, 1 + ]) + }, + verify: { + signature: Uint8Array.from([ + 3, 116, 81, 57, 91, 194, 7, 1, 230, 236, 229, 142, 36, 209, 208, 107, 47, 52, 164, 236, 139, 35, 155, 97, 43, 64, 145, 91, 19, 218, 149, 63, 99, 164, 191, 110, 145, 37, 18, 7, 98, 112, 144, 35, 29, 186, 169, 150, 165, 88, 145, 170, 197, 110, 42, 163, 188, 10, 42, 63, 34, 93, 91, 94, 199, 110, 10, 82, 238, 80, 93, 93, 77, 130, 22, 216, 229, 172, 36, 229, 82, 162, 20, 78, 19, 46, 82, 243, 43, 80, 115, 125, 145, 231, 194, 224, 30, 187, 55, 228, 74, 52, 203, 191, 254, 148, 136, 218, 62, 147, 171, 130, 251, 181, 105, 29, 238, 207, 197, 249, 61, 105, 202, 172, 160, 174, 43, 124, 115, 130, 169, 30, 76, 41, 52, 200, 2, 26, 53, 190, 43, 20, 203, 10, 217, 250, 47, 102, 92, 103, 197, 22, 108, 184, 74, 218, 82, 202, 180, 98, 13, 114, 12, 92, 1, 139, 150, 170, 8, 92, 32, 116, 168, 219, 157, 162, 28, 77, 29, 29, 74, 136, 144, 49, 173, 245, 253, 76, 167, 200, 169, 163, 7, 49, 133, 120, 99, 191, 53, 10, 66, 26, 234, 240, 139, 235, 134, 30, 55, 248, 150, 100, 242, 150, 159, 198, 44, 78, 150, 7, 133, 139, 59, 76, 3, 225, 94, 13, 89, 122, 34, 95, 95, 107, 74, 169, 171, 169, 222, 25, 191, 182, 148, 116, 66, 67, 102, 12, 193, 217, 247, 243, 148, 233, 161, 157 + ]), + data: Uint8Array.from([ + 10, 16, 27, 128, 228, 220, 147, 176, 53, 105, 175, 171, 32, 213, 35, 236, 203, 60, 18, 171, 2, 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 181, 113, 138, 108, 208, 103, 166, 102, 37, 36, 204, 250, 228, 165, 44, 64, 176, 210, 205, 141, 241, 55, 200, 110, 98, 68, 85, 199, 254, 19, 86, 204, 63, 250, 167, 38, 59, 249, 146, 228, 73, 171, 63, 18, 96, 104, 191, 137, 186, 244, 255, 90, 16, 119, 195, 52, 177, 213, 254, 187, 174, 84, 174, 173, 12, 236, 53, 234, 3, 209, 82, 37, 78, 111, 214, 135, 76, 195, 9, 242, 134, 188, 153, 84, 139, 231, 51, 146, 177, 60, 12, 25, 158, 91, 215, 152, 7, 0, 84, 35, 36, 230, 227, 67, 198, 72, 50, 110, 37, 209, 98, 193, 65, 93, 173, 199, 4, 198, 102, 99, 148, 144, 224, 217, 114, 53, 144, 245, 251, 114, 211, 20, 82, 163, 123, 75, 16, 192, 106, 213, 128, 2, 11, 200, 203, 84, 41, 199, 224, 155, 171, 217, 64, 109, 116, 188, 151, 183, 173, 52, 205, 164, 93, 13, 251, 65, 182, 160, 154, 185, 239, 33, 184, 84, 159, 105, 101, 173, 194, 251, 123, 84, 92, 66, 61, 180, 45, 104, 162, 224, 214, 233, 64, 220, 165, 2, 104, 116, 150, 2, 234, 203, 112, 21, 124, 23, 48, 66, 30, 63, 30, 36, 246, 135, 203, 218, 115, 22, 189, 39, 39, 125, 205, 65, 222, 220, 77, 18, 84, 121, 161, 153, 125, 25, 139, 137, 170, 239, 150, 106, 119, 168, 216, 140, 113, 121, 26, 53, 118, 110, 53, 192, 244, 252, 145, 85, 2, 3, 1, 0, 1, 26, 17, 80, 45, 50, 53, 54, 44, 80, 45, 51, 56, 52, 44, 80, 45, 53, 50, 49, 34, 24, 65, 69, 83, 45, 50, 53, 54, 44, 65, 69, 83, 45, 49, 50, 56, 44, 66, 108, 111, 119, 102, 105, 115, 104, 42, 13, 83, 72, 65, 50, 53, 54, 44, 83, 72, 65, 53, 49, 50, 10, 16, 220, 83, 240, 105, 6, 203, 78, 83, 210, 115, 6, 106, 98, 82, 1, 161, 18, 171, 2, 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 185, 234, 19, 191, 164, 33, 65, 94, 87, 42, 74, 83, 224, 25, 142, 44, 26, 7, 92, 242, 189, 42, 170, 197, 178, 92, 45, 240, 107, 141, 128, 59, 122, 252, 48, 140, 4, 85, 85, 203, 3, 197, 8, 127, 120, 98, 44, 169, 135, 196, 70, 137, 117, 180, 177, 134, 170, 35, 165, 88, 105, 30, 114, 138, 11, 96, 68, 99, 18, 149, 223, 166, 105, 12, 176, 77, 48, 214, 22, 236, 17, 154, 213, 209, 158, 169, 202, 5, 100, 210, 83, 90, 201, 38, 205, 246, 231, 106, 63, 86, 222, 143, 157, 173, 62, 4, 85, 232, 20, 188, 6, 209, 186, 132, 192, 117, 146, 181, 233, 26, 0, 240, 138, 206, 91, 170, 114, 137, 217, 132, 139, 242, 144, 213, 103, 101, 190, 146, 188, 250, 188, 134, 255, 70, 125, 78, 65, 136, 239, 190, 206, 139, 155, 140, 163, 233, 170, 247, 205, 87, 209, 19, 29, 173, 10, 147, 43, 28, 90, 46, 6, 197, 217, 186, 66, 68, 126, 76, 64, 184, 8, 170, 23, 79, 243, 223, 119, 133, 118, 50, 226, 44, 246, 176, 10, 161, 219, 83, 54, 68, 248, 5, 14, 177, 114, 54, 63, 11, 71, 136, 142, 56, 151, 123, 230, 61, 80, 15, 180, 42, 49, 220, 148, 99, 231, 20, 230, 220, 85, 207, 187, 37, 210, 137, 171, 125, 71, 14, 53, 100, 91, 83, 209, 50, 132, 165, 253, 25, 161, 5, 97, 164, 163, 83, 95, 53, 2, 3, 1, 0, 1, 26, 17, 80, 45, 50, 53, 54, 44, 80, 45, 51, 56, 52, 44, 80, 45, 53, 50, 49, 34, 15, 65, 69, 83, 45, 50, 53, 54, 44, 65, 69, 83, 45, 49, 50, 56, 42, 13, 83, 72, 65, 50, 53, 54, 44, 83, 72, 65, 53, 49, 50, 4, 97, 54, 203, 112, 136, 34, 231, 162, 19, 154, 131, 27, 105, 26, 121, 238, 120, 25, 203, 66, 232, 53, 198, 20, 19, 96, 119, 218, 90, 64, 170, 3, 132, 116, 1, 87, 116, 232, 165, 161, 198, 117, 167, 60, 145, 1, 253, 108, 50, 150, 117, 8, 140, 133, 48, 30, 236, 36, 84, 186, 22, 144, 87, 101 + ]), + publicKey: Uint8Array.from([ + 8, 0, 18, 166, 2, 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 181, 113, 138, 108, 208, 103, 166, 102, 37, 36, 204, 250, 228, 165, 44, 64, 176, 210, 205, 141, 241, 55, 200, 110, 98, 68, 85, 199, 254, 19, 86, 204, 63, 250, 167, 38, 59, 249, 146, 228, 73, 171, 63, 18, 96, 104, 191, 137, 186, 244, 255, 90, 16, 119, 195, 52, 177, 213, 254, 187, 174, 84, 174, 173, 12, 236, 53, 234, 3, 209, 82, 37, 78, 111, 214, 135, 76, 195, 9, 242, 134, 188, 153, 84, 139, 231, 51, 146, 177, 60, 12, 25, 158, 91, 215, 152, 7, 0, 84, 35, 36, 230, 227, 67, 198, 72, 50, 110, 37, 209, 98, 193, 65, 93, 173, 199, 4, 198, 102, 99, 148, 144, 224, 217, 114, 53, 144, 245, 251, 114, 211, 20, 82, 163, 123, 75, 16, 192, 106, 213, 128, 2, 11, 200, 203, 84, 41, 199, 224, 155, 171, 217, 64, 109, 116, 188, 151, 183, 173, 52, 205, 164, 93, 13, 251, 65, 182, 160, 154, 185, 239, 33, 184, 84, 159, 105, 101, 173, 194, 251, 123, 84, 92, 66, 61, 180, 45, 104, 162, 224, 214, 233, 64, 220, 165, 2, 104, 116, 150, 2, 234, 203, 112, 21, 124, 23, 48, 66, 30, 63, 30, 36, 246, 135, 203, 218, 115, 22, 189, 39, 39, 125, 205, 65, 222, 220, 77, 18, 84, 121, 161, 153, 125, 25, 139, 137, 170, 239, 150, 106, 119, 168, 216, 140, 113, 121, 26, 53, 118, 110, 53, 192, 244, 252, 145, 85, 2, 3, 1, 0, 1 + ]) + } +} diff --git a/packages/crypto/test/fixtures/go-key-secp256k1.ts b/packages/crypto/test/fixtures/go-key-secp256k1.ts new file mode 100644 index 0000000000..789d52aa2a --- /dev/null +++ b/packages/crypto/test/fixtures/go-key-secp256k1.ts @@ -0,0 +1,29 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +// The keypair and signature below were generated in a gore repl session (https://github.com/motemen/gore) +// using the secp256k1 fork of go-libp2p-crypto by github user @vyzo +// +// gore> :import github.com/vyzo/go-libp2p-crypto +// gore> :import crypto/rand +// gore> :import io/ioutil +// gore> priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Secp256k1, 256, rand.Reader) +// gore> privBytes, err := priv.Bytes() +// gore> pubBytes, err := pub.Bytes() +// gore> msg := []byte("hello! and welcome to some awesome crypto primitives") +// gore> sig, err := priv.Sign(msg) +// gore> ioutil.WriteFile("/tmp/secp-go-priv.bin", privBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-pub.bin", pubBytes, 0644) +// gore> ioutil.WriteFile("/tmp/secp-go-sig.bin", sig, 0644) +// +// The generated files were then read in a node repl with e.g.: +// > fs.readFileSync('/tmp/secp-go-pub.bin').toString('hex') +// '08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf' +// +// and the results copy/pasted in here + +export default { + privateKey: uint8ArrayFromString('08021220358f15db8c2014d570e8e3a622454e2273975a3cca443ec0c45375b13d381d18', 'base16'), + publicKey: uint8ArrayFromString('08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf', 'base16'), + message: uint8ArrayFromString('hello! and welcome to some awesome crypto primitives'), + signature: uint8ArrayFromString('304402200e4c629e9f5d99439115e60989cd40087f6978c36078b0b50cf3d30af5c38d4102204110342c8e7f0809897c1c7a66e49e1c6b7cb0a6ed6993640ec2fe742c1899a9', 'base16') +} diff --git a/packages/crypto/test/fixtures/go-stretch-key.ts b/packages/crypto/test/fixtures/go-stretch-key.ts new file mode 100644 index 0000000000..4fd4e9c4c0 --- /dev/null +++ b/packages/crypto/test/fixtures/go-stretch-key.ts @@ -0,0 +1,30 @@ + +export default [{ + cipher: 'AES-256' as 'AES-256', + hash: 'SHA256' as 'SHA256', + secret: Uint8Array.from([ + 195, 191, 209, 165, 209, 201, 127, 122, 136, 111, 31, 66, 111, 68, 38, 155, 216, 204, 46, 181, 200, 188, 170, 204, 104, 74, 239, 251, 173, 114, 222, 234 + ]), + k1: { + iv: Uint8Array.from([ + 208, 132, 203, 169, 253, 52, 40, 83, 161, 91, 17, 71, 33, 136, 67, 96 + ]), + cipherKey: Uint8Array.from([ + 156, 48, 241, 157, 92, 248, 153, 186, 114, 127, 195, 114, 106, 104, 215, 133, 35, 11, 131, 137, 123, 70, 74, 26, 15, 60, 189, 32, 67, 221, 115, 137 + ]), + macKey: Uint8Array.from([ + 6, 179, 91, 245, 224, 56, 153, 120, 77, 140, 29, 5, 15, 213, 187, 65, 137, 230, 202, 120 + ]) + }, + k2: { + iv: Uint8Array.from([ + 236, 17, 34, 141, 90, 106, 197, 56, 197, 184, 157, 135, 91, 88, 112, 19 + ]), + cipherKey: Uint8Array.from([ + 151, 145, 195, 219, 76, 195, 102, 109, 187, 231, 100, 150, 132, 245, 251, 130, 254, 37, 178, 55, 227, 34, 114, 39, 238, 34, 2, 193, 107, 130, 32, 87 + ]), + macKey: Uint8Array.from([ + 3, 229, 77, 212, 241, 217, 23, 113, 220, 126, 38, 255, 18, 117, 108, 205, 198, 89, 1, 236 + ]) + } +}] diff --git a/packages/crypto/test/fixtures/secp256k1.ts b/packages/crypto/test/fixtures/secp256k1.ts new file mode 100644 index 0000000000..507716f0db --- /dev/null +++ b/packages/crypto/test/fixtures/secp256k1.ts @@ -0,0 +1,8 @@ +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +export default { + // protobuf marshalled key pair generated with libp2p-crypto-secp256k1 + // and marshalled with libp2p-crypto.marshalPublicKey / marshalPrivateKey + pbmPrivateKey: uint8ArrayFromString('08021220e0600103010000000100000000000000be1dc82c2e000000e8d6030301000000', 'base16'), + pbmPublicKey: uint8ArrayFromString('0802122103a9a7272a726fa083abf31ba44037f8347fbc5e5d3113d62a7c6bc26752fd8ee1', 'base16') +} diff --git a/packages/crypto/test/helpers/test-garbage-error-handling.ts b/packages/crypto/test/helpers/test-garbage-error-handling.ts new file mode 100644 index 0000000000..60316b26f2 --- /dev/null +++ b/packages/crypto/test/helpers/test-garbage-error-handling.ts @@ -0,0 +1,28 @@ +/* eslint-env mocha */ +import util from 'util' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +const garbage = [uint8ArrayFromString('00010203040506070809', 'base16'), {}, null, false, undefined, true, 1, 0, uint8ArrayFromString(''), 'aGVsbG93b3JsZA==', 'helloworld', ''] + +export function testGarbage (fncName: string, fnc: (...args: Uint8Array[]) => Promise, num?: number, skipBuffersAndStrings?: boolean): void { + const count = num ?? 1 + + garbage.forEach((garbage) => { + if (skipBuffersAndStrings === true && (garbage instanceof Uint8Array || (typeof garbage) === 'string')) { + // skip this garbage because it's a Uint8Array or a String and we were told do do that + return + } + const args: any[] = [] + for (let i = 0; i < count; i++) { + args.push(garbage) + } + it(fncName + '(' + args.map(garbage => util.inspect(garbage)).join(', ') + ')', async () => { + try { + await fnc.apply(null, args) + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + }) +} diff --git a/packages/crypto/test/hmac/hmac.spec.ts b/packages/crypto/test/hmac/hmac.spec.ts new file mode 100644 index 0000000000..8ead33bfae --- /dev/null +++ b/packages/crypto/test/hmac/hmac.spec.ts @@ -0,0 +1,17 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' + +const hashes = ['SHA1', 'SHA256', 'SHA512'] as ['SHA1', 'SHA256', 'SHA512'] + +describe('HMAC', () => { + hashes.forEach((hash) => { + it(`${hash} - sign and verify`, async () => { + const hmac = await crypto.hmac.create(hash, uint8ArrayFromString('secret')) + const sig = await hmac.digest(uint8ArrayFromString('hello world')) + expect(sig).to.have.length(hmac.length) + }) + }) +}) diff --git a/packages/crypto/test/keys/ed25519.spec.ts b/packages/crypto/test/keys/ed25519.spec.ts new file mode 100644 index 0000000000..a52706b067 --- /dev/null +++ b/packages/crypto/test/keys/ed25519.spec.ts @@ -0,0 +1,224 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import { Ed25519PrivateKey } from '../../src/keys/ed25519-class.js' +import fixtures from '../fixtures/go-key-ed25519.js' +import { testGarbage } from '../helpers/test-garbage-error-handling.js' + +const ed25519 = crypto.keys.supportedKeys.ed25519 + +/** @typedef {import("libp2p-crypto").PrivateKey} PrivateKey */ + +describe('ed25519', function () { + this.timeout(20 * 1000) + let key: Ed25519PrivateKey + before(async () => { + const generated = await crypto.keys.generateKeyPair('Ed25519', 512) + + if (!(generated instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + key = generated + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(ed25519.Ed25519PrivateKey) + const digest = await key.hash() + expect(digest).to.have.length(34) + }) + + it('generates a valid key from seed', async () => { + const seed = crypto.randomBytes(32) + const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + expect(seededkey).to.be.an.instanceof(ed25519.Ed25519PrivateKey) + const digest = await seededkey.hash() + expect(digest).to.have.length(34) + }) + + it('generates the same key from the same seed', async () => { + const seed = crypto.randomBytes(32) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + expect(seededkey1.equals(seededkey2)).to.eql(true) + expect(seededkey1.public.equals(seededkey2.public)).to.eql(true) + }) + + it('generates different keys for different seeds', async () => { + const seed1 = crypto.randomBytes(32) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed1, 512) + const seed2 = crypto.randomBytes(32) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed2, 512) + expect(seededkey1.equals(seededkey2)).to.eql(false) + expect(seededkey1.public.equals(seededkey2.public)).to.eql(false) + }) + + it('signs', async () => { + const text = crypto.randomBytes(512) + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.be.eql(true) + }) + + it('encoding', () => { + const keyMarshal = key.marshal() + const key2 = ed25519.unmarshalEd25519PrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = ed25519.unmarshalEd25519PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.verify.privateKey) + const id = await key.id() + expect(id).to.eql('12D3KooWLqLxEfJ9nDdEe8Kh8PFvNPQRYDQBwyL7CMM7HhVd5LsX') + }) + + it('should export a password encrypted libp2p-key', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + + if (!(key instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + const encryptedKey = await key.export('my secret') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should export a libp2p-key with no password to encrypt', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + + if (!(key instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + const encryptedKey = await key.export('') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, '') + + if (!(importedKey instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const key = await crypto.keys.generateKeyPair('Ed25519') + const encryptedKey = await key.export('my secret', 'libp2p-key') + try { + await crypto.keys.importKey(encryptedKey, 'not my secret') + } catch (err) { + expect(err).to.exist() + return + } + expect.fail('should have thrown') + }) + + describe('key equals', () => { + it('equals itself', () => { + expect( + key.equals(key) + ).to.eql( + true + ) + + expect( + key.public.equals(key.public) + ).to.eql( + true + ) + }) + + it('not equals other key', async () => { + const key2 = await crypto.keys.generateKeyPair('Ed25519', 512) + + if (!(key2 instanceof Ed25519PrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('sign and verify from seed', async () => { + const seed = new Uint8Array(32).fill(1) + const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed) + const data = uint8ArrayFromString('hello world') + const sig = await seededkey.sign(data) + const valid = await seededkey.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.be.eql(false) + }) + + describe('throws error instead of crashing', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + testGarbage('key.verify', key.verify.bind(key), 2) + testGarbage('crypto.keys.unmarshalPrivateKey', crypto.keys.unmarshalPrivateKey.bind(crypto.keys)) + }) + + describe('go interop', () => { + // @ts-check + it('verifies with data from go', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + const ok = await key.verify(fixtures.verify.data, fixtures.verify.signature) + expect(ok).to.eql(true) + }) + + it('does not include the redundant public key when marshalling privatekey', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.redundantPubKey.privateKey) + const bytes = key.marshal() + expect(bytes.length).to.equal(64) + expect(bytes.subarray(32)).to.eql(key.public.marshal()) + }) + + it('verifies with data from go with redundant public key', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.redundantPubKey.publicKey) + const ok = await key.verify(fixtures.redundantPubKey.data, fixtures.redundantPubKey.signature) + expect(ok).to.eql(true) + }) + + it('generates the same signature as go', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.verify.privateKey) + const sig = await key.sign(fixtures.verify.data) + expect(sig).to.eql(fixtures.verify.signature) + }) + + it('generates the same signature as go with redundant public key', async () => { + const key = await crypto.keys.unmarshalPrivateKey(fixtures.redundantPubKey.privateKey) + const sig = await key.sign(fixtures.redundantPubKey.data) + expect(sig).to.eql(fixtures.redundantPubKey.signature) + }) + }) +}) diff --git a/packages/crypto/test/keys/ephemeral-keys.spec.ts b/packages/crypto/test/keys/ephemeral-keys.spec.ts new file mode 100644 index 0000000000..1caadbc2db --- /dev/null +++ b/packages/crypto/test/keys/ephemeral-keys.spec.ts @@ -0,0 +1,62 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from '../fixtures/go-elliptic-key.js' + +const curves = ['P-256', 'P-384'] // 'P-521' fails in tests :( no clue why +const lengths: Record = { + 'P-256': 65, + 'P-384': 97, + 'P-521': 133 +} + +const secretLengths: Record = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} + +describe('generateEphemeralKeyPair', () => { + curves.forEach((curve) => { + it(`generate and shared key ${curve}`, async () => { + const keys = await Promise.all([ + crypto.keys.generateEphemeralKeyPair(curve), + crypto.keys.generateEphemeralKeyPair(curve) + ]) + + expect(keys[0].key).to.have.length(lengths[curve]) + expect(keys[1].key).to.have.length(lengths[curve]) + + const shared = await keys[0].genSharedKey(keys[1].key) + expect(shared).to.have.length(secretLengths[curve]) + }) + }) + + describe('go interop', () => { + it('generates a shared secret', async () => { + const curve = fixtures.curve + + const keys = await Promise.all([ + crypto.keys.generateEphemeralKeyPair(curve), + crypto.keys.generateEphemeralKeyPair(curve) + ]) + + const alice = keys[0] + const bob = keys[1] + bob.key = fixtures.bob.public + + const secrets = await Promise.all([ + alice.genSharedKey(bob.key), + bob.genSharedKey(alice.key, fixtures.bob) + ]) + + expect(secrets[0]).to.eql(secrets[1]) + expect(secrets[0]).to.have.length(32) + }) + }) + + it('handles bad curve name', async () => { + await expect(crypto.keys.generateEphemeralKeyPair('bad name')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_CURVE') + }) +}) diff --git a/packages/crypto/test/keys/importer.spec.ts b/packages/crypto/test/keys/importer.spec.ts new file mode 100644 index 0000000000..5fe0e26427 --- /dev/null +++ b/packages/crypto/test/keys/importer.spec.ts @@ -0,0 +1,20 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { exporter } from '../../src/keys/exporter.js' +import { importer } from '../../src/keys/importer.js' + +describe('libp2p-crypto importer/exporter', function () { + it('roundtrips', async () => { + for (const password of ['', 'password']) { + const secret = new Uint8Array(32) + for (let i = 0; i < secret.length; i++) { + secret[i] = i + } + + const exported = await exporter(secret, password) + const imported = await importer(exported, password) + expect(imported).to.deep.equal(secret) + } + }) +}) diff --git a/packages/crypto/test/keys/key-stretcher.spec.ts b/packages/crypto/test/keys/key-stretcher.spec.ts new file mode 100644 index 0000000000..c0b902d36a --- /dev/null +++ b/packages/crypto/test/keys/key-stretcher.spec.ts @@ -0,0 +1,59 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import * as crypto from '../../src/index.js' +import fixtures from '../fixtures/go-stretch-key.js' +import type { ECDHKey } from '../../src/keys/interface.js' + +describe('keyStretcher', () => { + describe('generate', () => { + const ciphers = ['AES-128', 'AES-256', 'Blowfish'] as Array<'AES-128' | 'AES-256' | 'Blowfish'> + const hashes = ['SHA1', 'SHA256', 'SHA512'] as Array<'SHA1' | 'SHA256' | 'SHA512'> + let res: ECDHKey + let secret: Uint8Array + + before(async () => { + res = await crypto.keys.generateEphemeralKeyPair('P-256') + secret = await res.genSharedKey(res.key) + }) + + ciphers.forEach((cipher) => { + hashes.forEach((hash) => { + it(`${cipher} - ${hash}`, async () => { + const keys = await crypto.keys.keyStretcher(cipher, hash, secret) + expect(keys.k1).to.exist() + expect(keys.k2).to.exist() + }) + }) + }) + + it('handles invalid cipher type', () => { + // @ts-expect-error cipher name is invalid + return expect(crypto.keys.keyStretcher('invalid-cipher', 'SHA256', 'secret')).to.eventually.be.rejected().with.property('code', 'ERR_INVALID_CIPHER_TYPE') + }) + + it('handles missing hash type', () => { + // @ts-expect-error secret name is invalid + return expect(crypto.keys.keyStretcher('AES-128', undefined, 'secret')).to.eventually.be.rejected().with.property('code', 'ERR_MISSING_HASH_TYPE') + }) + }) + + describe('go interop', () => { + fixtures.forEach((test) => { + it(`${test.cipher} - ${test.hash}`, async () => { + const cipher = test.cipher + const hash = test.hash + const secret = test.secret + const keys = await crypto.keys.keyStretcher(cipher, hash, secret) + + expect(keys.k1.iv).to.be.eql(test.k1.iv) + expect(keys.k1.cipherKey).to.be.eql(test.k1.cipherKey) + expect(keys.k1.macKey).to.be.eql(test.k1.macKey) + + expect(keys.k2.iv).to.be.eql(test.k2.iv) + expect(keys.k2.cipherKey).to.be.eql(test.k2.cipherKey) + expect(keys.k2.macKey).to.be.eql(test.k2.macKey) + }) + }) + }) +}) diff --git a/packages/crypto/test/keys/rsa.spec.ts b/packages/crypto/test/keys/rsa.spec.ts new file mode 100644 index 0000000000..27c9740b54 --- /dev/null +++ b/packages/crypto/test/keys/rsa.spec.ts @@ -0,0 +1,414 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import { RsaPrivateKey } from '../../src/keys/rsa-class.js' +import fixtures from '../fixtures/go-key-rsa.js' +import { testGarbage } from '../helpers/test-garbage-error-handling.js' + +const rsa = crypto.keys.supportedKeys.rsa + +/** @typedef {import('libp2p-crypto').keys.supportedKeys.rsa.RsaPrivateKey} RsaPrivateKey */ + +describe('RSA', function () { + this.timeout(20 * 1000) + let key: RsaPrivateKey + + before(async () => { + key = await rsa.generateKeyPair(512) + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(rsa.RsaPrivateKey) + const digest = await key.hash() + expect(digest).to.have.length(34) + }) + + it('signs', async () => { + const text = key.genSecret() + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.be.eql(true) + }) + + it('encoding', async () => { + const keyMarshal = key.marshal() + const key2 = await rsa.unmarshalRsaPrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = rsa.unmarshalRsaPublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const key = await crypto.keys.unmarshalPrivateKey(uint8ArrayFromString('CAASqAkwggSkAgEAAoIBAQCk0O+6oNRxhcdZe2GxEDrFBkDV4TZFZnp2ly/dL1cGMBql/8oXPZgei6h7+P5zzfDq2YCfwbjbf0IVY1AshRl6B5VGE1WS+9p1y1OZxJf5os6V1ENnTi6FTcyuBl4BN8dmIKOif0hqgqflaT5OhfYZDXfbJyVQj4vb2+Stu2Xpph3nwqAnTw/7GC/7jrt2Cq6Tu1PoZi36wSwEPYW3eQ1HAYxZjTYYDXl2iyHygnTcbkGRwAQ7vjk+mW7u60zyoolCm9f6Y7c/orJ33DDUocbaGJLlHcfd8bioBwaZy/2m7q43X8pQs0Q1/iwUt0HHZj1YARmHKbh0zR31ciFiV37dAgMBAAECggEADtJBNKnA4QKURj47r0YT2uLwkqtBi6UnDyISalQXAdXyl4n0nPlrhBewC5H9I+HZr+zmTbeIjaiYgz7el1pSy7AB4v7bG7AtWZlyx6mvtwHGjR+8/f3AXjl8Vgv5iSeAdXUq8fJ7SyS7v3wi38HZOzCEXj9bci6ud5ODMYJgLE4gZD0+i1+/V9cpuYfGpS/gLTLEMQLiw/9o8NSZ7sAnxg0UlYhotqaQY23hvXPBOe+0oa95zl2n6XTxCafa3dQl/B6CD1tUq9dhbQew4bxqMq/mhRO9pREEqZ083Uh+u4PTc1BeHgIQaS864pHPb+AY1F7KDvPtHhdojnghp8d70QKBgQDeRYFxo6sd04ohY86Z/i9icVYIyCvfXAKnaMKeGUjK7ou6sDJwFX8W97+CzXpZ/vffsk/l5GGhC50KqrITxHAy/h5IjyDODfps7NMIp0Dm9sO4PWibbw3OOVBRc8w3b3i7I8MrUUA1nLHE1T1HA1rKOTz5jYhE0fi9XKiT1ciKOQKBgQC903w+n9y7M7eaMW7Z5/13kZ7PS3HlM681eaPrk8J4J+c6miFF40/8HOsmarS38v0fgTeKkriPz5A7aLzRHhSiOnp350JNM6c3sLwPEs2qx/CRuWWx1rMERatfDdUH6mvlK6QHu0QgSfQR27EO6a6XvVSJXbvFmimjmtIaz/IpxQKBgQDWJ9HYVAGC81abZTaiWK3/A4QJYhQjWNuVwPICsgnYvI4Uib+PDqcs0ffLZ38DRw48kek5bxpBuJbOuDhro1EXUJCNCJpq7jzixituovd9kTRyR3iKii2bDM2+LPwOTXDdnk9lZRugjCEbrPkleq33Ob7uEtfAty4aBTTHe6uEwQKBgQCB+2q8RyMSXNuADhFlzOFXGrOwJm0bEUUMTPrduRQUyt4e1qOqA3klnXe3mqGcxBpnlEe/76/JacvNom6Ikxx16a0qpYRU8OWz0KU1fR6vrrEgV98241k5t6sdL4+MGA1Bo5xyXtzLb1hdUh3vpDwVU2OrnC+To3iXus/b5EBiMQKBgEI1OaBcFiyjgLGEyFKoZbtzH1mdatTExfrAQqCjOVjQByoMpGhHTXwEaosvyYu63Pa8AJPT7juSGaiKYEJFcXO9BiNyVfmQiqSHJcYeuh+fmO9IlHRHgy5xaIIC00AHS2vC/gXwmXAdPis6BZqDJeiCuOLWJ94QXn8JBT8IgGAI', 'base64pad')) + const id = await key.id() + expect(id).to.eql('QmQgsppVMDUpe83wcAqaemKbYvHeF127gnSFQ1xFnBodVw') + }) + + describe('key equals', () => { + it('equals itself', () => { + expect(key.equals(key)).to.eql(true) + + expect(key.public.equals(key.public)).to.eql(true) + }) + + it('not equals other key', async () => { + const key2 = await crypto.keys.generateKeyPair('RSA', 512) + + if (!(key2 instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.be.eql(true) + }) + + it('encrypt and decrypt', () => { + const data = uint8ArrayFromString('hello world') + const enc = key.public.encrypt(data) + const dec = key.decrypt(enc) + expect(dec).to.be.eql(data) + }) + + it('encrypt decrypt browser/node interop', async () => { + // @ts-check + /** + * @type {any} + */ + const id = await crypto.keys.unmarshalPrivateKey(uint8ArrayFromString('CAASqAkwggSkAgEAAoIBAQCk0O+6oNRxhcdZe2GxEDrFBkDV4TZFZnp2ly/dL1cGMBql/8oXPZgei6h7+P5zzfDq2YCfwbjbf0IVY1AshRl6B5VGE1WS+9p1y1OZxJf5os6V1ENnTi6FTcyuBl4BN8dmIKOif0hqgqflaT5OhfYZDXfbJyVQj4vb2+Stu2Xpph3nwqAnTw/7GC/7jrt2Cq6Tu1PoZi36wSwEPYW3eQ1HAYxZjTYYDXl2iyHygnTcbkGRwAQ7vjk+mW7u60zyoolCm9f6Y7c/orJ33DDUocbaGJLlHcfd8bioBwaZy/2m7q43X8pQs0Q1/iwUt0HHZj1YARmHKbh0zR31ciFiV37dAgMBAAECggEADtJBNKnA4QKURj47r0YT2uLwkqtBi6UnDyISalQXAdXyl4n0nPlrhBewC5H9I+HZr+zmTbeIjaiYgz7el1pSy7AB4v7bG7AtWZlyx6mvtwHGjR+8/f3AXjl8Vgv5iSeAdXUq8fJ7SyS7v3wi38HZOzCEXj9bci6ud5ODMYJgLE4gZD0+i1+/V9cpuYfGpS/gLTLEMQLiw/9o8NSZ7sAnxg0UlYhotqaQY23hvXPBOe+0oa95zl2n6XTxCafa3dQl/B6CD1tUq9dhbQew4bxqMq/mhRO9pREEqZ083Uh+u4PTc1BeHgIQaS864pHPb+AY1F7KDvPtHhdojnghp8d70QKBgQDeRYFxo6sd04ohY86Z/i9icVYIyCvfXAKnaMKeGUjK7ou6sDJwFX8W97+CzXpZ/vffsk/l5GGhC50KqrITxHAy/h5IjyDODfps7NMIp0Dm9sO4PWibbw3OOVBRc8w3b3i7I8MrUUA1nLHE1T1HA1rKOTz5jYhE0fi9XKiT1ciKOQKBgQC903w+n9y7M7eaMW7Z5/13kZ7PS3HlM681eaPrk8J4J+c6miFF40/8HOsmarS38v0fgTeKkriPz5A7aLzRHhSiOnp350JNM6c3sLwPEs2qx/CRuWWx1rMERatfDdUH6mvlK6QHu0QgSfQR27EO6a6XvVSJXbvFmimjmtIaz/IpxQKBgQDWJ9HYVAGC81abZTaiWK3/A4QJYhQjWNuVwPICsgnYvI4Uib+PDqcs0ffLZ38DRw48kek5bxpBuJbOuDhro1EXUJCNCJpq7jzixituovd9kTRyR3iKii2bDM2+LPwOTXDdnk9lZRugjCEbrPkleq33Ob7uEtfAty4aBTTHe6uEwQKBgQCB+2q8RyMSXNuADhFlzOFXGrOwJm0bEUUMTPrduRQUyt4e1qOqA3klnXe3mqGcxBpnlEe/76/JacvNom6Ikxx16a0qpYRU8OWz0KU1fR6vrrEgV98241k5t6sdL4+MGA1Bo5xyXtzLb1hdUh3vpDwVU2OrnC+To3iXus/b5EBiMQKBgEI1OaBcFiyjgLGEyFKoZbtzH1mdatTExfrAQqCjOVjQByoMpGhHTXwEaosvyYu63Pa8AJPT7juSGaiKYEJFcXO9BiNyVfmQiqSHJcYeuh+fmO9IlHRHgy5xaIIC00AHS2vC/gXwmXAdPis6BZqDJeiCuOLWJ94QXn8JBT8IgGAI', 'base64pad')) + + if (!(id instanceof RsaPrivateKey)) { + throw new Error('Key was incorrect type') + } + + const msg = uint8ArrayFromString('hello') + + // browser + const dec1 = id.decrypt(uint8ArrayFromString('YRFUDx8UjbWSfDS84cDA4WowaaOmd1qFNAv5QutodCKYb9uPtU/tDiAvJzOGu5DCJRo2J0l/35P2weiB4/C2Cb1aZgXKMx/QQC+2jSJiymhqcZaYerjTvkCFwkjCaqthoVo/YXxsaFZ1q7bdTZUDH1TaJR7hWfSyzyPcA8c0w43MIsw16pY8ZaPSclvnCwhoTg1JGjMk6te3we7+wR8QU7VrPhs54mZWxrpu3NQ8xZ6xQqIedsEiNhBUccrCSzYghgsP0Ae/8iKyGyl3U6IegsJNn8jcocvzOJrmU03rgIFPjvuBdaqB38xDSTjbA123KadB28jNoSZh18q/yH3ZIg==', 'base64pad')) + expect(dec1).to.be.eql(msg) + // node + const dec2 = id.decrypt(uint8ArrayFromString('e6yxssqXsWc27ozDy0PGKtMkCS28KwFyES2Ijz89yiz+w6bSFkNOhHPKplpPzgQEuNoUGdbseKlJFyRYHjIT8FQFBHZM8UgSkgoimbY5on4xSxXs7E5/+twjqKdB7oNveTaTf7JCwaeUYnKSjbiYFEawtMiQE91F8sTT7TmSzOZ48tUhnddAAZ3Ac/O3Z9MSAKOCDipi+JdZtXRT8KimGt36/7hjjosYmPuHR1Xy/yMTL6SMbXtBM3yAuEgbQgP+q/7kHMHji3/JvTpYdIUU+LVtkMusXNasRA+UWG2zAht18vqjFMsm9JTiihZw9jRHD4vxAhf75M992tnC+0ZuQg==', 'base64pad')) + expect(dec2).to.be.eql(msg) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.be.eql(false) + }) + + describe('export and import', () => { + it('password protected PKCS #8', async () => { + const pem = await key.export('my secret', 'pkcs-8') + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + const clone = await crypto.keys.importKey(pem, 'my secret') + + if (!(clone instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + }) + + it('defaults to PKCS #8', async () => { + const pem = await key.export('another secret') + expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + const clone = await crypto.keys.importKey(pem, 'another secret') + + if (!(clone instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(clone).to.exist() + expect(key.equals(clone)).to.eql(true) + }) + + it('should export a password encrypted libp2p-key', async () => { + const encryptedKey = await key.export('my secret', 'libp2p-key') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof RsaPrivateKey)) { + throw new Error('Wrong kind of key imported') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const encryptedKey = await key.export('my secret', 'libp2p-key') + try { + await crypto.keys.importKey(encryptedKey, 'not my secret') + } catch (err) { + expect(err).to.exist() + return + } + expect.fail('should have thrown') + }) + + it('needs correct password', async () => { + const pem = await key.export('another secret') + try { + await crypto.keys.importKey(pem, 'not the secret') + } catch (err) { + return // expected + } + throw new Error('Expected error to be thrown') + }) + + it('handles invalid export type', () => { + return expect(key.export('secret', 'invalid-type')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_EXPORT_FORMAT') + }) + }) + + describe('throws error instead of crashing', () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + testGarbage('key.verify', key.verify.bind(key), 2, true) + testGarbage( + 'crypto.keys.unmarshalPrivateKey', + crypto.keys.unmarshalPrivateKey.bind(crypto.keys) + ) + }) + + describe('go interop', () => { + it('verifies with data from go', async () => { + const key = crypto.keys.unmarshalPublicKey(fixtures.verify.publicKey) + const ok = await key.verify(fixtures.verify.data, fixtures.verify.signature) + expect(ok).to.equal(true) + }) + }) + + describe('openssl interop', () => { + it('can read a private key', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:3072 + * -pkeyopt rsa_keygen_pubexp:65537 + */ + const pem = `-----BEGIN PRIVATE KEY----- +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 +4pMwXeXP+LO8NIfRXV8mgrm86g== +-----END PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, '') + expect(key).to.exist() + const id = await key.id() + expect(id).to.equal('QmfWu2Xp8DZzCkZZzoPB9rcrq4R4RZid6AWE6kmrUAzuHy') + }) + + // AssertionError: expected 'this only supports pkcs5PBES2' to not exist + it.skip('can read a private encrypted key (v1)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQI2563Jugj/KkCAggABIICgPxHkKtUUE8EWevq +eX9nTjqpbsv0QoXQMhegfxDELJLU8tj6V0bWNt7QDdfQ1n6FRgnNvNGick6gyqHH +yH9qC2oXwkDFP7OrHp2NEZd7DHQLLc+L4KJ/0dzsiZ1U9no7XzQMUay9Bc918ADE +pN2/EqigWkaG4gNjkAeKWr6+BNRevDXlSvls7YDboNcTiACi5zJkthivB9g3vT1m +gPdN6Gf/mmqtBTDHeqj5QsmXYqeCyo5b26JgYsziABVZDHph4ekPUsTvudRpE9Ex +baXwdYEAZxVpSbTvQ3A5qysjSZeM9ttfRTSSwL391q7dViz4+aujpk0Vj7piH+1B +CkfO8/XudRdRlnOe+KjMidktKCsMGCIOW92IlfMvIQ/Zn1GTYj9bRXONFNJ2WPND +UmCKnL7cmworwg/weRorrGKBWIGspU+tDASOPSvIGKo6Hoxm4CN1TpDRY7DAGlgm +Y3TEbMYfpXyzkPjvAhJDt03D3J9PrTO6uM5d7YUaaTmJ2TQFQVF2Lc3Uz8lDJLs0 +ZYtfQ/4H+YY2RrX7ua7t6ArUcYXZtv0J4lRYWjwV8fGPUVc0d8xLJU0Yjf4BD7K8 +rsavHo9b5YvBUX7SgUyxAEembEOe3SjQ+gPu2U5wovcjUuC9eItEEsXGrx30BQ0E +8BtK2+hp0eMkW5/BYckJkH+Yl8ypbzRGRRIZzLgeI4JveSx/mNhewfgTr+ORPThZ +mBdkD5r+ixWF174naw53L8U9wF8kiK7pIE1N9TR4USEeovLwX6Ni/2MMDZedOfof +2f77eUdLsK19/5/lcgAAYaXauXWhy2d2r3SayFrC9woy0lh2VLKRMBjcx1oWb7dp +0uxzo5Y= +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 aes-128-cbc)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-128-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIP5QK2RfqUl4CAggA +MB0GCWCGSAFlAwQBAgQQj3OyM9gnW2dd/eRHkxjGrgSCAoCpM5GZB0v27cxzZsGc +O4/xqgwB0c/bSJ6QogtYU2KVoc7ZNQ5q9jtzn3I4ONvneOkpm9arzYz0FWnJi2C3 +BPiF0D1NkfvjvMLv56bwiG2A1oBECacyAb2pXYeJY7SdtYKvcbgs3jx65uCm6TF2 +BylteH+n1ewTQN9DLfASp1n81Ajq9lQGaK03SN2MUtcAPp7N9gnxJrlmDGeqlPRs +KpQYRcot+kE6Ew8a5jAr7mAxwpqvr3SM4dMvADZmRQsM4Uc/9+YMUdI52DG87EWc +0OUB+fnQ8jw4DZgOE9KKM5/QTWc3aEw/dzXr/YJsrv01oLazhqVHnEMG0Nfr0+DP +q+qac1AsCsOb71VxaRlRZcVEkEfAq3gidSPD93qmlDrCnmLYTilcLanXUepda7ez +qhjkHtpwBLN5xRZxOn3oUuLGjk8VRwfmFX+RIMYCyihjdmbEDYpNUVkQVYFGi/F/ +1hxOyl9yhGdL0hb9pKHH10GGIgoqo4jSTLlb4ennihGMHCjehAjLdx/GKJkOWShy +V9hj8rAuYnRNb+tUW7ChXm1nLq14x9x1tX0ciVVn3ap/NoMkbFTr8M3pJ4bQlpAn +wCT2erYqwQtgSpOJcrFeph9TjIrNRVE7Zlmr7vayJrB/8/oPssVdhf82TXkna4fB +PcmO0YWLa117rfdeNM/Duy0ThSdTl39Qd+4FxqRZiHjbt+l0iSa/nOjTv1TZ/QqF +wqrO6EtcM45fbFJ1Y79o2ptC2D6MB4HKJq9WCt064/8zQCVx3XPbb3X8Z5o/6koy +ePGbz+UtSb9xczvqpRCOiFLh2MG1dUgWuHazjOtUcVWvilKnkjCMzZ9s1qG0sUDj +nPyn +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 aes-256-cbc)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 aes-256-cbc -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIhuL894loRucCAggA +MB0GCWCGSAFlAwQBKgQQEoEtsjW3iC9/u0uGvkxX7wSCAoAsX3l6JoR2OGbT8CkY +YT3RQFqquOgItYOHw6E3tir2YrmxEAo99nxoL8pdto37KSC32eAGnfv5R1zmHHSx +0M3/y2AWiCBTX95EEzdtGC1hK3PBa/qpp/xEmcrsjYN6NXxMAkhC0hMP/HdvqMAg +ee7upvaYJsJcl8QLFNayAWr8b8cZA/RBhGEIRl59Eyj6nNtxDt3bCrfe06o1CPCV +50/fRZEwFOi/C6GYvPN6MrPZO3ALBWgopLT2yQqycTKtfxYWIdOsMBkAjKf2D6Pk +u2mqBsaP4b71jIIeT4euSJLsoJV+O39s8YHXtW8GtOqp7V5kIlnm90lZ9wzeLTZ7 +HJsD/jEdYto5J3YWm2wwEDccraffJSm7UDtJBvQdIx832kxeFCcGQjW38Zl1qqkg +iTH1PLTypxj2ZuviS2EkXVFb/kVU6leWwOt6fqWFC58UvJKeCk/6veazz3PDnTWM +92ClUqFd+CZn9VT4CIaJaAc6v5NLpPp+T9sRX9AtequPm7FyTeevY9bElfyk9gW9 +JDKgKxs6DGWDa16RL5vzwtU+G3o6w6IU+mEwa6/c+hN+pRFs/KBNLLSP9OHBx7BJ +X/32Ft+VFhJaK+lQ+f+hve7od/bgKnz4c/Vtp7Dh51DgWgCpBgb8p0vqu02vTnxD +BXtDv3h75l5PhvdWfVIzpMWRYFvPR+vJi066FjAz2sjYc0NMLSYtZWyWoIInjhoX +Dp5CQujCtw/ZSSlwde1DKEWAW4SeDZAOQNvuz0rU3eosNUJxEmh3aSrcrRtDpw+Y +mBUuWAZMpz7njBi7h+JDfmSW/GAaMwrVFC2gef5375R0TejAh+COAjItyoeYEvv8 +DQd8 +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 des)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 des -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICwzA9BgkqhkiG9w0BBQ0wMDAbBgkqhkiG9w0BBQwwDgQI0lXp62ozXvwCAggA +MBEGBSsOAwIHBAiR3Id5vH0u4wSCAoDQQYOrrkPFPIa0S5fQGXnJw1F/66g92Gs1 +TkGydn4ouabWb++Vbi2chee1oyZsN2l8YNzDi0Gb2PfjsGpg2aJk0a3/efgA0u6T +leEH1dA/7Hr9NVspgHkaXpHt3X6wdbznLYJeAelfj7sDXpOkULGWCkCst0Txb6bi +Oxv4c0yYykiuUrp+2xvHbF9c2PrcDb58u/OBZcCg3QB1gTugQKM+ZIBRhcTEFLrm +8gWbzBfwYiUm6aJce4zoafP0NSlEOBbpbr73A08Q1IK6pISwltOUhhTvspSZnK41 +y2CHt5Drnpl1pfOw9Q0svO3VrUP+omxP1SFP17ZfaRGw2uHd08HJZs438x5dIQoH +QgjlZ8A5rcT3FjnytSh3fln2ZxAGuObghuzmOEL/+8fkGER9QVjmQlsL6OMfB4j4 +ZAkLf74uaTdegF3SqDQaGUwWgk7LyualmUXWTBoeP9kRIsRQLGzAEmd6duBPypED +HhKXP/ZFA1kVp3x1fzJ2llMFB3m1JBwy4PiohqrIJoR+YvKUvzVQtbOjxtCEAj87 +JFnlQj0wjTd6lfNn+okewMNjKINZx+08ui7XANNU/l18lHIIz3ssXJSmqMW+hRZ9 +9oB2tntLrnRMhkVZDVHadq7eMFOPu0rkekuaZm9CO2vu4V7Qa2h+gOoeczYza0H7 +A+qCKbprxyL8SKI5vug2hE+mfC1leXVRtUYm1DnE+oet99bFd0fN20NwTw0rOeRg +0Z+/ZpQNizrXxfd3sU7zaJypWCxZ6TD/U/AKBtcb2gqmUjObZhbfbWq6jU2Ye//w +EBqQkwAUXR1tNekF8CWLOrfC/wbLRxVRkayb8bQUfdgukLpz0bgw +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + + it('can read a private encrypted key (v2 des3)', async () => { + /* + * Generated with + * openssl genpkey -algorithm RSA + * -pkeyopt rsa_keygen_bits:1024 + * -pkeyopt rsa_keygen_pubexp:65537 + * -out foo.pem + * openssl pkcs8 -in foo.pem -topk8 -v2 des3 -passout pass:mypassword + */ + const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISznrfHd+D58CAggA +MBQGCCqGSIb3DQMHBAhx0DnnUvDiHASCAoCceplm+Cmwlgvn4hNsv6e4c/S1iA7w +2hU7Jt8JgRCIMWjP2FthXOAFLa2fD4g3qncYXcDAFBXNyoh25OgOwstO14YkxhDi +wG4TeppGUt9IlyyCol6Z4WhQs1TGm5OcD5xDta+zBXsBnlgmKLD5ZXPEYB+3v/Dg +SvM4sQz6NgkVHN52hchERsnknwSOghiK9mIBH0RZU5LgzlDy2VoBCiEPVdZ7m4F2 +dft5e82zFS58vwDeNN/0r7fC54TyJf/8k3q94+4Hp0mseZ67LR39cvnEKuDuFROm +kLPLekWt5R2NGdunSQlA79BkrNB1ADruO8hQOOHMO9Y3/gNPWLKk+qrfHcUni+w3 +Ofq+rdfakHRb8D6PUmsp3wQj6fSOwOyq3S50VwP4P02gKcZ1om1RvEzTbVMyL3sh +hZcVB3vViu3DO2/56wo29lPVTpj9bSYjw/CO5jNpPBab0B/Gv7JAR0z4Q8gn6OPy +qf+ddyW4Kcb6QUtMrYepghDthOiS3YJV/zCNdL3gTtVs5Ku9QwQ8FeM0/5oJZPlC +TxGuOFEJnYRWqIdByCP8mp/qXS5alSR4uoYQSd7vZG4vkhkPNSAwux/qK1IWfqiW +3XlZzrbD//9IzFVqGRs4nRIFq85ULK0zAR57HEKIwGyn2brEJzrxpV6xsHBp+m4w +6r0+PtwuWA0NauTCUzJ1biUdH8t0TgBL6YLaMjlrfU7JstH3TpcZzhJzsjfy0+zV +NT2TO3kSzXpQ5M2VjOoHPm2fqxD/js+ThDB3QLi4+C7HqakfiTY1lYzXl9/vayt6 +DUD29r9pYL9ErB9tYko2rat54EY7k7Ts6S5jf+8G7Zz234We1APhvqaG +-----END ENCRYPTED PRIVATE KEY----- +` + const key = await crypto.keys.importKey(pem, 'mypassword') + expect(key).to.exist() + }) + }) +}) diff --git a/packages/crypto/test/keys/secp256k1.spec.ts b/packages/crypto/test/keys/secp256k1.spec.ts new file mode 100644 index 0000000000..02e6ee77ad --- /dev/null +++ b/packages/crypto/test/keys/secp256k1.spec.ts @@ -0,0 +1,216 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as crypto from '../../src/index.js' +import * as Secp256k1 from '../../src/keys/secp256k1-class.js' +import * as secp256k1Crypto from '../../src/keys/secp256k1.js' +import fixtures from '../fixtures/go-key-secp256k1.js' + +const secp256k1 = crypto.keys.supportedKeys.secp256k1 +const keysPBM = crypto.keys.keysPBM +const randomBytes = crypto.randomBytes + +describe('secp256k1 keys', () => { + let key: Secp256k1.Secp256k1PrivateKey + + before(async () => { + key = await secp256k1.generateKeyPair() + }) + + it('generates a valid key', async () => { + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.public).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + + const digest = await key.hash() + expect(digest).to.have.length(34) + + const publicDigest = await key.public.hash() + expect(publicDigest).to.have.length(34) + }) + + it('optionally accepts a `bits` argument when generating a key', async () => { + const _key = await secp256k1.generateKeyPair() + expect(_key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + }) + + it('signs', async () => { + const text = randomBytes(512) + const sig = await key.sign(text) + const res = await key.public.verify(text, sig) + expect(res).to.equal(true) + }) + + it('encoding', () => { + const keyMarshal = key.marshal() + const key2 = secp256k1.unmarshalSecp256k1PrivateKey(keyMarshal) + const keyMarshal2 = key2.marshal() + + expect(keyMarshal).to.eql(keyMarshal2) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = secp256k1.unmarshalSecp256k1PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect(pkMarshal).to.eql(pkMarshal2) + }) + + it('key id', async () => { + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + const id = await key.id() + expect(id).to.eql('QmPCyMBGEyifPtx5aa6k6wkY9N1eBf9vHK1eKfNc35q9uq') + }) + + it('should export a password encrypted libp2p-key', async () => { + const key = await crypto.keys.generateKeyPair('secp256k1') + + if (!(key instanceof Secp256k1.Secp256k1PrivateKey)) { + throw new Error('Generated wrong key type') + } + + const encryptedKey = await key.export('my secret') + // Import the key + const importedKey = await crypto.keys.importKey(encryptedKey, 'my secret') + + if (!(importedKey instanceof Secp256k1.Secp256k1PrivateKey)) { + throw new Error('Imported wrong key type') + } + + expect(key.equals(importedKey)).to.equal(true) + }) + + it('should fail to import libp2p-key with wrong password', async () => { + const key = await crypto.keys.generateKeyPair('secp256k1') + const encryptedKey = await key.export('my secret', 'libp2p-key') + + await expect(crypto.keys.importKey(encryptedKey, 'not my secret')).to.eventually.be.rejected() + }) + + describe('key equals', () => { + it('equals itself', () => { + expect(key.equals(key)).to.eql(true) + + expect(key.public.equals(key.public)).to.eql(true) + }) + + it('not equals other key', async () => { + const key2 = await secp256k1.generateKeyPair() + expect(key.equals(key2)).to.eql(false) + expect(key2.equals(key)).to.eql(false) + expect(key.public.equals(key2.public)).to.eql(false) + expect(key2.public.equals(key.public)).to.eql(false) + }) + }) + + it('sign and verify', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(data, sig) + expect(valid).to.eql(true) + }) + + it('fails to verify for different data', async () => { + const data = uint8ArrayFromString('hello world') + const sig = await key.sign(data) + const valid = await key.public.verify(uint8ArrayFromString('hello'), sig) + expect(valid).to.eql(false) + }) +}) + +describe('crypto functions', () => { + let privKey: Uint8Array + let pubKey: Uint8Array + + before(() => { + privKey = secp256k1Crypto.generateKey() + pubKey = secp256k1Crypto.computePublicKey(privKey) + }) + + it('generates valid keys', () => { + expect(() => { + secp256k1Crypto.validatePrivateKey(privKey) + secp256k1Crypto.validatePublicKey(pubKey) + }).to.not.throw() + }) + + it('does not validate an invalid key', () => { + expect(() => { secp256k1Crypto.validatePublicKey(uint8ArrayFromString('42')) }).to.throw() + expect(() => { secp256k1Crypto.validatePrivateKey(uint8ArrayFromString('42')) }).to.throw() + }) + + it('validates a correct signature', async () => { + const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) + const valid = await secp256k1Crypto.hashAndVerify(pubKey, sig, uint8ArrayFromString('hello')) + expect(valid).to.equal(true) + }) + + it('does not validate when validating a message with an invalid signature', async () => { + const result = await secp256k1Crypto.hashAndVerify(pubKey, uint8ArrayFromString('invalid-sig'), uint8ArrayFromString('hello')) + + expect(result).to.be.false() + }) + + it('errors if given a null Uint8Array to sign', async () => { + // @ts-expect-error incorrect args + await expect(secp256k1Crypto.hashAndSign(privKey, null)).to.eventually.be.rejected() + }) + + it('errors when signing with an invalid key', async () => { + await expect(secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello'))).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_INPUT') + }) + + it('errors if given a null Uint8Array to validate', async () => { + const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) + + // @ts-expect-error incorrect args + await expect(secp256k1Crypto.hashAndVerify(privKey, sig, null)).to.eventually.be.rejected() + }) + + it('throws when compressing an invalid public key', () => { + expect(() => secp256k1Crypto.compressPublicKey(uint8ArrayFromString('42'))).to.throw() + }) + + it('throws when decompressing an invalid public key', () => { + expect(() => secp256k1Crypto.decompressPublicKey(uint8ArrayFromString('42'))).to.throw() + }) + + it('compresses/decompresses a valid public key', () => { + const decompressed = secp256k1Crypto.decompressPublicKey(pubKey) + expect(decompressed).to.exist() + expect(decompressed.length).to.be.eql(65) + const recompressed = secp256k1Crypto.compressPublicKey(decompressed) + expect(recompressed).to.eql(pubKey) + }) +}) + +describe('go interop', () => { + it('loads a private key marshaled by go-libp2p-crypto', () => { + // we need to first extract the key data from the protobuf, which is + // normally handled by js-libp2p-crypto + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey) + expect(key.bytes).to.eql(fixtures.privateKey) + }) + + it('loads a public key marshaled by go-libp2p-crypto', () => { + const decoded = keysPBM.PublicKey.decode(fixtures.publicKey) + expect(decoded.Type).to.be.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data ?? new Uint8Array()) + expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey) + expect(key.bytes).to.eql(fixtures.publicKey) + }) + + it('generates the same signature as go-libp2p-crypto', async () => { + const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey) + expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1) + + const key = secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data ?? new Uint8Array()) + const sig = await key.sign(fixtures.message) + expect(sig).to.eql(fixtures.signature) + }) +}) diff --git a/packages/crypto/test/random-bytes.spec.ts b/packages/crypto/test/random-bytes.spec.ts new file mode 100644 index 0000000000..06fa923b1e --- /dev/null +++ b/packages/crypto/test/random-bytes.spec.ts @@ -0,0 +1,22 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import randomBytes from '../src/random-bytes.js' + +describe('randomBytes', () => { + it('produces random bytes', () => { + expect(randomBytes(16)).to.have.length(16) + }) + + it('throws if length is 0', () => { + expect(() => randomBytes(0)).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) + + it('throws if length is < 0', () => { + expect(() => randomBytes(-1)).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) + + it('throws if length is not a number', () => { + // @ts-expect-error invalid params + expect(() => randomBytes('hi')).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') + }) +}) diff --git a/packages/crypto/test/util.spec.ts b/packages/crypto/test/util.spec.ts new file mode 100644 index 0000000000..84f6af0998 --- /dev/null +++ b/packages/crypto/test/util.spec.ts @@ -0,0 +1,34 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import 'node-forge/lib/jsbn.js' +// @ts-expect-error types are missing +import forge from 'node-forge/lib/forge.js' +import * as util from '../src/util.js' + +describe('Util', () => { + let bn: typeof forge.jsbn.BigInteger + + before(() => { + bn = new forge.jsbn.BigInteger('dead', 16) + }) + + it('should convert BigInteger to a uint base64url encoded string', () => { + expect(util.bigIntegerToUintBase64url(bn)).to.eql('3q0') + }) + + it('should convert BigInteger to a uint base64url encoded string with padding', () => { + const bnpad = new forge.jsbn.BigInteger('ff', 16) + expect(util.bigIntegerToUintBase64url(bnpad, 2)).to.eql('AP8') + }) + + it('should convert base64url encoded string to BigInteger', () => { + const num = util.base64urlToBigInteger('3q0') + expect(num.equals(bn)).to.be.true() + }) + + it('should convert base64url encoded string to Uint8Array with padding', () => { + const buf = util.base64urlToBuffer('AP8', 2) + expect(Uint8Array.from([0, 255])).to.eql(buf) + }) +}) diff --git a/packages/crypto/test/workaround.spec.ts b/packages/crypto/test/workaround.spec.ts new file mode 100644 index 0000000000..6b96c30b9d --- /dev/null +++ b/packages/crypto/test/workaround.spec.ts @@ -0,0 +1,26 @@ + +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { derivedEmptyPasswordKey } from '../src/ciphers/aes-gcm.browser.js' + +describe('Constant derived key is generated correctly', () => { + it('Generates correctly', async () => { + if ((typeof navigator !== 'undefined' && navigator.userAgent.includes('Safari')) || typeof crypto === 'undefined') { + // WebKit Linux can't generate this. Hence the workaround. + return + } + + const generatedKey = await crypto.subtle.exportKey('jwk', + await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } }, + await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']), + { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']) + ) + + // Webkit macos flips these. Sort them so they match. + derivedEmptyPasswordKey.key_ops.sort() + generatedKey?.key_ops?.sort() + + expect(generatedKey).to.eql(derivedEmptyPasswordKey) + }) +}) diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json new file mode 100644 index 0000000000..0051483a8d --- /dev/null +++ b/packages/crypto/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-keys" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/interface-address-manager/package.json b/packages/interface-address-manager/package.json index 9dacdafde4..7452506c40 100644 --- a/packages/interface-address-manager/package.json +++ b/packages/interface-address-manager/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,10 +42,10 @@ "build": "aegir build" }, "dependencies": { - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-compliance-tests/package.json b/packages/interface-compliance-tests/package.json index 3513dc667d..22123fb6ae 100644 --- a/packages/interface-compliance-tests/package.json +++ b/packages/interface-compliance-tests/package.json @@ -59,91 +59,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-connection-compliance-tests/package.json b/packages/interface-connection-compliance-tests/package.json index ebc9e3d97c..4630b98670 100644 --- a/packages/interface-connection-compliance-tests/package.json +++ b/packages/interface-connection-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-connection-encrypter-compliance-tests/package.json b/packages/interface-connection-encrypter-compliance-tests/package.json index c628c41665..2994e30dab 100644 --- a/packages/interface-connection-encrypter-compliance-tests/package.json +++ b/packages/interface-connection-encrypter-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -132,13 +47,13 @@ "@libp2p/interface-connection-encrypter": "^4.0.0", "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-pair": "^2.0.2", "it-pipe": "^3.0.1", "it-stream-types": "^2.0.1", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-encrypter-compliance-tests/tsconfig.json b/packages/interface-connection-encrypter-compliance-tests/tsconfig.json index 77fd5244fb..e00ae1221b 100644 --- a/packages/interface-connection-encrypter-compliance-tests/tsconfig.json +++ b/packages/interface-connection-encrypter-compliance-tests/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../interface-peer-id" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-connection-encrypter/package.json b/packages/interface-connection-encrypter/package.json index 5d511231e1..812ec5e4ed 100644 --- a/packages/interface-connection-encrypter/package.json +++ b/packages/interface-connection-encrypter/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -151,7 +66,7 @@ "it-stream-types": "^2.0.1" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-gater/package.json b/packages/interface-connection-gater/package.json index a048a32700..28ea0b4003 100644 --- a/packages/interface-connection-gater/package.json +++ b/packages/interface-connection-gater/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -129,10 +44,10 @@ "dependencies": { "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-manager/package.json b/packages/interface-connection-manager/package.json index 1d405ebb65..1f9076c2f4 100644 --- a/packages/interface-connection-manager/package.json +++ b/packages/interface-connection-manager/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,11 +45,11 @@ "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/peer-collections": "^3.0.1", - "@multiformats/multiaddr": "^12.0.0" + "@libp2p/peer-collections": "^3.0.0", + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-connection-manager/tsconfig.json b/packages/interface-connection-manager/tsconfig.json index cbde3d05cb..3e21f519cc 100644 --- a/packages/interface-connection-manager/tsconfig.json +++ b/packages/interface-connection-manager/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../peer-collections" } ] } diff --git a/packages/interface-connection/package.json b/packages/interface-connection/package.json index 8fbfb60ba0..9bde521b06 100644 --- a/packages/interface-connection/package.json +++ b/packages/interface-connection/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -149,12 +64,12 @@ "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "it-stream-types": "^2.0.1", "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-content-routing/package.json b/packages/interface-content-routing/package.json index d73315f4de..e63a95f976 100644 --- a/packages/interface-content-routing/package.json +++ b/packages/interface-content-routing/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -129,10 +44,10 @@ "dependencies": { "@libp2p/interface-peer-info": "^1.0.0", "@libp2p/interfaces": "^3.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-dht/package.json b/packages/interface-dht/package.json index 69d381003e..46ac76cde9 100644 --- a/packages/interface-dht/package.json +++ b/packages/interface-dht/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,10 +46,10 @@ "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/interface-peer-info": "^1.0.0", "@libp2p/interfaces": "^3.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-keychain/package.json b/packages/interface-keychain/package.json index 1d786072db..8facba1250 100644 --- a/packages/interface-keychain/package.json +++ b/packages/interface-keychain/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -128,10 +43,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-keys/package.json b/packages/interface-keys/package.json index ff87fd33b5..8cc349bfe5 100644 --- a/packages/interface-keys/package.json +++ b/packages/interface-keys/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,7 +42,7 @@ "build": "aegir build" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-libp2p/package.json b/packages/interface-libp2p/package.json index 0057e4bd6b..1a97324de2 100644 --- a/packages/interface-libp2p/package.json +++ b/packages/interface-libp2p/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -138,10 +53,10 @@ "@libp2p/interface-registrar": "^2.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-metrics/package.json b/packages/interface-metrics/package.json index 5e8774cacb..4d253595bd 100644 --- a/packages/interface-metrics/package.json +++ b/packages/interface-metrics/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,7 +45,7 @@ "@libp2p/interface-connection": "^5.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-mocks/package.json b/packages/interface-mocks/package.json index 0e2f4c096b..5a19d0d066 100644 --- a/packages/interface-mocks/package.json +++ b/packages/interface-mocks/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -148,12 +63,12 @@ "@libp2p/interface-stream-muxer": "^4.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/logger": "^2.1.1", - "@libp2p/multistream-select": "^3.1.8", - "@libp2p/peer-collections": "^3.0.1", + "@libp2p/logger": "^2.0.0", + "@libp2p/multistream-select": "^3.0.0", + "@libp2p/peer-collections": "^3.0.0", "@libp2p/peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", "it-handshake": "^4.1.3", @@ -165,14 +80,14 @@ "it-stream-types": "^2.0.1", "merge-options": "^3.0.4", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "devDependencies": { "@libp2p/interface-connection-compliance-tests": "^2.0.0", "@libp2p/interface-connection-encrypter-compliance-tests": "^5.0.0", "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", "@libp2p/interface-stream-muxer-compliance-tests": "^7.0.0", - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-mocks/tsconfig.json b/packages/interface-mocks/tsconfig.json index d49aa5945a..19360077a1 100644 --- a/packages/interface-mocks/tsconfig.json +++ b/packages/interface-mocks/tsconfig.json @@ -61,6 +61,21 @@ }, { "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../multistream-select" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-peer-discovery-compliance-tests/package.json b/packages/interface-peer-discovery-compliance-tests/package.json index ceb756a488..5c541d6e09 100644 --- a/packages/interface-peer-discovery-compliance-tests/package.json +++ b/packages/interface-peer-discovery-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,7 +45,7 @@ "@libp2p/interface-compliance-tests": "^3.0.0", "@libp2p/interface-peer-discovery": "^2.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", "delay": "^6.0.0", "p-defer": "^4.0.0" diff --git a/packages/interface-peer-discovery/package.json b/packages/interface-peer-discovery/package.json index ca70962919..28f2e12c49 100644 --- a/packages/interface-peer-discovery/package.json +++ b/packages/interface-peer-discovery/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "@libp2p/interfaces": "^3.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-id/package.json b/packages/interface-peer-id/package.json index bb29939d29..748a8c67c1 100644 --- a/packages/interface-peer-id/package.json +++ b/packages/interface-peer-id/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -127,10 +42,10 @@ "build": "aegir build" }, "dependencies": { - "multiformats": "^11.0.0" + "multiformats": "^11.0.2" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-info/package.json b/packages/interface-peer-info/package.json index be47c49d99..fcdd888d93 100644 --- a/packages/interface-peer-info/package.json +++ b/packages/interface-peer-info/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -128,10 +43,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-routing/package.json b/packages/interface-peer-routing/package.json index cebeb953eb..5bb055b234 100644 --- a/packages/interface-peer-routing/package.json +++ b/packages/interface-peer-routing/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -132,7 +47,7 @@ "@libp2p/interfaces": "^3.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-peer-store/package.json b/packages/interface-peer-store/package.json index 99c0d436c6..d548cfffee 100644 --- a/packages/interface-peer-store/package.json +++ b/packages/interface-peer-store/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -148,10 +63,10 @@ }, "dependencies": { "@libp2p/interface-peer-id": "^2.0.0", - "@multiformats/multiaddr": "^12.0.0" + "@multiformats/multiaddr": "^12.1.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-pubsub-compliance-tests/package.json b/packages/interface-pubsub-compliance-tests/package.json index bf29959591..5fb951a1a3 100644 --- a/packages/interface-pubsub-compliance-tests/package.json +++ b/packages/interface-pubsub-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -138,10 +53,10 @@ "aegir": "^39.0.5", "delay": "^6.0.0", "p-defer": "^4.0.0", - "p-event": "^5.0.1", + "p-event": "^6.0.0", "p-wait-for": "^5.0.0", "sinon": "^15.0.0", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-pubsub-compliance-tests/tsconfig.json b/packages/interface-pubsub-compliance-tests/tsconfig.json index e128c8d1e9..1a1ed83a7b 100644 --- a/packages/interface-pubsub-compliance-tests/tsconfig.json +++ b/packages/interface-pubsub-compliance-tests/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../peer-id-factory" } ] } diff --git a/packages/interface-pubsub/package.json b/packages/interface-pubsub/package.json index 2e16c712cc..264136f0e5 100644 --- a/packages/interface-pubsub/package.json +++ b/packages/interface-pubsub/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -134,7 +49,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-record-compliance-tests/package.json b/packages/interface-record-compliance-tests/package.json index 83d8263247..0e73648f1b 100644 --- a/packages/interface-record-compliance-tests/package.json +++ b/packages/interface-record-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", diff --git a/packages/interface-record/package.json b/packages/interface-record/package.json index 9a31a1c865..7df588efd1 100644 --- a/packages/interface-record/package.json +++ b/packages/interface-record/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-registrar/package.json b/packages/interface-registrar/package.json index 35b81c65c5..eda94ddd25 100644 --- a/packages/interface-registrar/package.json +++ b/packages/interface-registrar/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -131,7 +46,7 @@ "@libp2p/interface-peer-id": "^2.0.0" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer-compliance-tests/package.json b/packages/interface-stream-muxer-compliance-tests/package.json index 7a2c836cb4..ed4b33eac7 100644 --- a/packages/interface-stream-muxer-compliance-tests/package.json +++ b/packages/interface-stream-muxer-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -133,7 +48,7 @@ "abortable-iterator": "^5.0.1", "aegir": "^39.0.5", "delay": "^6.0.0", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-map": "^3.0.2", "it-pair": "^2.0.2", @@ -142,7 +57,7 @@ "p-defer": "^4.0.0", "p-limit": "^4.0.0", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer/package.json b/packages/interface-stream-muxer/package.json index 1f26ce0ba1..e238d958c2 100644 --- a/packages/interface-stream-muxer/package.json +++ b/packages/interface-stream-muxer/package.json @@ -55,91 +55,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -149,7 +64,7 @@ "dependencies": { "@libp2p/interface-connection": "^5.0.0", "@libp2p/interfaces": "^3.0.0", - "@libp2p/logger": "^2.1.1", + "@libp2p/logger": "^2.0.0", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", "it-pushable": "^3.1.3", @@ -157,7 +72,7 @@ "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-stream-muxer/tsconfig.json b/packages/interface-stream-muxer/tsconfig.json index 181d553f61..30fd079a69 100644 --- a/packages/interface-stream-muxer/tsconfig.json +++ b/packages/interface-stream-muxer/tsconfig.json @@ -13,6 +13,9 @@ }, { "path": "../interfaces" + }, + { + "path": "../logger" } ] } diff --git a/packages/interface-transport-compliance-tests/package.json b/packages/interface-transport-compliance-tests/package.json index 20d73a23af..7e25d123d8 100644 --- a/packages/interface-transport-compliance-tests/package.json +++ b/packages/interface-transport-compliance-tests/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -133,15 +48,15 @@ "@libp2p/interface-registrar": "^2.0.0", "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "aegir": "^39.0.5", - "it-all": "^3.0.1", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-pipe": "^3.0.1", "p-defer": "^4.0.0", "p-wait-for": "^5.0.0", "sinon": "^15.0.0", - "uint8arrays": "^4.0.2" + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interface-transport/package.json b/packages/interface-transport/package.json index 5195f574c6..96e5e13b0f 100644 --- a/packages/interface-transport/package.json +++ b/packages/interface-transport/package.json @@ -35,91 +35,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -130,11 +45,11 @@ "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-stream-muxer": "^4.0.0", "@libp2p/interfaces": "^3.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@multiformats/multiaddr": "^12.1.3", "it-stream-types": "^2.0.1" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index 7f5c8e2f83..6f01645347 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -63,91 +63,6 @@ "sourceType": "module" } }, - "release": { - "branches": [ - "master" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, "scripts": { "clean": "aegir clean", "lint": "aegir lint", @@ -155,7 +70,7 @@ "build": "aegir build" }, "devDependencies": { - "aegir": "^39.0.5" + "aegir": "^39.0.10" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/kad-dht/.aegir.js b/packages/kad-dht/.aegir.js new file mode 100644 index 0000000000..d0f4b4703f --- /dev/null +++ b/packages/kad-dht/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + bundlesizeMax: '160KB' + } +} diff --git a/packages/kad-dht/CHANGELOG.md b/packages/kad-dht/CHANGELOG.md new file mode 100644 index 0000000000..04d3b10bc3 --- /dev/null +++ b/packages/kad-dht/CHANGELOG.md @@ -0,0 +1,1654 @@ +## [9.3.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.5...v9.3.6) (2023-06-01) + + +### Bug Fixes + +* skip self-query if not running ([#479](https://github.com/libp2p/js-libp2p-kad-dht/issues/479)) ([7095290](https://github.com/libp2p/js-libp2p-kad-dht/commit/70952907a27fd8778773172059879656b4f08855)) + +## [9.3.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.4...v9.3.5) (2023-05-26) + + +### Bug Fixes + +* use events to delay before self-query ([#478](https://github.com/libp2p/js-libp2p-kad-dht/issues/478)) ([46313a8](https://github.com/libp2p/js-libp2p-kad-dht/commit/46313a876783da7c036f79daa69a558bd8f1a245)) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#476](https://github.com/libp2p/js-libp2p-kad-dht/issues/476)) ([50524f9](https://github.com/libp2p/js-libp2p-kad-dht/commit/50524f9d74a819fc1fbc2d6faff28a59e7d9b4aa)) + +## [9.3.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.3...v9.3.4) (2023-05-22) + + +### Bug Fixes + +* add events dep ([#477](https://github.com/libp2p/js-libp2p-kad-dht/issues/477)) ([3744a20](https://github.com/libp2p/js-libp2p-kad-dht/commit/3744a209e6bf8eaa75365056286f2f734c9ad1bf)) + +## [9.3.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.2...v9.3.3) (2023-05-09) + + +### Bug Fixes + +* work in browsers without extra deps ([#474](https://github.com/libp2p/js-libp2p-kad-dht/issues/474)) ([0c8d464](https://github.com/libp2p/js-libp2p-kad-dht/commit/0c8d464e91f63a799debd29c54dd358b38e11ea9)) + +## [9.3.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.1...v9.3.2) (2023-05-09) + + +### Bug Fixes + +* only start self-query if node is not stopped ([843fe61](https://github.com/libp2p/js-libp2p-kad-dht/commit/843fe6171b7bc8a61e0b5de18a7ccced5398307a)) + +## [9.3.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.3.0...v9.3.1) (2023-05-09) + + +### Bug Fixes + +* if client mode is specifed, do not auto-switch to server mode ([#475](https://github.com/libp2p/js-libp2p-kad-dht/issues/475)) ([abe6a25](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe6a25c09b8e619661c427e5c7b7a4c45fa3d92)) + +## [9.3.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.2.0...v9.3.0) (2023-05-09) + + +### Features + +* switch to server mode automatically when addresses change ([#473](https://github.com/libp2p/js-libp2p-kad-dht/issues/473)) ([de51cbe](https://github.com/libp2p/js-libp2p-kad-dht/commit/de51cbe0c3f5e6b17c45d1297c359cdafd83b0a2)) + +## [9.2.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.5...v9.2.0) (2023-05-05) + + +### Features + +* invoke onProgress callback if passed as an option ([#472](https://github.com/libp2p/js-libp2p-kad-dht/issues/472)) ([0bef25f](https://github.com/libp2p/js-libp2p-kad-dht/commit/0bef25ff823581cf3462be32b23e737abd2fca3a)) + +## [9.1.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.4...v9.1.5) (2023-05-05) + + +### Bug Fixes + +* log peer id not whole object ([#470](https://github.com/libp2p/js-libp2p-kad-dht/issues/470)) ([e9efb7f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e9efb7f7e5141bd40dc422cd45c964f8cc404764)) +* only choose query peers after initial self-query has run ([#471](https://github.com/libp2p/js-libp2p-kad-dht/issues/471)) ([4d05154](https://github.com/libp2p/js-libp2p-kad-dht/commit/4d0515497511e283c5d5b5a4d723c4ea783eb2a8)) + +## [9.1.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.3...v9.1.4) (2023-05-04) + + +### Dependencies + +* update @libp2p/interface-peer-discovery to 2.0.0 ([#469](https://github.com/libp2p/js-libp2p-kad-dht/issues/469)) ([2c0fc68](https://github.com/libp2p/js-libp2p-kad-dht/commit/2c0fc6865c4e6ddc5b598b1693cf8fb408a31ecf)) + +## [9.1.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.2...v9.1.3) (2023-05-04) + + +### Dependencies + +* **dev:** bump @libp2p/interface-libp2p from 2.0.0 to 3.0.0 ([#466](https://github.com/libp2p/js-libp2p-kad-dht/issues/466)) ([d8f8e5a](https://github.com/libp2p/js-libp2p-kad-dht/commit/d8f8e5a3b2d5c57fc89129c2cf8ed2ed7c52d171)) + +## [9.1.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.1...v9.1.2) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#468](https://github.com/libp2p/js-libp2p-kad-dht/issues/468)) ([dc53728](https://github.com/libp2p/js-libp2p-kad-dht/commit/dc53728db6772323bbac6e1e876e3d999f1d8fd0)) + +## [9.1.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.1.0...v9.1.1) (2023-05-04) + + +### Bug Fixes + +* wait for self-query to have run before running queries ([#457](https://github.com/libp2p/js-libp2p-kad-dht/issues/457)) ([9d5bdb9](https://github.com/libp2p/js-libp2p-kad-dht/commit/9d5bdb98e8ed9183064b895f9fc858bbde5f1127)) + +## [9.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v9.0.0...v9.1.0) (2023-04-27) + + +### Features + +* use symbols to return routers ([#464](https://github.com/libp2p/js-libp2p-kad-dht/issues/464)) ([d681aaf](https://github.com/libp2p/js-libp2p-kad-dht/commit/d681aaf49425e6e8638dc31eba5f158085cd4485)) + +## [9.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.12...v9.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 (#460) + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#460](https://github.com/libp2p/js-libp2p-kad-dht/issues/460)) ([e3f15f1](https://github.com/libp2p/js-libp2p-kad-dht/commit/e3f15f17a65d98940fcba4ba7a46fd17ab509785)) + +## [8.0.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.11...v8.0.12) (2023-04-19) + + +### Tests + +* enable skipped xor test ([#458](https://github.com/libp2p/js-libp2p-kad-dht/issues/458)) ([4acfd70](https://github.com/libp2p/js-libp2p-kad-dht/commit/4acfd7047154276ae70a2c1f07775abcacd82170)) + +## [8.0.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.10...v8.0.11) (2023-04-19) + + +### Dependencies + +* update it-stream-types ([#456](https://github.com/libp2p/js-libp2p-kad-dht/issues/456)) ([2dfccee](https://github.com/libp2p/js-libp2p-kad-dht/commit/2dfccee3614bbcde1bf0ff9b1c8dbded79f86d91)) + +## [8.0.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.9...v8.0.10) (2023-04-17) + + +### Bug Fixes + +* remove timeout-abort-controller dependency ([#454](https://github.com/libp2p/js-libp2p-kad-dht/issues/454)) ([7f3245e](https://github.com/libp2p/js-libp2p-kad-dht/commit/7f3245e5b9379c7fcf0eb3f7cccb60d2b81aaadb)) + +## [8.0.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.8...v8.0.9) (2023-04-14) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#452](https://github.com/libp2p/js-libp2p-kad-dht/issues/452)) ([75b1b50](https://github.com/libp2p/js-libp2p-kad-dht/commit/75b1b504ce529dfa447113092fd600041fb112de)) + +## [8.0.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.7...v8.0.8) (2023-04-14) + + +### Dependencies + +* bump @libp2p/interface-connection-manager from 1.5.0 to 2.0.0 ([#455](https://github.com/libp2p/js-libp2p-kad-dht/issues/455)) ([e4ed616](https://github.com/libp2p/js-libp2p-kad-dht/commit/e4ed6168add1653400853c5c2bc416790b0699a2)) + +## [8.0.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.6...v8.0.7) (2023-04-13) + + +### Dependencies + +* update any-signal to 4.x.x ([#453](https://github.com/libp2p/js-libp2p-kad-dht/issues/453)) ([852d757](https://github.com/libp2p/js-libp2p-kad-dht/commit/852d757cc09bfa468b7c405c3a1506d3e3cfb90b)) + +## [8.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.5...v8.0.6) (2023-04-03) + + +### Dependencies + +* update `it-*` deps to latest versions ([#450](https://github.com/libp2p/js-libp2p-kad-dht/issues/450)) ([0d07558](https://github.com/libp2p/js-libp2p-kad-dht/commit/0d07558c94728f2a0323ccd6d86b3816f4562966)) + +## [8.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.4...v8.0.5) (2023-03-31) + + +### Dependencies + +* bump it-map from 2.0.1 to 3.0.1 ([#440](https://github.com/libp2p/js-libp2p-kad-dht/issues/440)) ([8e02b3d](https://github.com/libp2p/js-libp2p-kad-dht/commit/8e02b3d6db1f323e103fbcdff59fabcc8f4d67c6)) +* bump it-take from 2.0.1 to 3.0.1 ([#439](https://github.com/libp2p/js-libp2p-kad-dht/issues/439)) ([f85e2f9](https://github.com/libp2p/js-libp2p-kad-dht/commit/f85e2f9576095035117ddb3f5794bf9aef1e8453)) +* **dev:** bump it-last from 2.0.1 to 3.0.1 ([#438](https://github.com/libp2p/js-libp2p-kad-dht/issues/438)) ([23cc94f](https://github.com/libp2p/js-libp2p-kad-dht/commit/23cc94f224dbb01a3f0454c6a8d9f6722a1f40c4)) + +## [8.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.3...v8.0.4) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#436](https://github.com/libp2p/js-libp2p-kad-dht/issues/436)) ([5024646](https://github.com/libp2p/js-libp2p-kad-dht/commit/502464690ab8f5610fea9347fc99f2a41cf62774)) + +## [8.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.2...v8.0.3) (2023-03-29) + + +### Bug Fixes + +* updated for pqueue7 ([#433](https://github.com/libp2p/js-libp2p-kad-dht/issues/433)) ([62205a0](https://github.com/libp2p/js-libp2p-kad-dht/commit/62205a0cce3f40f238116810f75640466f8e3972)) + +## [8.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.1...v8.0.2) (2023-03-24) + + +### Dependencies + +* **dev:** bump @types/which from 2.0.2 to 3.0.0 ([#435](https://github.com/libp2p/js-libp2p-kad-dht/issues/435)) ([f8ad02a](https://github.com/libp2p/js-libp2p-kad-dht/commit/f8ad02a26d80a66f684817e2587e5f705e83de2b)) + +## [8.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v8.0.0...v8.0.1) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#434](https://github.com/libp2p/js-libp2p-kad-dht/issues/434)) ([49dcc65](https://github.com/libp2p/js-libp2p-kad-dht/commit/49dcc65c4b34bc4ddce8f04f6b1a9761f1693311)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.3...v8.0.0) (2023-03-14) + + +### ⚠ BREAKING CHANGES + +* requires an instance of `interface-datastore@8.x.x` + +### Dependencies + +* update interface-datastore to 8.x.x ([#430](https://github.com/libp2p/js-libp2p-kad-dht/issues/430)) ([923ef72](https://github.com/libp2p/js-libp2p-kad-dht/commit/923ef72ff39930fb773f3d619f8d1a23cf15d65d)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.2...v7.0.3) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#416](https://github.com/libp2p/js-libp2p-kad-dht/issues/416)) ([7ebf172](https://github.com/libp2p/js-libp2p-kad-dht/commit/7ebf172b6b454315ff8d0fcd15509b768a5da4ad)) +* **dev:** bump execa from 6.1.0 to 7.0.0 ([#421](https://github.com/libp2p/js-libp2p-kad-dht/issues/421)) ([04124d4](https://github.com/libp2p/js-libp2p-kad-dht/commit/04124d48a065d226a4ec370efec38dfe4f3c5ff4)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.1...v7.0.2) (2023-03-10) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#427](https://github.com/libp2p/js-libp2p-kad-dht/issues/427)) ([bf7d1ba](https://github.com/libp2p/js-libp2p-kad-dht/commit/bf7d1bac21d0346098cc206951602dc39224d43a)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v7.0.0...v7.0.1) (2023-03-10) + + +### Bug Fixes + +* correct `KBucketTree` types ([#426](https://github.com/libp2p/js-libp2p-kad-dht/issues/426)) ([ea8e6d0](https://github.com/libp2p/js-libp2p-kad-dht/commit/ea8e6d0fc7db9192539532c8d74b3e5e053056fd)), closes [/github.com/tristanls/k-bucket/blob/3aa5b4f1dacb835752995a25409ab319d2070b9e/index.js#L413](https://github.com/libp2p//github.com/tristanls/k-bucket/blob/3aa5b4f1dacb835752995a25409ab319d2070b9e/index.js/issues/L413) +* update p-queue types ([#428](https://github.com/libp2p/js-libp2p-kad-dht/issues/428)) ([f5b85fc](https://github.com/libp2p/js-libp2p-kad-dht/commit/f5b85fccfd920984073319f6c62015231611ba26)) + + +### Trivial Changes + +* replace err-code with CodeError ([#413](https://github.com/libp2p/js-libp2p-kad-dht/issues/413)) ([e05d2a0](https://github.com/libp2p/js-libp2p-kad-dht/commit/e05d2a07eee96bfd91bdd01707f5c8112151c377)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a70ab3f](https://github.com/libp2p/js-libp2p-kad-dht/commit/a70ab3f200cb73703b6301b81ba6b922c37b5bc3)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1652c6c](https://github.com/libp2p/js-libp2p-kad-dht/commit/1652c6cccd8dc1381c4b28091f87cadcac9782b5)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([ea13c2a](https://github.com/libp2p/js-libp2p-kad-dht/commit/ea13c2a10c689655656880b584315ebab374871c)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.1.1...v7.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 (#412) + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#412](https://github.com/libp2p/js-libp2p-kad-dht/issues/412)) ([18c8276](https://github.com/libp2p/js-libp2p-kad-dht/commit/18c8276b8638c2e3a3733dae2c973c552f92303c)) + +## [6.1.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.1.0...v6.1.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#411](https://github.com/libp2p/js-libp2p-kad-dht/issues/411)) ([718e01b](https://github.com/libp2p/js-libp2p-kad-dht/commit/718e01b86299e69ac1f2fed12c77b0675aeee9cf)) + +## [6.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.4...v6.1.0) (2022-12-07) + + +### Features + +* allow passing ProvidersInit in KadDHT constructor ([#404](https://github.com/libp2p/js-libp2p-kad-dht/issues/404)) ([e64af85](https://github.com/libp2p/js-libp2p-kad-dht/commit/e64af85d6ef02d99521689ed8b60e0c3702efbc5)) + + +### Bug Fixes + +* treat /dns, /dns4, and /dns6 addrs as public ([#406](https://github.com/libp2p/js-libp2p-kad-dht/issues/406)) ([e27747a](https://github.com/libp2p/js-libp2p-kad-dht/commit/e27747ab9c32b6f72b04bb24cbc51e95384c1747)), closes [#377](https://github.com/libp2p/js-libp2p-kad-dht/issues/377) + +## [6.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.3...v6.0.4) (2022-12-07) + + +### Bug Fixes + +* use multihash bytes for provide message keys ([#405](https://github.com/libp2p/js-libp2p-kad-dht/issues/405)) ([d7e7b5d](https://github.com/libp2p/js-libp2p-kad-dht/commit/d7e7b5d56334ca6e9a305c9473e9d62468933f05)), closes [#381](https://github.com/libp2p/js-libp2p-kad-dht/issues/381) + +## [6.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.2...v6.0.3) (2022-12-07) + + +### Dependencies + +* bump private-ip from 2.3.4 to 3.0.0 ([#400](https://github.com/libp2p/js-libp2p-kad-dht/issues/400)) ([5a567e3](https://github.com/libp2p/js-libp2p-kad-dht/commit/5a567e31e46671c0e5bcb9a7859a2ca82e33e58a)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.1...v6.0.2) (2022-12-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.1 ([#399](https://github.com/libp2p/js-libp2p-kad-dht/issues/399)) ([26cbb88](https://github.com/libp2p/js-libp2p-kad-dht/commit/26cbb88772da0496b46909019ea4b371a7947b51)) +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#403](https://github.com/libp2p/js-libp2p-kad-dht/issues/403)) ([f78a0ea](https://github.com/libp2p/js-libp2p-kad-dht/commit/f78a0ea8d0f387906422797668697fb66d4b3749)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v6.0.0...v6.0.1) (2022-11-07) + + +### Bug Fixes + +* enable and fix browser tests ([#352](https://github.com/libp2p/js-libp2p-kad-dht/issues/352)) ([5244428](https://github.com/libp2p/js-libp2p-kad-dht/commit/5244428d0a6a7c3151f2fcb043400e61a4c78a36)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.2...v6.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires @libp2p/interface-metrics v4 + +### Bug Fixes + +* update to metrics v4 ([#398](https://github.com/libp2p/js-libp2p-kad-dht/issues/398)) ([3182cb0](https://github.com/libp2p/js-libp2p-kad-dht/commit/3182cb0dbcfa8eca0ceb243db53c43eed14c1af8)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.1...v5.0.2) (2022-11-05) + + +### Dependencies + +* bump it-all from 1.0.6 to 2.0.0 ([#389](https://github.com/libp2p/js-libp2p-kad-dht/issues/389)) ([0596485](https://github.com/libp2p/js-libp2p-kad-dht/commit/059648530a991cb04e8f7408791d6facf4b57e41)) +* bump it-drain from 1.0.5 to 2.0.0 ([#390](https://github.com/libp2p/js-libp2p-kad-dht/issues/390)) ([fdda357](https://github.com/libp2p/js-libp2p-kad-dht/commit/fdda357118f7020469aba57122f322e6210ff907)) +* bump it-first from 1.0.7 to 2.0.0 ([#391](https://github.com/libp2p/js-libp2p-kad-dht/issues/391)) ([681c24e](https://github.com/libp2p/js-libp2p-kad-dht/commit/681c24ea1cd7c65f82ca76c00c9c3051432665f8)) +* bump it-length from 1.0.4 to 2.0.0 ([#394](https://github.com/libp2p/js-libp2p-kad-dht/issues/394)) ([ae07736](https://github.com/libp2p/js-libp2p-kad-dht/commit/ae0773619c80c30c2eb556a8802833c1051c3cb0)) +* bump it-map from 1.0.6 to 2.0.0 ([#396](https://github.com/libp2p/js-libp2p-kad-dht/issues/396)) ([ac5101c](https://github.com/libp2p/js-libp2p-kad-dht/commit/ac5101c764a7eb1be0d531e6a61268b2a8f1a613)) +* bump it-merge from 1.0.4 to 2.0.0 ([#393](https://github.com/libp2p/js-libp2p-kad-dht/issues/393)) ([1acb5f1](https://github.com/libp2p/js-libp2p-kad-dht/commit/1acb5f170351a72860bf9552ecba0f1bf98dc3b6)) +* bump it-parallel from 2.0.2 to 3.0.0 ([#392](https://github.com/libp2p/js-libp2p-kad-dht/issues/392)) ([06a2c48](https://github.com/libp2p/js-libp2p-kad-dht/commit/06a2c480f88447c8631d9348ad28af5308d73eb9)) +* bump it-take from 1.0.2 to 2.0.0 ([#397](https://github.com/libp2p/js-libp2p-kad-dht/issues/397)) ([4e909d2](https://github.com/libp2p/js-libp2p-kad-dht/commit/4e909d286e0bee6f423abe59216b412fe38a2563)) +* **dev:** bump it-filter from 1.0.3 to 2.0.0 ([#395](https://github.com/libp2p/js-libp2p-kad-dht/issues/395)) ([5668f9c](https://github.com/libp2p/js-libp2p-kad-dht/commit/5668f9c6ea9afddc76dd41714515cea65aed1c50)) +* **dev:** bump it-last from 1.0.6 to 2.0.0 ([#388](https://github.com/libp2p/js-libp2p-kad-dht/issues/388)) ([5b55239](https://github.com/libp2p/js-libp2p-kad-dht/commit/5b55239d5a85cb00ea8189fa2803a881313070cc)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v5.0.0...v5.0.1) (2022-10-12) + + +### Bug Fixes + +* update export ([#387](https://github.com/libp2p/js-libp2p-kad-dht/issues/387)) ([9dbbe55](https://github.com/libp2p/js-libp2p-kad-dht/commit/9dbbe55a6a525e78ed8ee8cc7b30636ff93cd18d)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.2...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#386](https://github.com/libp2p/js-libp2p-kad-dht/issues/386)) ([abe5207](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe52072b41d4188af59d61a78b02f38f1cc38a8)), closes [libp2p/js-libp2p-components#6](https://github.com/libp2p/js-libp2p-components/issues/6) + +## [4.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.1...v4.0.2) (2022-10-11) + + +### Dependencies + +* **dev:** bump @libp2p/peer-store from 3.1.5 to 4.0.0 ([#382](https://github.com/libp2p/js-libp2p-kad-dht/issues/382)) ([94c0dc8](https://github.com/libp2p/js-libp2p-kad-dht/commit/94c0dc850f2b7f16eaa6e70d7157c3b2a3bbf7d5)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v4.0.0...v4.0.1) (2022-10-11) + + +### Dependencies + +* bump @libp2p/interface-address-manager from 1.0.3 to 2.0.0 ([#383](https://github.com/libp2p/js-libp2p-kad-dht/issues/383)) ([5e7dfeb](https://github.com/libp2p/js-libp2p-kad-dht/commit/5e7dfeb0fa0479180d92e604052dbb5b16baf5f7)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.1.0...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#379) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#379](https://github.com/libp2p/js-libp2p-kad-dht/issues/379)) ([124be9c](https://github.com/libp2p/js-libp2p-kad-dht/commit/124be9c3be6d49a460d7fdb3e01217fcd7729e8d)) +* **dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.0 ([#378](https://github.com/libp2p/js-libp2p-kad-dht/issues/378)) ([fc6741b](https://github.com/libp2p/js-libp2p-kad-dht/commit/fc6741b01edf5c54dfa8177aff20e035bd28f425)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.6...v3.1.0) (2022-10-04) + + +### Features + +* tag kad-close peers ([#375](https://github.com/libp2p/js-libp2p-kad-dht/issues/375)) ([df15a83](https://github.com/libp2p/js-libp2p-kad-dht/commit/df15a832f9f274b3ebea9b7752e66a149828147c)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.5...v3.0.6) (2022-09-29) + + +### Bug Fixes + +* re-enable ensuring queries run along disjoint paths ([#371](https://github.com/libp2p/js-libp2p-kad-dht/issues/371)) ([5ae4440](https://github.com/libp2p/js-libp2p-kad-dht/commit/5ae4440e7578d3d6adb557f780fcd23ce7fc13b5)) + + +### Trivial Changes + +* remove IRC badge from readme ([#374](https://github.com/libp2p/js-libp2p-kad-dht/issues/374)) ([e48754f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e48754fa5f16420000804d5f8cb9f536cb7e2596)) + +## [3.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.4...v3.0.5) (2022-09-21) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#372](https://github.com/libp2p/js-libp2p-kad-dht/issues/372)) ([cb0bc9e](https://github.com/libp2p/js-libp2p-kad-dht/commit/cb0bc9e2ec44a7d9ba9ac3c2b6079af5b6d76840)) + + +### Trivial Changes + +* increase test timeouts ([#373](https://github.com/libp2p/js-libp2p-kad-dht/issues/373)) ([ad25fae](https://github.com/libp2p/js-libp2p-kad-dht/commit/ad25fae47426fd14ac14364e83b755837fb3a400)) +* Update .github/workflows/stale.yml [skip ci] ([2236c50](https://github.com/libp2p/js-libp2p-kad-dht/commit/2236c505f756c3efa7deefe70a2c070f009c36c6)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.3...v3.0.4) (2022-08-12) + + +### Dependencies + +* update interface-datastore and datastore-level ([#367](https://github.com/libp2p/js-libp2p-kad-dht/issues/367)) ([b2f8e55](https://github.com/libp2p/js-libp2p-kad-dht/commit/b2f8e557eb9721873a401f59ddd3fa12d6ee2ba3)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.2...v3.0.3) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#364](https://github.com/libp2p/js-libp2p-kad-dht/issues/364)) ([52323ab](https://github.com/libp2p/js-libp2p-kad-dht/commit/52323ab051bc6a02084c5653d9998aaa5721b49f)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.1...v3.0.2) (2022-08-10) + + +### Bug Fixes + +* update deps to new versions ([#363](https://github.com/libp2p/js-libp2p-kad-dht/issues/363)) ([7d058d6](https://github.com/libp2p/js-libp2p-kad-dht/commit/7d058d6b8dfd28fd83991778bc32ff463238fe42)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v3.0.0...v3.0.1) (2022-07-31) + + +### Trivial Changes + +* update project config ([#356](https://github.com/libp2p/js-libp2p-kad-dht/issues/356)) ([d944d81](https://github.com/libp2p/js-libp2p-kad-dht/commit/d944d81a24875aad602b2a56bdd6a2c012e4208e)) + + +### Dependencies + +* update it-length-prefixed and protons for no-copy ops ([#357](https://github.com/libp2p/js-libp2p-kad-dht/issues/357)) ([518abfe](https://github.com/libp2p/js-libp2p-kad-dht/commit/518abfe34d7b733f7652ea72e6302a15fc5de64b)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v2.0.0...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* Updates to new registrar API + +### Features + +* specify stream limits ([#348](https://github.com/libp2p/js-libp2p-kad-dht/issues/348)) ([c2a9238](https://github.com/libp2p/js-libp2p-kad-dht/commit/c2a923863c2b4dd1fd95922ffdb21b6bc45f42c8)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.16...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update libp2p interfaces ([#345](https://github.com/libp2p/js-libp2p-kad-dht/issues/345)) ([273f756](https://github.com/libp2p/js-libp2p-kad-dht/commit/273f756fe9da43e9eb6184f51eae29d6d50cffac)) + +### [1.0.16](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.15...v1.0.16) (2022-06-01) + + +### Bug Fixes + +* ping peers with correct protocol ([#344](https://github.com/libp2p/js-libp2p-kad-dht/issues/344)) ([e7ccf13](https://github.com/libp2p/js-libp2p-kad-dht/commit/e7ccf13df07c6a4770fb75307996fd38bfbb54ae)) + +### [1.0.15](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.14...v1.0.15) (2022-05-25) + + +### Trivial Changes + +* fix readme.md ([#342](https://github.com/libp2p/js-libp2p-kad-dht/issues/342)) ([ddea70d](https://github.com/libp2p/js-libp2p-kad-dht/commit/ddea70d23faca82b269271be3fa647896cf498e7)), closes [#324](https://github.com/libp2p/js-libp2p-kad-dht/issues/324) + +### [1.0.14](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.13...v1.0.14) (2022-05-24) + + +### Bug Fixes + +* increase ping concurrency ([#341](https://github.com/libp2p/js-libp2p-kad-dht/issues/341)) ([fa7cfc1](https://github.com/libp2p/js-libp2p-kad-dht/commit/fa7cfc15134cd3909f1c12ea222bae3d1cb06360)) + +### [1.0.13](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.12...v1.0.13) (2022-05-23) + + +### Bug Fixes + +* update deps ([#340](https://github.com/libp2p/js-libp2p-kad-dht/issues/340)) ([69ebfbd](https://github.com/libp2p/js-libp2p-kad-dht/commit/69ebfbd2aaa4d0fb948d2f8c0c8b329b2222aee9)) + +### [1.0.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.11...v1.0.12) (2022-05-16) + + +### Bug Fixes + +* remove p-map dependency ([#335](https://github.com/libp2p/js-libp2p-kad-dht/issues/335)) ([b50039d](https://github.com/libp2p/js-libp2p-kad-dht/commit/b50039d1e0e7fba6ed5c8cc25777302037a0dadb)) + +### [1.0.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.10...v1.0.11) (2022-05-10) + + +### Bug Fixes + +* encode enums correctly ([#332](https://github.com/libp2p/js-libp2p-kad-dht/issues/332)) ([af1e701](https://github.com/libp2p/js-libp2p-kad-dht/commit/af1e70179bf2889015966a48de38450289920ae1)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.9...v1.0.10) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#330](https://github.com/libp2p/js-libp2p-kad-dht/issues/330)) ([e10d5f5](https://github.com/libp2p/js-libp2p-kad-dht/commit/e10d5f5346adbfef5f46fa81961f8870b48210f9)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.8...v1.0.9) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#329](https://github.com/libp2p/js-libp2p-kad-dht/issues/329)) ([b97187e](https://github.com/libp2p/js-libp2p-kad-dht/commit/b97187ed26ec49ee8cc551f2f1e1e72dcdd9de13)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.7...v1.0.8) (2022-05-03) + + +### Trivial Changes + +* update aegir config ([#312](https://github.com/libp2p/js-libp2p-kad-dht/issues/312)) ([526e65e](https://github.com/libp2p/js-libp2p-kad-dht/commit/526e65ede9bc154245970bda3f63113a81ca6c6e)) +* update interfaces to new version ([#327](https://github.com/libp2p/js-libp2p-kad-dht/issues/327)) ([388042b](https://github.com/libp2p/js-libp2p-kad-dht/commit/388042b239e7dc9215c4f0e0cd9d2424df4109f6)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.6...v1.0.7) (2022-04-19) + + +### Bug Fixes + +* return self when asked for self ([#318](https://github.com/libp2p/js-libp2p-kad-dht/issues/318)) ([c84fa2a](https://github.com/libp2p/js-libp2p-kad-dht/commit/c84fa2a7959e9c1b2f60349b7d9271df7cb992d0)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.5...v1.0.6) (2022-04-17) + + +### Bug Fixes + +* max listeners warning ([#316](https://github.com/libp2p/js-libp2p-kad-dht/issues/316)) ([18ba9c0](https://github.com/libp2p/js-libp2p-kad-dht/commit/18ba9c0b57e6aa51cbbaeea44b36888be88b1df7)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.4...v1.0.5) (2022-04-14) + + +### Trivial Changes + +* update deps ([#313](https://github.com/libp2p/js-libp2p-kad-dht/issues/313)) ([347a597](https://github.com/libp2p/js-libp2p-kad-dht/commit/347a5974edc34fb548305929bdc3af10da2a0563)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.3...v1.0.4) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#311](https://github.com/libp2p/js-libp2p-kad-dht/issues/311)) ([fc44105](https://github.com/libp2p/js-libp2p-kad-dht/commit/fc44105e6c2c1ea6714201e5be26fbd7c8b10321)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.2...v1.0.3) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#305](https://github.com/libp2p/js-libp2p-kad-dht/issues/305)) ([2def2bd](https://github.com/libp2p/js-libp2p-kad-dht/commit/2def2bdcd2cf60daeb36da94c041361cc2e683cb)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.1...v1.0.2) (2022-03-23) + + +### Bug Fixes + +* add record validators and selectors ([#304](https://github.com/libp2p/js-libp2p-kad-dht/issues/304)) ([27c3948](https://github.com/libp2p/js-libp2p-kad-dht/commit/27c3948376106ba8bc80e5ac0148e64f5b066cb0)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v1.0.0...v1.0.1) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#302](https://github.com/libp2p/js-libp2p-kad-dht/issues/302)) ([940aba3](https://github.com/libp2p/js-libp2p-kad-dht/commit/940aba35018e5b34cf43d682e29176a1df37d20b)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.6...v1.0.0) (2022-03-04) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#300](https://github.com/libp2p/js-libp2p-kad-dht/issues/300)) ([9696346](https://github.com/libp2p/js-libp2p-kad-dht/commit/9696346bcf48e882c0126268bdec99ec01ec2023)) + +## [0.28.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.5...v0.28.6) (2022-01-19) + + + +## [0.28.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.4...v0.28.5) (2022-01-19) + + +### Bug Fixes + +* update component metric API use ([#293](https://github.com/libp2p/js-libp2p-kad-dht/issues/293)) ([c026f03](https://github.com/libp2p/js-libp2p-kad-dht/commit/c026f0389373718131ee26424b786e55285c1c5e)) + + + +## [0.28.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.3...v0.28.4) (2022-01-17) + + + +## [0.28.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.2...v0.28.3) (2022-01-17) + + +### Bug Fixes + +* catch not found errors ([#291](https://github.com/libp2p/js-libp2p-kad-dht/issues/291)) ([f0a4307](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0a430731d5b026d80495ec1c8fc457d77c29451)) + + + +## [0.28.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.1...v0.28.2) (2022-01-15) + + +### Bug Fixes + +* remove abort controller deps ([#276](https://github.com/libp2p/js-libp2p-kad-dht/issues/276)) ([26cd857](https://github.com/libp2p/js-libp2p-kad-dht/commit/26cd8571a0e050f4ef524f2672d604dcd1288b14)) + + + +## [0.28.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.28.0...v0.28.1) (2021-12-31) + + +### Bug Fixes + +* catch errors from setMaxListeners ([#275](https://github.com/libp2p/js-libp2p-kad-dht/issues/275)) ([de2c601](https://github.com/libp2p/js-libp2p-kad-dht/commit/de2c601632bac41cc8b85b2d3a122f4ed24a7aed)) + + + +# [0.28.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.6...v0.28.0) (2021-12-30) + + +### Features + +* async peer store ([#272](https://github.com/libp2p/js-libp2p-kad-dht/issues/272)) ([12804e2](https://github.com/libp2p/js-libp2p-kad-dht/commit/12804e260e76ac9b990244ff437e9147795fde3d)) + + +### BREAKING CHANGES + +* peerstore methods are now all async + + + +## [0.27.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.5...v0.27.6) (2021-12-29) + + +### Bug Fixes + +* return pk when found ([#273](https://github.com/libp2p/js-libp2p-kad-dht/issues/273)) ([e7d2d7f](https://github.com/libp2p/js-libp2p-kad-dht/commit/e7d2d7ff6744fda4d984bf1ca802027427726809)) + + + +## [0.27.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.4...v0.27.5) (2021-12-21) + + +### Bug Fixes + +* silence max listeners exceeded warning ([#270](https://github.com/libp2p/js-libp2p-kad-dht/issues/270)) ([7b6c90f](https://github.com/libp2p/js-libp2p-kad-dht/commit/7b6c90fa76207a028c14609b4f8834ae9be2bf76)) + + + +## [0.27.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.3...v0.27.4) (2021-12-15) + + +### Features + +* log component metrics ([#269](https://github.com/libp2p/js-libp2p-kad-dht/issues/269)) ([db4f7f7](https://github.com/libp2p/js-libp2p-kad-dht/commit/db4f7f7e56ff7f146112c06f18ffe93f359b8856)) + + + +## [0.27.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.2...v0.27.3) (2021-12-07) + + +### Bug Fixes + +* add default query timeouts ([#266](https://github.com/libp2p/js-libp2p-kad-dht/issues/266)) ([4df2c3f](https://github.com/libp2p/js-libp2p-kad-dht/commit/4df2c3f1f1a8de7583e71acecb64a03e050263d4)) + + + +## [0.27.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.1...v0.27.2) (2021-12-03) + + +### Bug Fixes + +* only provide to wan in server mode ([#264](https://github.com/libp2p/js-libp2p-kad-dht/issues/264)) ([79c0bdb](https://github.com/libp2p/js-libp2p-kad-dht/commit/79c0bdb6471adbea69383f3537c73cf8c5797de8)) + + + +## [0.27.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.27.0...v0.27.1) (2021-12-02) + + + +# [0.27.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.7...v0.27.0) (2021-12-01) + + +### Bug Fixes + +* do not send messages if the network is not running ([#259](https://github.com/libp2p/js-libp2p-kad-dht/issues/259)) ([50ea7aa](https://github.com/libp2p/js-libp2p-kad-dht/commit/50ea7aaa5b22fc7269ec73bf269abfcf6f35b657)) + + +### chore + +* update libp2p-crypto ([#260](https://github.com/libp2p/js-libp2p-kad-dht/issues/260)) ([64f775b](https://github.com/libp2p/js-libp2p-kad-dht/commit/64f775b34397d02eec6eb3c2ccde05abab551722)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +## [0.26.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.6...v0.26.7) (2021-11-26) + + + +## [0.26.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.5...v0.26.6) (2021-11-26) + + +### Bug Fixes + +* increase time between table refresh ([#256](https://github.com/libp2p/js-libp2p-kad-dht/issues/256)) ([1471fb9](https://github.com/libp2p/js-libp2p-kad-dht/commit/1471fb94000c6f80c8a7d64b4a9ca342275a7ec8)) + + + +## [0.26.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.4...v0.26.5) (2021-11-25) + + +### Bug Fixes + +* do not pollute routing table with useless peers ([#254](https://github.com/libp2p/js-libp2p-kad-dht/issues/254)) ([4f79899](https://github.com/libp2p/js-libp2p-kad-dht/commit/4f7989900c6239fa449841be90ebe7f0ed517316)) + + + +## [0.26.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.3...v0.26.4) (2021-11-25) + + +### Bug Fixes + +* require at least one successful put ([#253](https://github.com/libp2p/js-libp2p-kad-dht/issues/253)) ([f7a2a02](https://github.com/libp2p/js-libp2p-kad-dht/commit/f7a2a02ef49c35bf7de1fc0d7a8256281819a740)) + + + +## [0.26.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.2...v0.26.3) (2021-11-25) + + +### Bug Fixes + +* count successful puts ([#252](https://github.com/libp2p/js-libp2p-kad-dht/issues/252)) ([d90f1a6](https://github.com/libp2p/js-libp2p-kad-dht/commit/d90f1a61d8bfd1128312ab5daed3bf831aced74d)) + + + +## [0.26.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.1...v0.26.2) (2021-11-24) + + +### Bug Fixes + +* remove trailing slash from datastore prefixes ([#241](https://github.com/libp2p/js-libp2p-kad-dht/issues/241)) ([2d26f9b](https://github.com/libp2p/js-libp2p-kad-dht/commit/2d26f9bbf77ceabf6cdc7904896454b82b2a8b38)) + + + +## [0.26.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.26.0...v0.26.1) (2021-11-22) + + +### Bug Fixes + +* prefix records with key, remove disjoint queries ([#239](https://github.com/libp2p/js-libp2p-kad-dht/issues/239)) ([e31696c](https://github.com/libp2p/js-libp2p-kad-dht/commit/e31696c3a4363f2fa7c6a6534d67b57252bafe36)) + + + +# [0.26.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.25.0...v0.26.0) (2021-11-18) + + +### Bug Fixes + +* refactor query logic ([#237](https://github.com/libp2p/js-libp2p-kad-dht/issues/237)) ([1f8bc6a](https://github.com/libp2p/js-libp2p-kad-dht/commit/1f8bc6a23d3db592606c789648f13199078e176c)) + + +### Features + +* ping old DHT peers before eviction ([#229](https://github.com/libp2p/js-libp2p-kad-dht/issues/229)) ([eff54bf](https://github.com/libp2p/js-libp2p-kad-dht/commit/eff54bf0c40f03dcff03d139d0bb275e2af175b0)) + + + +# [0.25.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.0...v0.25.0) (2021-09-24) + + +### Bug Fixes + +* browser compatibility ([#226](https://github.com/libp2p/js-libp2p-kad-dht/issues/226)) ([01b7ec1](https://github.com/libp2p/js-libp2p-kad-dht/commit/01b7ec15c059653a83634020bc9668bd7d25c1a9)) +* browser override path ([#228](https://github.com/libp2p/js-libp2p-kad-dht/issues/228)) ([3c737c1](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c737c16399ac7e541b417f0e8b76157ed2f86ff)) + + +### chore + +* update datastore ([#227](https://github.com/libp2p/js-libp2p-kad-dht/issues/227)) ([64a3044](https://github.com/libp2p/js-libp2p-kad-dht/commit/64a304432ecc69c5a13b2af17781ea8b833295d0)) + + +### BREAKING CHANGES + +* provided datastore must implement interface-datastore@6.0.0 + + + +## [0.24.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.1...v0.24.2) (2021-09-14) + + +### Bug Fixes + +* browser override path ([#228](https://github.com/libp2p/js-libp2p-kad-dht/issues/228)) ([3c737c1](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c737c16399ac7e541b417f0e8b76157ed2f86ff)) + + + +## [0.24.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.24.0...v0.24.1) (2021-09-07) + + +### Bug Fixes + +* browser compatibility ([#226](https://github.com/libp2p/js-libp2p-kad-dht/issues/226)) ([01b7ec1](https://github.com/libp2p/js-libp2p-kad-dht/commit/01b7ec15c059653a83634020bc9668bd7d25c1a9)) + + + +# [0.24.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.4...v0.24.0) (2021-09-03) + + +### Features + +* periodically fill the routing table with KADIds ([#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215)) ([d812a91](https://github.com/libp2p/js-libp2p-kad-dht/commit/d812a91e7b59589e8f46b60ba23dcbb4db02d75a)) + + +### BREAKING CHANGES + +* .start() is now async and random walk has been removed + + + +## [0.23.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.3...v0.23.4) (2021-09-03) + + +### Reverts + +* Revert "feat: periodically fill the routing table with KADIds (#215)" ([dd16a28](https://github.com/libp2p/js-libp2p-kad-dht/commit/dd16a28d321e82b8c41ca942a07023b31c23f250)), closes [#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215) + + + +## [0.23.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.2...v0.23.3) (2021-09-03) + + +### Features + +* periodically fill the routing table with KADIds ([#215](https://github.com/libp2p/js-libp2p-kad-dht/issues/215)) ([10f0cc8](https://github.com/libp2p/js-libp2p-kad-dht/commit/10f0cc860b47581019dd8d9ec5b383337708679d)) + + +### BREAKING CHANGES + +* .start() is now async and random walk has been removed + + + +## [0.23.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.22.0...v0.23.2) (2021-08-18) + + +### chore + +* update to new multiformats ([#220](https://github.com/libp2p/js-libp2p-kad-dht/issues/220)) ([565eb00](https://github.com/libp2p/js-libp2p-kad-dht/commit/565eb003c0c5d165088d113f8caecc5f7a5a12ad)) + + +### BREAKING CHANGES + +* uses new multiformats CID class + + + +## [0.23.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.23.0...v0.23.1) (2021-07-08) + + + +# [0.23.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.22.0...v0.23.0) (2021-07-07) + + +### chore + +* update to new multiformats ([#220](https://github.com/libp2p/js-libp2p-kad-dht/issues/220)) ([565eb00](https://github.com/libp2p/js-libp2p-kad-dht/commit/565eb003c0c5d165088d113f8caecc5f7a5a12ad)) + + +### BREAKING CHANGES + +* uses new multiformats CID class + + + +# [0.22.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.21.0...v0.22.0) (2021-04-28) + + + +# [0.21.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.6...v0.21.0) (2021-02-16) + + +### Features + +* add types and update all deps ([#214](https://github.com/libp2p/js-libp2p-kad-dht/issues/214)) ([7195282](https://github.com/libp2p/js-libp2p-kad-dht/commit/71952820ef3f737204b7a615db69ae680ef652a8)) + + + + +## [0.20.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.5...v0.20.6) (2021-01-26) + + + + +## [0.20.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.4...v0.20.5) (2021-01-21) + + +### Bug Fixes + +* do not throw on empty provider list ([#212](https://github.com/libp2p/js-libp2p-kad-dht/issues/212)) ([3c2096e](https://github.com/libp2p/js-libp2p-kad-dht/commit/3c2096e)) + + + + +## [0.20.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.3...v0.20.4) (2020-12-17) + + +### Features + +* adds removeLocal function ([#211](https://github.com/libp2p/js-libp2p-kad-dht/issues/211)) ([d0db16b](https://github.com/libp2p/js-libp2p-kad-dht/commit/d0db16b)) + + + + +## [0.20.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.2...v0.20.3) (2020-12-09) + + +### Features + +* adds custom multicodec protocol option ([#206](https://github.com/libp2p/js-libp2p-kad-dht/issues/206)) ([20d57b5](https://github.com/libp2p/js-libp2p-kad-dht/commit/20d57b5)) + + + + +## [0.20.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.20.1...v0.20.2) (2020-12-04) + + +### Features + +* onPut and onRemove events ([#205](https://github.com/libp2p/js-libp2p-kad-dht/issues/205)) ([b28afdd](https://github.com/libp2p/js-libp2p-kad-dht/commit/b28afdd)) + + + + +## [0.20.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.9...v0.20.1) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#202](https://github.com/libp2p/js-libp2p-kad-dht/issues/202)) ([989be87](https://github.com/libp2p/js-libp2p-kad-dht/commit/989be87)) + + +### BREAKING CHANGES + +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove gh dep urls + + + + +# [0.20.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.9...v0.20.0) (2020-08-10) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#202](https://github.com/libp2p/js-libp2p-kad-dht/issues/202)) ([989be87](https://github.com/libp2p/js-libp2p-kad-dht/commit/989be87)) + + +### BREAKING CHANGES + +* - Where node Buffers were returned, now Uint8Arrays are + +* chore: remove gh dep urls + + + + +## [0.19.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.8...v0.19.9) (2020-07-10) + + +### Bug Fixes + +* actually send the add provider rpc with addresses ([#201](https://github.com/libp2p/js-libp2p-kad-dht/issues/201)) ([f3188be](https://github.com/libp2p/js-libp2p-kad-dht/commit/f3188be)) + + +### Features + +* add support for client mode ([#200](https://github.com/libp2p/js-libp2p-kad-dht/issues/200)) ([91f6e4f](https://github.com/libp2p/js-libp2p-kad-dht/commit/91f6e4f)) + + + + +## [0.19.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.7...v0.19.8) (2020-07-08) + + +### Bug Fixes + +* check for an existing connection before using the dialer ([#199](https://github.com/libp2p/js-libp2p-kad-dht/issues/199)) ([578c5d0](https://github.com/libp2p/js-libp2p-kad-dht/commit/578c5d0)) + + + + +## [0.19.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.6...v0.19.7) (2020-06-23) + + + + +## [0.19.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.5...v0.19.6) (2020-06-16) + + +### Bug Fixes + +* use utils.mapParallel for parallel processing of peers ([#166](https://github.com/libp2p/js-libp2p-kad-dht/issues/166)) ([534a2d9](https://github.com/libp2p/js-libp2p-kad-dht/commit/534a2d9)) + + + + +## [0.19.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.4...v0.19.5) (2020-06-05) + + +### Bug Fixes + +* providers leaking resources on dht construction ([#194](https://github.com/libp2p/js-libp2p-kad-dht/issues/194)) ([59f373a](https://github.com/libp2p/js-libp2p-kad-dht/commit/59f373a)) + + + + +## [0.19.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.3...v0.19.4) (2020-05-20) + + + + +## [0.19.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.2...v0.19.3) (2020-05-15) + + + + +## [0.19.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.1...v0.19.2) (2020-04-28) + + +### Bug Fixes + +* add buffer ([#185](https://github.com/libp2p/js-libp2p-kad-dht/issues/185)) ([a28d279](https://github.com/libp2p/js-libp2p-kad-dht/commit/a28d279)) + + + + +## [0.19.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.0...v0.19.1) (2020-04-27) + + + + +# [0.19.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.19.0-pre.0...v0.19.0) (2020-04-24) + + +### Chores + +* peer-discovery not using peer-info ([#180](https://github.com/libp2p/js-libp2p-kad-dht/issues/180)) ([f0fb212](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0fb212)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + + + + +# [0.19.0-pre.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.6...v0.19.0-pre.0) (2020-04-16) + + +### Chores + +* use new peer store api ([#179](https://github.com/libp2p/js-libp2p-kad-dht/issues/179)) ([194c701](https://github.com/libp2p/js-libp2p-kad-dht/commit/194c701)) + + +### BREAKING CHANGES + +* uses new peer-store api + + + + +## [0.18.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.5...v0.18.6) (2020-03-26) + + + + +## [0.18.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.4...v0.18.5) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#173](https://github.com/libp2p/js-libp2p-kad-dht/issues/173)) ([de85eb6](https://github.com/libp2p/js-libp2p-kad-dht/commit/de85eb6)) + + + + +## [0.18.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.3...v0.18.4) (2020-02-05) + + + + +## [0.18.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.2...v0.18.3) (2019-12-12) + + +### Bug Fixes + +* dont use peer ids in sets ([#165](https://github.com/libp2p/js-libp2p-kad-dht/issues/165)) ([e12e540](https://github.com/libp2p/js-libp2p-kad-dht/commit/e12e540)) + + + + +## [0.18.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.1...v0.18.2) (2019-12-06) + + +### Bug Fixes + +* get many should not fail if found locally ([#161](https://github.com/libp2p/js-libp2p-kad-dht/issues/161)) ([091db13](https://github.com/libp2p/js-libp2p-kad-dht/commit/091db13)) + + + + +## [0.18.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.18.0...v0.18.1) (2019-12-05) + + +### Bug Fixes + +* find providers should yield when found locally ([#160](https://github.com/libp2p/js-libp2p-kad-dht/issues/160)) ([e40834a](https://github.com/libp2p/js-libp2p-kad-dht/commit/e40834a)) + + + + +# [0.18.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.17.1...v0.18.0) (2019-11-30) + + +### Features + +* find providers and closest peers return async iterable ([#157](https://github.com/libp2p/js-libp2p-kad-dht/issues/157)) ([f0e6800](https://github.com/libp2p/js-libp2p-kad-dht/commit/f0e6800)) + + +### BREAKING CHANGES + +* API for find providers and closest peers return async iterable instead of an array of PeerInfo + + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.17.0...v0.17.1) (2019-11-28) + + +### Bug Fixes + +* remove extraneous message size filter ([#156](https://github.com/libp2p/js-libp2p-kad-dht/issues/156)) ([58b6b36](https://github.com/libp2p/js-libp2p-kad-dht/commit/58b6b36)) + + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.16.1...v0.17.0) (2019-11-26) + + +### Bug Fixes + +* stop and start should not fail ([#152](https://github.com/libp2p/js-libp2p-kad-dht/issues/152)) ([eee2f61](https://github.com/libp2p/js-libp2p-kad-dht/commit/eee2f61)) + + +### Code Refactoring + +* async await ([#148](https://github.com/libp2p/js-libp2p-kad-dht/issues/148)) ([c49fa92](https://github.com/libp2p/js-libp2p-kad-dht/commit/c49fa92)) + + +### BREAKING CHANGES + +* Switch to using async/await and async iterators. + +Co-Authored-By: Jacob Heun + + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.16.0...v0.16.1) (2019-10-21) + + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.3...v0.16.0) (2019-08-16) + + +### Code Refactoring + +* use async datastore ([#140](https://github.com/libp2p/js-libp2p-kad-dht/issues/140)) ([daf9b00](https://github.com/libp2p/js-libp2p-kad-dht/commit/daf9b00)) + + +### BREAKING CHANGES + +* The DHT now requires its datastore to have +a promise based api, instead of callbacks. Datastores that use +ipfs/interface-datastore@0.7 or later should be used. +https://github.com/ipfs/interface-datastore/releases/tag/v0.7.0 + + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.2...v0.15.3) (2019-07-29) + + +### Bug Fixes + +* _findNProvidersAsync discarding search results ([#137](https://github.com/libp2p/js-libp2p-kad-dht/issues/137)) ([e656c6b](https://github.com/libp2p/js-libp2p-kad-dht/commit/e656c6b)) + + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.1...v0.15.2) (2019-05-31) + + +### Bug Fixes + +* favour providers peerInfo over sender peerInfo in ADD_PROVIDER ([#129](https://github.com/libp2p/js-libp2p-kad-dht/issues/129)) ([6da26b0](https://github.com/libp2p/js-libp2p-kad-dht/commit/6da26b0)) + + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.15.0...v0.15.1) (2019-05-30) + + +### Bug Fixes + +* in _findNProviders correctly calculate pathSize ([5841dfe](https://github.com/libp2p/js-libp2p-kad-dht/commit/5841dfe)) +* send correct payload in ADD_PROVIDER RPC ([#127](https://github.com/libp2p/js-libp2p-kad-dht/issues/127)) ([8d92d5a](https://github.com/libp2p/js-libp2p-kad-dht/commit/8d92d5a)) + + +### Features + +* use promisify-es6 instead of pify ([1d228e0](https://github.com/libp2p/js-libp2p-kad-dht/commit/1d228e0)) + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.15...v0.15.0) (2019-05-13) + + +### Chores + +* update cids dependency ([#117](https://github.com/libp2p/js-libp2p-kad-dht/issues/117)) ([04e213a](https://github.com/libp2p/js-libp2p-kad-dht/commit/04e213a)) + + +### BREAKING CHANGES + +* v1 CIDs are now encoded in base32 when stringified. + +https://github.com/ipfs/js-ipfs/issues/1995 + +License: MIT +Signed-off-by: Alan Shaw + + + + +## [0.14.15](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.14...v0.14.15) (2019-05-10) + + +### Bug Fixes + +* query stop with query not initialized ([b29dfde](https://github.com/libp2p/js-libp2p-kad-dht/commit/b29dfde)) + + + + +## [0.14.14](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.13...v0.14.14) (2019-05-08) + + +### Bug Fixes + +* performance improvements ([#107](https://github.com/libp2p/js-libp2p-kad-dht/issues/107)) ([ddf80fe](https://github.com/libp2p/js-libp2p-kad-dht/commit/ddf80fe)) + + + + +## [0.14.13](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.12...v0.14.13) (2019-04-22) + + +### Bug Fixes + +* random walk ([#104](https://github.com/libp2p/js-libp2p-kad-dht/issues/104)) ([9db17eb](https://github.com/libp2p/js-libp2p-kad-dht/commit/9db17eb)) + + +### Features + +* add delay support to random walk ([#101](https://github.com/libp2p/js-libp2p-kad-dht/issues/101)) ([7b70fa7](https://github.com/libp2p/js-libp2p-kad-dht/commit/7b70fa7)) +* limit scope of queries to k closest peers ([#97](https://github.com/libp2p/js-libp2p-kad-dht/issues/97)) ([f03619e](https://github.com/libp2p/js-libp2p-kad-dht/commit/f03619e)) + + + + +## [0.14.12](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.11...v0.14.12) (2019-04-04) + + +### Bug Fixes + +* stop running queries on shutdown ([#95](https://github.com/libp2p/js-libp2p-kad-dht/issues/95)) ([e137297](https://github.com/libp2p/js-libp2p-kad-dht/commit/e137297)) + + + + +## [0.14.11](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.10...v0.14.11) (2019-03-28) + + +### Bug Fixes + +* ensure queries stop after error or success ([#93](https://github.com/libp2p/js-libp2p-kad-dht/issues/93)) ([0e55b20](https://github.com/libp2p/js-libp2p-kad-dht/commit/0e55b20)) +* getMany with nvals=1 now goes out to network if no local val ([#91](https://github.com/libp2p/js-libp2p-kad-dht/issues/91)) ([478ee88](https://github.com/libp2p/js-libp2p-kad-dht/commit/478ee88)) + + + + +## [0.14.10](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.9...v0.14.10) (2019-03-27) + + +### Bug Fixes + +* false discovery ([#92](https://github.com/libp2p/js-libp2p-kad-dht/issues/92)) ([466c992](https://github.com/libp2p/js-libp2p-kad-dht/commit/466c992)) + + + + +## [0.14.9](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.8...v0.14.9) (2019-03-18) + + +### Bug Fixes + +* reduce bundle size ([#90](https://github.com/libp2p/js-libp2p-kad-dht/issues/90)) ([f79eeb2](https://github.com/libp2p/js-libp2p-kad-dht/commit/f79eeb2)) + + + + +## [0.14.8](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.7...v0.14.8) (2019-03-13) + + +### Bug Fixes + +* incoming message should not connect to peers ([#88](https://github.com/libp2p/js-libp2p-kad-dht/issues/88)) ([8c16b81](https://github.com/libp2p/js-libp2p-kad-dht/commit/8c16b81)) + + + + +## [0.14.7](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.6...v0.14.7) (2019-03-04) + + +### Bug Fixes + +* put query for closest peers ([#85](https://github.com/libp2p/js-libp2p-kad-dht/issues/85)) ([84a40cd](https://github.com/libp2p/js-libp2p-kad-dht/commit/84a40cd)) + + + + +## [0.14.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.5...v0.14.6) (2019-02-25) + + +### Bug Fixes + +* specify # of peers for successful put ([#72](https://github.com/libp2p/js-libp2p-kad-dht/issues/72)) ([97e8e60](https://github.com/libp2p/js-libp2p-kad-dht/commit/97e8e60)) + + +### Features + +* expose randomwalk parameters in config ([#77](https://github.com/libp2p/js-libp2p-kad-dht/issues/77)) ([dc5a67f](https://github.com/libp2p/js-libp2p-kad-dht/commit/dc5a67f)) + + + + +## [0.14.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.4...v0.14.5) (2019-02-05) + + +### Features + +* emit event on peer connected ([#66](https://github.com/libp2p/js-libp2p-kad-dht/issues/66)) ([ba0a537](https://github.com/libp2p/js-libp2p-kad-dht/commit/ba0a537)) + + + + +## [0.14.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.3...v0.14.4) (2019-01-14) + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.2...v0.14.3) (2019-01-04) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.1...v0.14.2) (2019-01-04) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.14.0...v0.14.1) (2018-12-11) + + +### Bug Fixes + +* typo get many option ([#63](https://github.com/libp2p/js-libp2p-kad-dht/issues/63)) ([de5a9fb](https://github.com/libp2p/js-libp2p-kad-dht/commit/de5a9fb)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.13.0...v0.14.0) (2018-12-11) + + +### Chores + +* update options timeout property ([#62](https://github.com/libp2p/js-libp2p-kad-dht/issues/62)) ([3046b54](https://github.com/libp2p/js-libp2p-kad-dht/commit/3046b54)) + + +### BREAKING CHANGES + +* get, getMany, findProviders and findPeer do not accept a timeout number anymore. It must be a property of an object options. + +Co-Authored-By: vasco-santos + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.12.1...v0.13.0) (2018-12-05) + + +### Bug Fixes + +* make 'find peer query' test reliable ([#58](https://github.com/libp2p/js-libp2p-kad-dht/issues/58)) ([54336dd](https://github.com/libp2p/js-libp2p-kad-dht/commit/54336dd)) + + +### Features + +* run queries on disjoint paths ([#37](https://github.com/libp2p/js-libp2p-kad-dht/issues/37)) ([#39](https://github.com/libp2p/js-libp2p-kad-dht/issues/39)) ([742b3fb](https://github.com/libp2p/js-libp2p-kad-dht/commit/742b3fb)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.12.0...v0.12.1) (2018-11-30) + + +### Features + +* allow configurable validators and selectors ([#57](https://github.com/libp2p/js-libp2p-kad-dht/issues/57)) ([b731a1d](https://github.com/libp2p/js-libp2p-kad-dht/commit/b731a1d)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.11.1...v0.12.0) (2018-11-22) + + + + +## [0.11.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.11.0...v0.11.1) (2018-11-12) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.6...v0.11.0) (2018-11-09) + + +### Bug Fixes + +* record outdated local correction ([#49](https://github.com/libp2p/js-libp2p-kad-dht/issues/49)) ([d1869ed](https://github.com/libp2p/js-libp2p-kad-dht/commit/d1869ed)) + + +### Features + +* select first record when no selector function ([#51](https://github.com/libp2p/js-libp2p-kad-dht/issues/51)) ([683a903](https://github.com/libp2p/js-libp2p-kad-dht/commit/683a903)) + + + + +## [0.10.6](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.5...v0.10.6) (2018-10-25) + + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.4...v0.10.5) (2018-10-01) + + +### Features + +* start random walk and allow configuration for disabling ([#42](https://github.com/libp2p/js-libp2p-kad-dht/issues/42)) ([abe9407](https://github.com/libp2p/js-libp2p-kad-dht/commit/abe9407)) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.3...v0.10.4) (2018-09-27) + + +### Bug Fixes + +* find peer and providers options ([#45](https://github.com/libp2p/js-libp2p-kad-dht/issues/45)) ([bba7500](https://github.com/libp2p/js-libp2p-kad-dht/commit/bba7500)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.2...v0.10.3) (2018-09-20) + + +### Bug Fixes + +* dht get options ([#40](https://github.com/libp2p/js-libp2p-kad-dht/issues/40)) ([0a2f9fe](https://github.com/libp2p/js-libp2p-kad-dht/commit/0a2f9fe)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.0...v0.10.2) (2018-08-29) + + +### Bug Fixes + +* dont read when just doing a write ([7a92139](https://github.com/libp2p/js-libp2p-kad-dht/commit/7a92139)) +* make findProviders treat timeout the same as findPeer ([#35](https://github.com/libp2p/js-libp2p-kad-dht/issues/35)) ([fcdb01d](https://github.com/libp2p/js-libp2p-kad-dht/commit/fcdb01d)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.10.0...v0.10.1) (2018-07-13) + + +### Bug Fixes + +* dont read when just doing a write ([7a92139](https://github.com/libp2p/js-libp2p-kad-dht/commit/7a92139)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.9.0...v0.10.0) (2018-04-05) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.8.0...v0.9.0) (2018-03-15) + + +### Features + +* upgrade the discovery service to random-walk ([b8e0f72](https://github.com/libp2p/js-libp2p-kad-dht/commit/b8e0f72)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.7.0...v0.8.0) (2018-02-07) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.0...v0.7.0) (2018-02-07) + + +### Bug Fixes + +* release providers resources ([#23](https://github.com/libp2p/js-libp2p-kad-dht/issues/23)) ([ff87f4b](https://github.com/libp2p/js-libp2p-kad-dht/commit/ff87f4b)) + + +### Features + +* use libp2p-switch ([054e5e5](https://github.com/libp2p/js-libp2p-kad-dht/commit/054e5e5)) + + + + +## [0.6.3](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.2...v0.6.3) (2018-01-30) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.1...v0.6.2) (2018-01-30) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.6.0...v0.6.1) (2018-01-30) + + +### Bug Fixes + +* release providers resources ([#23](https://github.com/libp2p/js-libp2p-kad-dht/issues/23)) ([ff87f4b](https://github.com/libp2p/js-libp2p-kad-dht/commit/ff87f4b)) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.5.1...v0.6.0) (2017-11-09) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.5.0...v0.5.1) (2017-09-07) + + +### Features + +* replace protocol-buffers with protons ([#16](https://github.com/libp2p/js-libp2p-kad-dht/issues/16)) ([de259ff](https://github.com/libp2p/js-libp2p-kad-dht/commit/de259ff)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.4.1...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#15](https://github.com/libp2p/js-libp2p-kad-dht/issues/15)) ([3870dd2](https://github.com/libp2p/js-libp2p-kad-dht/commit/3870dd2)) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.4.0...v0.4.1) (2017-07-22) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.3.0...v0.4.0) (2017-07-22) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.2.1...v0.3.0) (2017-07-17) + + +### Bug Fixes + +* no more circular dependency, become a good block of libp2p ([#13](https://github.com/libp2p/js-libp2p-kad-dht/issues/13)) ([810be4d](https://github.com/libp2p/js-libp2p-kad-dht/commit/810be4d)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.2.0...v0.2.1) (2017-07-13) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/v0.1.0...v0.2.0) (2017-07-07) + + +### Features + +* using libp2p new state methods ([#12](https://github.com/libp2p/js-libp2p-kad-dht/issues/12)) ([982f789](https://github.com/libp2p/js-libp2p-kad-dht/commit/982f789)) + + + + +# [0.1.0](https://github.com/libp2p/js-libp2p-kad-dht/compare/4bd1fbc...v0.1.0) (2017-04-07) + + +### Features + +* v0.1.0 ([4bd1fbc](https://github.com/libp2p/js-libp2p-kad-dht/commit/4bd1fbc)) diff --git a/packages/kad-dht/LICENSE b/packages/kad-dht/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/kad-dht/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/kad-dht/LICENSE-APACHE b/packages/kad-dht/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/kad-dht/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/kad-dht/LICENSE-MIT b/packages/kad-dht/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/kad-dht/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/kad-dht/README.md b/packages/kad-dht/README.md new file mode 100644 index 0000000000..d0cb29462c --- /dev/null +++ b/packages/kad-dht/README.md @@ -0,0 +1,103 @@ +# @libp2p/kad-dht + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> JavaScript implementation of the Kad-DHT for libp2p + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +```sh +> npm i @libp2p/kad-dht +``` + +### Use in Node.js + +```js +import { create } from '@libp2p/kad-dht' +``` + +## API + +See for the auto generated docs. + +The libp2p-kad-dht module offers 3 APIs: Peer Routing, Content Routing and Peer Discovery. + +### Custom secondary DHT in libp2p + +```js +import { createLibp2pNode } from 'libp2p' +import { kadDHT } from '@libp2p/kad-dht' + +const node = await createLibp2pNode({ + dht: kadDHT() + //... other config +}) +await node.start() + +for await (const event of node.dht.findPeer(node.peerId)) { + console.info(event) +} +``` + +Note that you may want to supply your own peer discovery function and datastore + +### Peer Routing + +[![](https://raw.githubusercontent.com/libp2p/interface-peer-routing/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/peer-routing) + +### Content Routing + +[![](https://raw.githubusercontent.com/libp2p/interface-content-routing/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/content-routing) + +### Peer Discovery + +[![](https://github.com/libp2p/interface-peer-discovery/blob/master/img/badge.png?raw=true)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/peer-discovery) + +## Spec + +js-libp2p-kad-dht follows the [libp2p/kad-dht spec](https://github.com/libp2p/specs/tree/master/kad-dht) and implements the algorithms described in the [IPFS DHT documentation](https://docs.ipfs.io/concepts/dht/). + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/kad-dht/package.json b/packages/kad-dht/package.json new file mode 100644 index 0000000000..d4feddee61 --- /dev/null +++ b/packages/kad-dht/package.json @@ -0,0 +1,132 @@ +{ + "name": "@libp2p/kad-dht", + "version": "9.3.6", + "description": "JavaScript implementation of the Kad-DHT for libp2p", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/kad-dht#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/message/dht.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "build": "aegir build", + "generate": "protons ./src/message/dht.proto", + "test": "aegir test", + "test:node": "aegir test -t node --cov", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "dep-check": "aegir dep-check -i protons -i events" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-address-manager": "^3.0.0", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-connection-manager": "^3.0.0", + "@libp2p/interface-content-routing": "^2.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interface-peer-routing": "^1.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-collections": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/record": "^3.0.0", + "@libp2p/topology": "^4.0.0", + "@multiformats/multiaddr": "^12.1.3", + "@types/sinon": "^10.0.15", + "abortable-iterator": "^5.0.1", + "any-signal": "^4.1.1", + "datastore-core": "^9.0.1", + "events": "^3.3.0", + "hashlru": "^2.3.0", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", + "it-drain": "^3.0.1", + "it-first": "^3.0.1", + "it-length": "^3.0.1", + "it-length-prefixed": "^9.0.1", + "it-map": "^3.0.2", + "it-merge": "^3.0.0", + "it-parallel": "^3.0.0", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "it-take": "^3.0.1", + "multiformats": "^11.0.2", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "p-queue": "^7.3.4", + "private-ip": "^3.0.0", + "progress-events": "^1.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3", + "varint": "^6.0.0" + }, + "devDependencies": { + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/peer-store": "^8.0.0", + "@types/lodash.random": "^3.2.6", + "@types/lodash.range": "^3.2.6", + "@types/varint": "^6.0.0", + "@types/which": "^3.0.0", + "aegir": "^39.0.10", + "datastore-level": "^10.0.0", + "delay": "^6.0.0", + "execa": "^7.0.0", + "it-filter": "^3.0.1", + "it-last": "^3.0.1", + "lodash.random": "^3.2.0", + "lodash.range": "^3.2.0", + "p-retry": "^5.0.0", + "p-wait-for": "^5.0.0", + "protons": "^7.0.2", + "sinon": "^15.1.0", + "ts-sinon": "^2.0.2", + "which": "^3.0.0" + }, + "browser": { + "./dist/src/routing-table/generated-prefix-list.js": "./dist/src/routing-table/generated-prefix-list-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/kad-dht/src/constants.ts b/packages/kad-dht/src/constants.ts new file mode 100644 index 0000000000..12edd7f512 --- /dev/null +++ b/packages/kad-dht/src/constants.ts @@ -0,0 +1,57 @@ +// MaxRecordAge specifies the maximum time that any node will hold onto a record +// from the time its received. This does not apply to any other forms of validity that +// the record may contain. +// For example, a record may contain an ipns entry with an EOL saying its valid +// until the year 2020 (a great time in the future). For that record to stick around +// it must be rebroadcasted more frequently than once every 'MaxRecordAge' + +export const second = 1000 +export const minute = 60 * second +export const hour = 60 * minute + +export const MAX_RECORD_AGE = 36 * hour + +export const LAN_PREFIX = '/lan' + +export const PROTOCOL_PREFIX = '/ipfs' + +export const PROTOCOL_DHT = '/kad/1.0.0' + +export const RECORD_KEY_PREFIX = '/dht/record' + +export const PROVIDER_KEY_PREFIX = '/dht/provider' + +export const PROVIDERS_LRU_CACHE_SIZE = 256 + +export const PROVIDERS_VALIDITY = 24 * hour + +export const PROVIDERS_CLEANUP_INTERVAL = hour + +export const READ_MESSAGE_TIMEOUT = 10 * second + +// The number of records that will be retrieved on a call to getMany() +export const GET_MANY_RECORD_COUNT = 16 + +// K is the maximum number of requests to perform before returning failure +export const K = 20 + +// Alpha is the concurrency for asynchronous requests +export const ALPHA = 3 + +// How often we look for our closest DHT neighbours +export const QUERY_SELF_INTERVAL = Number(5 * minute) + +// How often we look for the first set of our closest DHT neighbours +export const QUERY_SELF_INITIAL_INTERVAL = Number(Number(second)) + +// How long to look for our closest DHT neighbours for +export const QUERY_SELF_TIMEOUT = Number(5 * second) + +// How often we try to find new peers +export const TABLE_REFRESH_INTERVAL = Number(5 * minute) + +// How how long to look for new peers for +export const TABLE_REFRESH_QUERY_TIMEOUT = Number(30 * second) + +// When a timeout is not specified, run a query for this long +export const DEFAULT_QUERY_TIMEOUT = Number(30 * second) diff --git a/packages/kad-dht/src/content-fetching/index.ts b/packages/kad-dht/src/content-fetching/index.ts new file mode 100644 index 0000000000..67c86dbde1 --- /dev/null +++ b/packages/kad-dht/src/content-fetching/index.ts @@ -0,0 +1,263 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { Libp2pRecord } from '@libp2p/record' +import { bestRecord } from '@libp2p/record/selectors' +import { verifyRecord } from '@libp2p/record/validators' +import map from 'it-map' +import parallel from 'it-parallel' +import { pipe } from 'it-pipe' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { + ALPHA +} from '../constants.js' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { + valueEvent, + queryErrorEvent +} from '../query/events.js' +import { createPutRecord, bufferToRecordKey } from '../utils.js' +import type { KadDHTComponents, Validators, Selectors, ValueEvent, QueryOptions, QueryEvent } from '../index.js' +import type { Network } from '../network.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { QueryManager } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Logger } from '@libp2p/logger' + +export interface ContentFetchingInit { + validators: Validators + selectors: Selectors + peerRouting: PeerRouting + queryManager: QueryManager + network: Network + lan: boolean +} + +export class ContentFetching { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly validators: Validators + private readonly selectors: Selectors + private readonly peerRouting: PeerRouting + private readonly queryManager: QueryManager + private readonly network: Network + + constructor (components: KadDHTComponents, init: ContentFetchingInit) { + const { validators, selectors, peerRouting, queryManager, network, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:content-fetching`) + this.validators = validators + this.selectors = selectors + this.peerRouting = peerRouting + this.queryManager = queryManager + this.network = network + } + + async putLocal (key: Uint8Array, rec: Uint8Array): Promise { + const dsKey = bufferToRecordKey(key) + await this.components.datastore.put(dsKey, rec) + } + + /** + * Attempt to retrieve the value for the given key from + * the local datastore + */ + async getLocal (key: Uint8Array): Promise { + this.log('getLocal %b', key) + + const dsKey = bufferToRecordKey(key) + + this.log('fetching record for key %k', dsKey) + + const raw = await this.components.datastore.get(dsKey) + this.log('found %k in local datastore', dsKey) + + const rec = Libp2pRecord.deserialize(raw) + + await verifyRecord(this.validators, rec) + + return rec + } + + /** + * Send the best record found to any peers that have an out of date record + */ + async * sendCorrectionRecord (key: Uint8Array, vals: ValueEvent[], best: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + this.log('sendCorrection for %b', key) + const fixupRec = createPutRecord(key, best) + + for (const { value, from } of vals) { + // no need to do anything + if (uint8ArrayEquals(value, best)) { + this.log('record was ok') + continue + } + + // correct ourself + if (this.components.peerId.equals(from)) { + try { + const dsKey = bufferToRecordKey(key) + this.log(`Storing corrected record for key ${dsKey.toString()}`) + await this.components.datastore.put(dsKey, fixupRec.subarray()) + } catch (err: any) { + this.log.error('Failed error correcting self', err) + } + + continue + } + + // send correction + let sentCorrection = false + const request = new Message(MESSAGE_TYPE.PUT_VALUE, key, 0) + request.record = Libp2pRecord.deserialize(fixupRec) + + for await (const event of this.network.sendRequest(from, request, options)) { + if (event.name === 'PEER_RESPONSE' && (event.record != null) && uint8ArrayEquals(event.record.value, Libp2pRecord.deserialize(fixupRec).value)) { + sentCorrection = true + } + + yield event + } + + if (!sentCorrection) { + yield queryErrorEvent({ from, error: new CodeError('value not put correctly', 'ERR_PUT_VALUE_INVALID') }, options) + } + + this.log.error('Failed error correcting entry') + } + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + this.log('put key %b value %b', key, value) + + // create record in the dht format + const record = createPutRecord(key, value) + + // store the record locally + const dsKey = bufferToRecordKey(key) + this.log(`storing record for key ${dsKey.toString()}`) + await this.components.datastore.put(dsKey, record.subarray()) + + // put record to the closest peers + yield * pipe( + this.peerRouting.getClosestPeers(key, { signal: options.signal }), + (source) => map(source, (event) => { + return async () => { + if (event.name !== 'FINAL_PEER') { + return [event] + } + + const events = [] + + const msg = new Message(MESSAGE_TYPE.PUT_VALUE, key, 0) + msg.record = Libp2pRecord.deserialize(record) + + this.log('send put to %p', event.peer.id) + for await (const putEvent of this.network.sendRequest(event.peer.id, msg, options)) { + events.push(putEvent) + + if (putEvent.name !== 'PEER_RESPONSE') { + continue + } + + if (!(putEvent.record != null && uint8ArrayEquals(putEvent.record.value, Libp2pRecord.deserialize(record).value))) { + events.push(queryErrorEvent({ from: event.peer.id, error: new CodeError('value not put correctly', 'ERR_PUT_VALUE_INVALID') }, options)) + } + } + + return events + } + }), + (source) => parallel(source, { + ordered: false, + concurrency: ALPHA + }), + async function * (source) { + for await (const events of source) { + yield * events + } + } + ) + } + + /** + * Get the value to the given key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('get %b', key) + + const vals: ValueEvent[] = [] + + for await (const event of this.getMany(key, options)) { + if (event.name === 'VALUE') { + vals.push(event) + } + + yield event + } + + if (vals.length === 0) { + return + } + + const records = vals.map((v) => v.value) + let i = 0 + + try { + i = bestRecord(this.selectors, key, records) + } catch (err: any) { + // Assume the first record if no selector available + if (err.code !== 'ERR_NO_SELECTOR_FUNCTION_FOR_RECORD_KEY') { + throw err + } + } + + const best = records[i] + this.log('GetValue %b %b', key, best) + + if (best == null) { + throw new CodeError('best value was not found', 'ERR_NOT_FOUND') + } + + yield * this.sendCorrectionRecord(key, vals, best, options) + + yield vals[i] + } + + /** + * Get the `n` values to the given key without sorting + */ + async * getMany (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('getMany values for %b', key) + + try { + const localRec = await this.getLocal(key) + + yield valueEvent({ + value: localRec.value, + from: this.components.peerId + }, options) + } catch (err: any) { + this.log('error getting local value for %b', key, err) + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const getValueQuery: QueryFunc = async function * ({ peer, signal }) { + for await (const event of self.peerRouting.getValueOrPeers(peer, key, { signal })) { + yield event + + if (event.name === 'PEER_RESPONSE' && (event.record != null)) { + yield valueEvent({ from: peer, value: event.record.value }, options) + } + } + } + + // we have peers, lets send the actual query to them + yield * this.queryManager.run(key, getValueQuery, options) + } +} diff --git a/packages/kad-dht/src/content-routing/index.ts b/packages/kad-dht/src/content-routing/index.ts new file mode 100644 index 0000000000..5e4a8e0d72 --- /dev/null +++ b/packages/kad-dht/src/content-routing/index.ts @@ -0,0 +1,205 @@ +import { logger } from '@libp2p/logger' +import map from 'it-map' +import parallel from 'it-parallel' +import { pipe } from 'it-pipe' +import { ALPHA } from '../constants.js' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { + queryErrorEvent, + peerResponseEvent, + providerEvent +} from '../query/events.js' +import type { KadDHTComponents, PeerResponseEvent, ProviderEvent, QueryEvent, QueryOptions } from '../index.js' +import type { Network } from '../network.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { Providers } from '../providers.js' +import type { QueryManager } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Logger } from '@libp2p/logger' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { CID } from 'multiformats/cid' + +export interface ContentRoutingInit { + network: Network + peerRouting: PeerRouting + queryManager: QueryManager + routingTable: RoutingTable + providers: Providers + lan: boolean +} + +export class ContentRouting { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly network: Network + private readonly peerRouting: PeerRouting + private readonly queryManager: QueryManager + private readonly routingTable: RoutingTable + private readonly providers: Providers + + constructor (components: KadDHTComponents, init: ContentRoutingInit) { + const { network, peerRouting, queryManager, routingTable, providers, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:content-routing`) + this.network = network + this.peerRouting = peerRouting + this.queryManager = queryManager + this.routingTable = routingTable + this.providers = providers + } + + /** + * Announce to the network that we can provide the value for a given key and + * are contactable on the given multiaddrs + */ + async * provide (key: CID, multiaddrs: Multiaddr[], options: QueryOptions = {}): AsyncGenerator { + this.log('provide %s', key) + + // Add peer as provider + await this.providers.addProvider(key, this.components.peerId) + + const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, key.multihash.bytes, 0) + msg.providerPeers = [{ + id: this.components.peerId, + multiaddrs, + protocols: [] + }] + + let sent = 0 + + const maybeNotifyPeer = (event: QueryEvent) => { + return async () => { + if (event.name !== 'FINAL_PEER') { + return [event] + } + + const events = [] + + this.log('putProvider %s to %p', key, event.peer.id) + + try { + this.log('sending provider record for %s to %p', key, event.peer.id) + + for await (const sendEvent of this.network.sendMessage(event.peer.id, msg, options)) { + if (sendEvent.name === 'PEER_RESPONSE') { + this.log('sent provider record for %s to %p', key, event.peer.id) + sent++ + } + + events.push(sendEvent) + } + } catch (err: any) { + this.log.error('error sending provide record to peer %p', event.peer.id, err) + events.push(queryErrorEvent({ from: event.peer.id, error: err }, options)) + } + + return events + } + } + + // Notify closest peers + yield * pipe( + this.peerRouting.getClosestPeers(key.multihash.bytes, options), + (source) => map(source, (event) => maybeNotifyPeer(event)), + (source) => parallel(source, { + ordered: false, + concurrency: ALPHA + }), + async function * (source) { + for await (const events of source) { + yield * events + } + } + ) + + this.log('sent provider records to %d peers', sent) + } + + /** + * Search the dht for up to `K` providers of the given CID. + */ + async * findProviders (key: CID, options: QueryOptions): AsyncGenerator { + const toFind = this.routingTable.kBucketSize + const target = key.multihash.bytes + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + this.log('findProviders %c', key) + + const provs = await this.providers.getProviders(key) + + // yield values if we have some, also slice because maybe we got lucky and already have too many? + if (provs.length > 0) { + const providers: PeerInfo[] = [] + + for (const peerId of provs.slice(0, toFind)) { + try { + const peer = await this.components.peerStore.get(peerId) + + providers.push({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + + this.log('no peer store entry for %p', peerId) + } + } + + yield peerResponseEvent({ from: this.components.peerId, messageType: MESSAGE_TYPE.GET_PROVIDERS, providers }, options) + yield providerEvent({ from: this.components.peerId, providers }, options) + } + + // All done + if (provs.length >= toFind) { + return + } + + /** + * The query function to use on this particular disjoint path + */ + const findProvidersQuery: QueryFunc = async function * ({ peer, signal }) { + const request = new Message(MESSAGE_TYPE.GET_PROVIDERS, target, 0) + + yield * self.network.sendRequest(peer, request, { + ...options, + signal + }) + } + + const providers = new Set(provs.map(p => p.toString())) + + for await (const event of this.queryManager.run(target, findProvidersQuery, options)) { + yield event + + if (event.name === 'PEER_RESPONSE') { + this.log('Found %d provider entries for %c and %d closer peers', event.providers.length, key, event.closer.length) + + const newProviders = [] + + for (const peer of event.providers) { + if (providers.has(peer.id.toString())) { + continue + } + + providers.add(peer.id.toString()) + newProviders.push(peer) + } + + if (newProviders.length > 0) { + yield providerEvent({ from: event.from, providers: newProviders }, options) + } + + if (providers.size === toFind) { + return + } + } + } + } +} diff --git a/packages/kad-dht/src/dual-kad-dht.ts b/packages/kad-dht/src/dual-kad-dht.ts new file mode 100644 index 0000000000..32a9f71d10 --- /dev/null +++ b/packages/kad-dht/src/dual-kad-dht.ts @@ -0,0 +1,400 @@ +import { type ContentRouting, contentRouting } from '@libp2p/interface-content-routing' +import { type PeerDiscovery, peerDiscovery, type PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import { type PeerRouting, peerRouting } from '@libp2p/interface-peer-routing' +import { CodeError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import drain from 'it-drain' +import merge from 'it-merge' +import isPrivate from 'private-ip' +import { DefaultKadDHT } from './kad-dht.js' +import { queryErrorEvent } from './query/events.js' +import type { DualKadDHT, KadDHT, KadDHTComponents, KadDHTInit, QueryEvent, QueryOptions } from './index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { CID } from 'multiformats/cid' + +const log = logger('libp2p:kad-dht') + +/** + * Wrapper class to convert events into returned values + */ +class DHTContentRouting implements ContentRouting { + private readonly dht: KadDHT + + constructor (dht: KadDHT) { + this.dht = dht + } + + async provide (cid: CID, options: QueryOptions = {}): Promise { + await drain(this.dht.provide(cid, options)) + } + + async * findProviders (cid: CID, options: QueryOptions = {}): AsyncGenerator { + for await (const event of this.dht.findProviders(cid, options)) { + if (event.name === 'PROVIDER') { + yield * event.providers + } + } + } + + async put (key: Uint8Array, value: Uint8Array, options?: QueryOptions): Promise { + await drain(this.dht.put(key, value, options)) + } + + async get (key: Uint8Array, options?: QueryOptions): Promise { + for await (const event of this.dht.get(key, options)) { + if (event.name === 'VALUE') { + return event.value + } + } + + throw new CodeError('Not found', 'ERR_NOT_FOUND') + } +} + +/** + * Wrapper class to convert events into returned values + */ +class DHTPeerRouting implements PeerRouting { + private readonly dht: KadDHT + + constructor (dht: KadDHT) { + this.dht = dht + } + + async findPeer (peerId: PeerId, options: QueryOptions = {}): Promise { + for await (const event of this.dht.findPeer(peerId, options)) { + if (event.name === 'FINAL_PEER') { + return event.peer + } + } + + throw new CodeError('Not found', 'ERR_NOT_FOUND') + } + + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncIterable { + for await (const event of this.dht.getClosestPeers(key, options)) { + if (event.name === 'FINAL_PEER') { + yield event.peer + } + } + } +} + +// see https://github.com/multiformats/multiaddr/blob/master/protocols.csv +const P2P_CIRCUIT_CODE = 290 +const DNS4_CODE = 54 +const DNS6_CODE = 55 +const DNSADDR_CODE = 56 +const IP4_CODE = 4 +const IP6_CODE = 41 + +function multiaddrIsPublic (multiaddr: Multiaddr): boolean { + const tuples = multiaddr.stringTuples() + + // p2p-circuit should not enable server mode + for (const tuple of tuples) { + if (tuple[0] === P2P_CIRCUIT_CODE) { + return false + } + } + + // dns4 or dns6 or dnsaddr + if (tuples[0][0] === DNS4_CODE || tuples[0][0] === DNS6_CODE || tuples[0][0] === DNSADDR_CODE) { + log('%m is public %s', multiaddr, true) + + return true + } + + // ip4 or ip6 + if (tuples[0][0] === IP4_CODE || tuples[0][0] === IP6_CODE) { + const result = isPrivate(`${tuples[0][1]}`) + const isPublic = result == null || !result + + log('%m is public %s', multiaddr, isPublic) + + return isPublic + } + + return false +} + +/** + * A DHT implementation modelled after Kademlia with S/Kademlia modifications. + * Original implementation in go: https://github.com/libp2p/go-libp2p-kad-dht. + */ +export class DefaultDualKadDHT extends EventEmitter implements DualKadDHT, PeerDiscovery { + public readonly wan: DefaultKadDHT + public readonly lan: DefaultKadDHT + public readonly components: KadDHTComponents + private readonly contentRouting: ContentRouting + private readonly peerRouting: PeerRouting + + constructor (components: KadDHTComponents, init: KadDHTInit = {}) { + super() + + this.components = components + + this.wan = new DefaultKadDHT(components, { + protocolPrefix: '/ipfs', + ...init, + lan: false + }) + this.lan = new DefaultKadDHT(components, { + protocolPrefix: '/ipfs', + ...init, + clientMode: false, + lan: true + }) + + this.contentRouting = new DHTContentRouting(this) + this.peerRouting = new DHTPeerRouting(this) + + // handle peers being discovered during processing of DHT messages + this.wan.addEventListener('peer', (evt) => { + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + }) + this.lan.addEventListener('peer', (evt) => { + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + }) + + // if client mode has not been explicitly specified, auto-switch to server + // mode when the node's peer data is updated with publicly dialable addresses + if (init.clientMode == null) { + components.events.addEventListener('self:peer:update', (evt) => { + log('received update of self-peer info') + const hasPublicAddress = evt.detail.peer.addresses + .some(({ multiaddr }) => { + const isPublic = multiaddrIsPublic(multiaddr) + + log('%m is public %s', multiaddr, isPublic) + + return isPublic + }) + + this.getMode() + .then(async mode => { + if (hasPublicAddress && mode === 'client') { + await this.setMode('server') + } else if (mode === 'server' && !hasPublicAddress) { + await this.setMode('client') + } + }) + .catch(err => { + log.error('error setting dht server mode', err) + }) + }) + } + } + + readonly [Symbol.toStringTag] = '@libp2p/dual-kad-dht' + + get [contentRouting] (): ContentRouting { + return this.contentRouting + } + + get [peerRouting] (): PeerRouting { + return this.peerRouting + } + + get [peerDiscovery] (): PeerDiscovery { + return this + } + + /** + * Is this DHT running. + */ + isStarted (): boolean { + return this.wan.isStarted() && this.lan.isStarted() + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async getMode (): Promise<'client' | 'server'> { + return this.wan.getMode() + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async setMode (mode: 'client' | 'server'): Promise { + await this.wan.setMode(mode) + } + + /** + * Start listening to incoming connections. + */ + async start (): Promise { + await Promise.all([ + this.lan.start(), + this.wan.start() + ]) + } + + /** + * Stop accepting incoming connections and sending outgoing + * messages. + */ + async stop (): Promise { + await Promise.all([ + this.lan.stop(), + this.wan.stop() + ]) + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + for await (const event of merge( + this.lan.put(key, value, options), + this.wan.put(key, value, options) + )) { + yield event + } + } + + /** + * Get the value that corresponds to the passed key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + let queriedPeers = false + let foundValue = false + + for await (const event of merge( + this.lan.get(key, options), + this.wan.get(key, options) + )) { + yield event + + if (event.name === 'DIAL_PEER') { + queriedPeers = true + } + + if (event.name === 'VALUE') { + queriedPeers = true + + if (event.value != null) { + foundValue = true + } + } + + if (event.name === 'SEND_QUERY') { + queriedPeers = true + } + } + + if (!queriedPeers) { + throw new CodeError('No peers found in routing table!', 'ERR_NO_PEERS_IN_ROUTING_TABLE') + } + + if (!foundValue) { + yield queryErrorEvent({ + from: this.components.peerId, + error: new CodeError('Not found', 'ERR_NOT_FOUND') + }, options) + } + } + + // ----------- Content Routing + + /** + * Announce to the network that we can provide given key's value + */ + async * provide (key: CID, options: QueryOptions = {}): AsyncGenerator { + let sent = 0 + let success = 0 + const errors = [] + + const dhts = [this.lan] + + // only run provide on the wan if we are in server mode + if ((await this.wan.getMode()) === 'server') { + dhts.push(this.wan) + } + + for await (const event of merge(...dhts.map(dht => dht.provide(key, options)))) { + yield event + + if (event.name === 'SEND_QUERY') { + sent++ + } + + if (event.name === 'QUERY_ERROR') { + errors.push(event.error) + } + + if (event.name === 'PEER_RESPONSE' && event.messageName === 'ADD_PROVIDER') { + log('sent provider record for %s to %p', key, event.from) + success++ + } + } + + if (success === 0) { + if (errors.length > 0) { + // if all sends failed, throw an error to inform the caller + throw new CodeError(`Failed to provide to ${errors.length} of ${sent} peers`, 'ERR_PROVIDES_FAILED', { errors }) + } + + throw new CodeError('Failed to provide - no peers found', 'ERR_PROVIDES_FAILED') + } + } + + /** + * Search the dht for up to `K` providers of the given CID + */ + async * findProviders (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * merge( + this.lan.findProviders(key, options), + this.wan.findProviders(key, options) + ) + } + + // ----------- Peer Routing ----------- + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + let queriedPeers = false + + for await (const event of merge( + this.lan.findPeer(id, options), + this.wan.findPeer(id, options) + )) { + yield event + + if (event.name === 'SEND_QUERY' || event.name === 'FINAL_PEER') { + queriedPeers = true + } + } + + if (!queriedPeers) { + throw new CodeError('Peer lookup failed', 'ERR_LOOKUP_FAILED') + } + } + + /** + * Kademlia 'node lookup' operation + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * merge( + this.lan.getClosestPeers(key, options), + this.wan.getClosestPeers(key, options) + ) + } + + async refreshRoutingTable (): Promise { + await Promise.all([ + this.lan.refreshRoutingTable(), + this.wan.refreshRoutingTable() + ]) + } +} diff --git a/packages/kad-dht/src/index.ts b/packages/kad-dht/src/index.ts new file mode 100644 index 0000000000..81ea30d402 --- /dev/null +++ b/packages/kad-dht/src/index.ts @@ -0,0 +1,321 @@ +import { DefaultDualKadDHT } from './dual-kad-dht.js' +import type { ProvidersInit } from './providers.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' +import type { AbortOptions } from '@libp2p/interfaces' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Datastore } from 'interface-datastore' +import type { CID } from 'multiformats/cid' +import type { ProgressOptions, ProgressEvent } from 'progress-events' + +/** + * The types of events emitted during DHT queries + */ +export enum EventTypes { + SEND_QUERY = 0, + PEER_RESPONSE, + FINAL_PEER, + QUERY_ERROR, + PROVIDER, + VALUE, + ADD_PEER, + DIAL_PEER +} + +/** + * The types of messages sent to peers during DHT queries + */ +export enum MessageType { + PUT_VALUE = 0, + GET_VALUE, + ADD_PROVIDER, + GET_PROVIDERS, + FIND_NODE, + PING +} + +export type MessageName = keyof typeof MessageType + +export interface DHTRecord { + key: Uint8Array + value: Uint8Array + timeReceived?: Date +} + +export type DHTProgressEvents = + ProgressEvent<'kad-dht:query:send-query', SendQueryEvent> | + ProgressEvent<'kad-dht:query:peer-response', PeerResponseEvent> | + ProgressEvent<'kad-dht:query:final-peer', FinalPeerEvent> | + ProgressEvent<'kad-dht:query:query-error', QueryErrorEvent> | + ProgressEvent<'kad-dht:query:provider', ProviderEvent> | + ProgressEvent<'kad-dht:query:value', ValueEvent> | + ProgressEvent<'kad-dht:query:add-peer', AddPeerEvent> | + ProgressEvent<'kad-dht:query:dial-peer', DialPeerEvent> + +export interface QueryOptions extends AbortOptions, ProgressOptions { + queryFuncTimeout?: number +} + +/** + * Emitted when sending queries to remote peers + */ +export interface SendQueryEvent { + to: PeerId + type: EventTypes.SEND_QUERY + name: 'SEND_QUERY' + messageName: keyof typeof MessageType + messageType: MessageType +} + +/** + * Emitted when query responses are received form remote peers. Depending on the query + * these events may be followed by a `FinalPeerEvent`, a `ValueEvent` or a `ProviderEvent`. + */ +export interface PeerResponseEvent { + from: PeerId + type: EventTypes.PEER_RESPONSE + name: 'PEER_RESPONSE' + messageName: keyof typeof MessageType + messageType: MessageType + closer: PeerInfo[] + providers: PeerInfo[] + record?: DHTRecord +} + +/** + * Emitted at the end of a `findPeer` query + */ +export interface FinalPeerEvent { + from: PeerId + peer: PeerInfo + type: EventTypes.FINAL_PEER + name: 'FINAL_PEER' +} + +/** + * Something went wrong with the query + */ +export interface QueryErrorEvent { + from: PeerId + type: EventTypes.QUERY_ERROR + name: 'QUERY_ERROR' + error: Error +} + +/** + * Emitted when providers are found + */ +export interface ProviderEvent { + from: PeerId + type: EventTypes.PROVIDER + name: 'PROVIDER' + providers: PeerInfo[] +} + +/** + * Emitted when values are found + */ +export interface ValueEvent { + from: PeerId + type: EventTypes.VALUE + name: 'VALUE' + value: Uint8Array +} + +/** + * Emitted when peers are added to a query + */ +export interface AddPeerEvent { + type: EventTypes.ADD_PEER + name: 'ADD_PEER' + peer: PeerId +} + +/** + * Emitted when peers are dialled as part of a query + */ +export interface DialPeerEvent { + peer: PeerId + type: EventTypes.DIAL_PEER + name: 'DIAL_PEER' +} + +export type QueryEvent = SendQueryEvent | PeerResponseEvent | FinalPeerEvent | QueryErrorEvent | ProviderEvent | ValueEvent | AddPeerEvent | DialPeerEvent + +export interface RoutingTable { + size: number +} + +export interface KadDHT { + /** + * Get a value from the DHT, the final ValueEvent will be the best value + */ + get: (key: Uint8Array, options?: QueryOptions) => AsyncIterable + + /** + * Find providers of a given CID + */ + findProviders: (key: CID, options?: QueryOptions) => AsyncIterable + + /** + * Find a peer on the DHT + */ + findPeer: (id: PeerId, options?: QueryOptions) => AsyncIterable + + /** + * Find the closest peers to the passed key + */ + getClosestPeers: (key: Uint8Array, options?: QueryOptions) => AsyncIterable + + /** + * Store provider records for the passed CID on the DHT pointing to us + */ + provide: (key: CID, options?: QueryOptions) => AsyncIterable + + /** + * Store the passed value under the passed key on the DHT + */ + put: (key: Uint8Array, value: Uint8Array, options?: QueryOptions) => AsyncIterable + + /** + * Returns the mode this node is in + */ + getMode: () => Promise<'client' | 'server'> + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + setMode: (mode: 'client' | 'server') => Promise + + /** + * Force a routing table refresh + */ + refreshRoutingTable: () => Promise +} + +export interface SingleKadDHT extends KadDHT { + routingTable: RoutingTable +} + +export interface DualKadDHT extends KadDHT { + wan: SingleKadDHT + lan: SingleKadDHT +} + +/** + * A selector function takes a DHT key and a list of records and returns the + * index of the best record in the list + */ +export interface SelectFn { (key: Uint8Array, records: Uint8Array[]): number } + +/** + * A validator function takes a DHT key and the value of the record for that key + * and throws if the record is invalid + */ +export interface ValidateFn { (key: Uint8Array, value: Uint8Array): Promise } + +/** + * Selectors are a map of key prefixes to selector functions + */ +export type Selectors = Record + +/** + * Validators are a map of key prefixes to validator functions + */ +export type Validators = Record + +export interface KadDHTInit { + /** + * How many peers to store in each kBucket (default 20) + */ + kBucketSize?: number + + /** + * Whether to start up as a DHT client or server + */ + clientMode?: boolean + + /** + * Record selectors + */ + selectors?: Selectors + + /** + * Record validators + */ + validators?: Validators + + /** + * How often to query our own PeerId in order to ensure we have a + * good view on the KAD address space local to our PeerId + */ + querySelfInterval?: number + + /** + * During startup we run the self-query at a shorter interval to ensure + * the containing node can respond to queries quickly. Set this interval + * here in ms (default: 1000) + */ + initialQuerySelfInterval?: number + + /** + * After startup by default all queries will be paused until the initial + * self-query has run and there are some peers in the routing table. + * + * Pass true here to disable this behaviour. (default: false) + */ + allowQueryWithZeroPeers?: boolean + + /** + * A custom protocol prefix to use (default: '/ipfs') + */ + protocolPrefix?: string + + /** + * How long to wait in ms when pinging DHT peers to decide if they + * should be evicted from the routing table or not (default 10000) + */ + pingTimeout?: number + + /** + * How many peers to ping in parallel when deciding if they should + * be evicted from the routing table or not (default 10) + */ + pingConcurrency?: number + + /** + * How many parallel incoming streams to allow on the DHT protocol per-connection + */ + maxInboundStreams?: number + + /** + * How many parallel outgoing streams to allow on the DHT protocol per-connection + */ + maxOutboundStreams?: number + + /** + * Initialization options for the Providers component + */ + providers?: ProvidersInit +} + +export interface KadDHTComponents { + peerId: PeerId + registrar: Registrar + addressManager: AddressManager + peerStore: PeerStore + metrics?: Metrics + connectionManager: ConnectionManager + datastore: Datastore + events: EventEmitter +} + +export function kadDHT (init?: KadDHTInit): (components: KadDHTComponents) => DualKadDHT { + return (components: KadDHTComponents) => new DefaultDualKadDHT(components, init) +} diff --git a/packages/kad-dht/src/kad-dht.ts b/packages/kad-dht/src/kad-dht.ts new file mode 100644 index 0000000000..b0e40154f0 --- /dev/null +++ b/packages/kad-dht/src/kad-dht.ts @@ -0,0 +1,364 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { type Logger, logger } from '@libp2p/logger' +import { selectors as recordSelectors } from '@libp2p/record/selectors' +import { validators as recordValidators } from '@libp2p/record/validators' +import pDefer from 'p-defer' +import { PROTOCOL_DHT, PROTOCOL_PREFIX, LAN_PREFIX } from './constants.js' +import { ContentFetching } from './content-fetching/index.js' +import { ContentRouting } from './content-routing/index.js' +import { Network } from './network.js' +import { PeerRouting } from './peer-routing/index.js' +import { Providers } from './providers.js' +import { QueryManager } from './query/manager.js' +import { QuerySelf } from './query-self.js' +import { RoutingTable } from './routing-table/index.js' +import { RoutingTableRefresh } from './routing-table/refresh.js' +import { RPC } from './rpc/index.js' +import { TopologyListener } from './topology-listener.js' +import { + removePrivateAddresses, + removePublicAddresses +} from './utils.js' +import type { KadDHTComponents, KadDHTInit, QueryOptions, Validators, Selectors, KadDHT, QueryEvent } from './index.js' +import type { PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { CID } from 'multiformats/cid' + +export const DEFAULT_MAX_INBOUND_STREAMS = 32 +export const DEFAULT_MAX_OUTBOUND_STREAMS = 64 + +export interface SingleKadDHTInit extends KadDHTInit { + /** + * Whether to start up in lan or wan mode + */ + lan?: boolean +} + +/** + * A DHT implementation modelled after Kademlia with S/Kademlia modifications. + * Original implementation in go: https://github.com/libp2p/go-libp2p-kad-dht. + */ +export class DefaultKadDHT extends EventEmitter implements KadDHT { + public protocol: string + public routingTable: RoutingTable + public providers: Providers + public network: Network + public peerRouting: PeerRouting + + public readonly components: KadDHTComponents + private readonly log: Logger + private running: boolean + private readonly kBucketSize: number + private clientMode: boolean + private readonly lan: boolean + private readonly validators: Validators + private readonly selectors: Selectors + private readonly queryManager: QueryManager + private readonly contentFetching: ContentFetching + private readonly contentRouting: ContentRouting + private readonly routingTableRefresh: RoutingTableRefresh + private readonly rpc: RPC + private readonly topologyListener: TopologyListener + private readonly querySelf: QuerySelf + private readonly maxInboundStreams: number + private readonly maxOutboundStreams: number + + /** + * Create a new KadDHT + */ + constructor (components: KadDHTComponents, init: SingleKadDHTInit) { + super() + + const { + kBucketSize, + clientMode, + validators, + selectors, + querySelfInterval, + lan, + protocolPrefix, + pingTimeout, + pingConcurrency, + maxInboundStreams, + maxOutboundStreams, + providers: providersInit + } = init + + this.running = false + this.components = components + this.lan = Boolean(lan) + this.log = logger(`libp2p:kad-dht:${lan === true ? 'lan' : 'wan'}`) + this.protocol = `${protocolPrefix ?? PROTOCOL_PREFIX}${lan === true ? LAN_PREFIX : ''}${PROTOCOL_DHT}` + this.kBucketSize = kBucketSize ?? 20 + this.clientMode = clientMode ?? true + this.maxInboundStreams = maxInboundStreams ?? DEFAULT_MAX_INBOUND_STREAMS + this.maxOutboundStreams = maxOutboundStreams ?? DEFAULT_MAX_OUTBOUND_STREAMS + this.routingTable = new RoutingTable(components, { + kBucketSize, + lan: this.lan, + pingTimeout, + pingConcurrency, + protocol: this.protocol + }) + + this.providers = new Providers(components, providersInit ?? {}) + + this.validators = { + ...recordValidators, + ...validators + } + this.selectors = { + ...recordSelectors, + ...selectors + } + this.network = new Network(components, { + protocol: this.protocol, + lan: this.lan + }) + + // all queries should wait for the initial query-self query to run so we have + // some peers and don't force consumers to use arbitrary timeouts + const initialQuerySelfHasRun = pDefer() + + // if the user doesn't want to wait for query peers, resolve the initial + // self-query promise immediately + if (init.allowQueryWithZeroPeers === true) { + initialQuerySelfHasRun.resolve() + } + + this.queryManager = new QueryManager(components, { + // Number of disjoint query paths to use - This is set to `kBucketSize/2` per the S/Kademlia paper + disjointPaths: Math.ceil(this.kBucketSize / 2), + lan, + initialQuerySelfHasRun, + routingTable: this.routingTable + }) + + // DHT components + this.peerRouting = new PeerRouting(components, { + routingTable: this.routingTable, + network: this.network, + validators: this.validators, + queryManager: this.queryManager, + lan: this.lan + }) + this.contentFetching = new ContentFetching(components, { + validators: this.validators, + selectors: this.selectors, + peerRouting: this.peerRouting, + queryManager: this.queryManager, + network: this.network, + lan: this.lan + }) + this.contentRouting = new ContentRouting(components, { + network: this.network, + peerRouting: this.peerRouting, + queryManager: this.queryManager, + routingTable: this.routingTable, + providers: this.providers, + lan: this.lan + }) + this.routingTableRefresh = new RoutingTableRefresh({ + peerRouting: this.peerRouting, + routingTable: this.routingTable, + lan: this.lan + }) + this.rpc = new RPC(components, { + routingTable: this.routingTable, + providers: this.providers, + peerRouting: this.peerRouting, + validators: this.validators, + lan: this.lan + }) + this.topologyListener = new TopologyListener(components, { + protocol: this.protocol, + lan: this.lan + }) + this.querySelf = new QuerySelf(components, { + peerRouting: this.peerRouting, + interval: querySelfInterval, + initialInterval: init.initialQuerySelfInterval, + lan: this.lan, + initialQuerySelfHasRun, + routingTable: this.routingTable + }) + + // handle peers being discovered during processing of DHT messages + this.network.addEventListener('peer', (evt) => { + const peerData = evt.detail + + this.onPeerConnect(peerData).catch(err => { + this.log.error('could not add %p to routing table', peerData.id, err) + }) + + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + + // handle peers being discovered via other peer discovery mechanisms + this.topologyListener.addEventListener('peer', (evt) => { + const peerId = evt.detail + + Promise.resolve().then(async () => { + const peer = await this.components.peerStore.get(peerId) + + const peerData = { + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + } + + await this.onPeerConnect(peerData) + }).catch(err => { + this.log.error('could not add %p to routing table', peerId, err) + }) + }) + } + + async onPeerConnect (peerData: PeerInfo): Promise { + this.log('peer %p connected with protocols', peerData.id, peerData.protocols) + + if (this.lan) { + peerData = removePublicAddresses(peerData) + } else { + peerData = removePrivateAddresses(peerData) + } + + if (peerData.multiaddrs.length === 0) { + this.log('ignoring %p as they do not have any %s addresses in %s', peerData.id, this.lan ? 'private' : 'public', peerData.multiaddrs.map(addr => addr.toString())) + return + } + + try { + await this.routingTable.add(peerData.id) + } catch (err: any) { + this.log.error('could not add %p to routing table', peerData.id, err) + } + } + + /** + * Is this DHT running. + */ + isStarted (): boolean { + return this.running + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async getMode (): Promise<'client' | 'server'> { + return this.clientMode ? 'client' : 'server' + } + + /** + * If 'server' this node will respond to DHT queries, if 'client' this node will not + */ + async setMode (mode: 'client' | 'server'): Promise { + await this.components.registrar.unhandle(this.protocol) + + if (mode === 'client') { + this.log('enabling client mode') + this.clientMode = true + } else { + this.log('enabling server mode') + this.clientMode = false + await this.components.registrar.handle(this.protocol, this.rpc.onIncomingStream.bind(this.rpc), { + maxInboundStreams: this.maxInboundStreams, + maxOutboundStreams: this.maxOutboundStreams + }) + } + } + + /** + * Start listening to incoming connections. + */ + async start (): Promise { + this.running = true + + // Only respond to queries when not in client mode + await this.setMode(this.clientMode ? 'client' : 'server') + + await Promise.all([ + this.providers.start(), + this.queryManager.start(), + this.network.start(), + this.routingTable.start(), + this.topologyListener.start() + ]) + + this.querySelf.start() + + await this.routingTableRefresh.start() + } + + /** + * Stop accepting incoming connections and sending outgoing + * messages. + */ + async stop (): Promise { + this.running = false + + this.querySelf.stop() + + await Promise.all([ + this.providers.stop(), + this.queryManager.stop(), + this.network.stop(), + this.routingTable.stop(), + this.routingTableRefresh.stop(), + this.topologyListener.stop() + ]) + } + + /** + * Store the given key/value pair in the DHT + */ + async * put (key: Uint8Array, value: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentFetching.put(key, value, options) + } + + /** + * Get the value that corresponds to the passed key + */ + async * get (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentFetching.get(key, options) + } + + // ----------- Content Routing + + /** + * Announce to the network that we can provide given key's value + */ + async * provide (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentRouting.provide(key, this.components.addressManager.getAddresses(), options) + } + + /** + * Search the dht for providers of the given CID + */ + async * findProviders (key: CID, options: QueryOptions = {}): AsyncGenerator { + yield * this.contentRouting.findProviders(key, options) + } + + // ----------- Peer Routing ----------- + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + yield * this.peerRouting.findPeer(id, options) + } + + /** + * Kademlia 'node lookup' operation + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + yield * this.peerRouting.getClosestPeers(key, options) + } + + async refreshRoutingTable (): Promise { + this.routingTableRefresh.refreshTable(true) + } +} diff --git a/packages/kad-dht/src/message/dht.proto b/packages/kad-dht/src/message/dht.proto new file mode 100644 index 0000000000..160735b952 --- /dev/null +++ b/packages/kad-dht/src/message/dht.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; +// can't use, because protocol-buffers doesn't support imports +// so we have to duplicate for now :( +// import "record.proto"; + +message Record { + // adjusted for javascript + optional bytes key = 1; + optional bytes value = 2; + optional bytes author = 3; + optional bytes signature = 4; + optional string timeReceived = 5; +} + +message Message { + enum MessageType { + PUT_VALUE = 0; + GET_VALUE = 1; + ADD_PROVIDER = 2; + GET_PROVIDERS = 3; + FIND_NODE = 4; + PING = 5; + } + + enum ConnectionType { + // sender does not have a connection to peer, and no extra information (default) + NOT_CONNECTED = 0; + + // sender has a live connection to peer + CONNECTED = 1; + + // sender recently connected to peer + CAN_CONNECT = 2; + + // sender recently tried to connect to peer repeatedly but failed to connect + // ("try" here is loose, but this should signal "made strong effort, failed") + CANNOT_CONNECT = 3; + } + + message Peer { + // ID of a given peer. + optional bytes id = 1; + + // multiaddrs for a given peer + repeated bytes addrs = 2; + + // used to signal the sender's connection capabilities to the peer + optional ConnectionType connection = 3; + } + + // defines what type of message it is. + optional MessageType type = 1; + + // defines what coral cluster level this query/response belongs to. + // in case we want to implement coral's cluster rings in the future. + optional int32 clusterLevelRaw = 10; + + // Used to specify the key associated with this message. + // PUT_VALUE, GET_VALUE, ADD_PROVIDER, GET_PROVIDERS + // adjusted for javascript + optional bytes key = 2; + + // Used to return a value + // PUT_VALUE, GET_VALUE + // adjusted Record to bytes for js + optional bytes record = 3; + + // Used to return peers closer to a key in a query + // GET_VALUE, GET_PROVIDERS, FIND_NODE + repeated Peer closerPeers = 8; + + // Used to return Providers + // GET_VALUE, ADD_PROVIDER, GET_PROVIDERS + repeated Peer providerPeers = 9; +} diff --git a/packages/kad-dht/src/message/dht.ts b/packages/kad-dht/src/message/dht.ts new file mode 100644 index 0000000000..af919307e3 --- /dev/null +++ b/packages/kad-dht/src/message/dht.ts @@ -0,0 +1,331 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Record { + key?: Uint8Array + value?: Uint8Array + author?: Uint8Array + signature?: Uint8Array + timeReceived?: string +} + +export namespace Record { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.key != null) { + w.uint32(10) + w.bytes(obj.key) + } + + if (obj.value != null) { + w.uint32(18) + w.bytes(obj.value) + } + + if (obj.author != null) { + w.uint32(26) + w.bytes(obj.author) + } + + if (obj.signature != null) { + w.uint32(34) + w.bytes(obj.signature) + } + + if (obj.timeReceived != null) { + w.uint32(42) + w.string(obj.timeReceived) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.bytes() + break + case 2: + obj.value = reader.bytes() + break + case 3: + obj.author = reader.bytes() + break + case 4: + obj.signature = reader.bytes() + break + case 5: + obj.timeReceived = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Record.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Record => { + return decodeMessage(buf, Record.codec()) + } +} + +export interface Message { + type?: Message.MessageType + clusterLevelRaw?: number + key?: Uint8Array + record?: Uint8Array + closerPeers: Message.Peer[] + providerPeers: Message.Peer[] +} + +export namespace Message { + export enum MessageType { + PUT_VALUE = 'PUT_VALUE', + GET_VALUE = 'GET_VALUE', + ADD_PROVIDER = 'ADD_PROVIDER', + GET_PROVIDERS = 'GET_PROVIDERS', + FIND_NODE = 'FIND_NODE', + PING = 'PING' + } + + enum __MessageTypeValues { + PUT_VALUE = 0, + GET_VALUE = 1, + ADD_PROVIDER = 2, + GET_PROVIDERS = 3, + FIND_NODE = 4, + PING = 5 + } + + export namespace MessageType { + export const codec = (): Codec => { + return enumeration(__MessageTypeValues) + } + } + + export enum ConnectionType { + NOT_CONNECTED = 'NOT_CONNECTED', + CONNECTED = 'CONNECTED', + CAN_CONNECT = 'CAN_CONNECT', + CANNOT_CONNECT = 'CANNOT_CONNECT' + } + + enum __ConnectionTypeValues { + NOT_CONNECTED = 0, + CONNECTED = 1, + CAN_CONNECT = 2, + CANNOT_CONNECT = 3 + } + + export namespace ConnectionType { + export const codec = (): Codec => { + return enumeration(__ConnectionTypeValues) + } + } + + export interface Peer { + id?: Uint8Array + addrs: Uint8Array[] + connection?: Message.ConnectionType + } + + export namespace Peer { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.id != null) { + w.uint32(10) + w.bytes(obj.id) + } + + if (obj.addrs != null) { + for (const value of obj.addrs) { + w.uint32(18) + w.bytes(value) + } + } + + if (obj.connection != null) { + w.uint32(24) + Message.ConnectionType.codec().encode(obj.connection, w) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + addrs: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.id = reader.bytes() + break + case 2: + obj.addrs.push(reader.bytes()) + break + case 3: + obj.connection = Message.ConnectionType.codec().decode(reader) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer => { + return decodeMessage(buf, Peer.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.type != null) { + w.uint32(8) + Message.MessageType.codec().encode(obj.type, w) + } + + if (obj.clusterLevelRaw != null) { + w.uint32(80) + w.int32(obj.clusterLevelRaw) + } + + if (obj.key != null) { + w.uint32(18) + w.bytes(obj.key) + } + + if (obj.record != null) { + w.uint32(26) + w.bytes(obj.record) + } + + if (obj.closerPeers != null) { + for (const value of obj.closerPeers) { + w.uint32(66) + Message.Peer.codec().encode(value, w) + } + } + + if (obj.providerPeers != null) { + for (const value of obj.providerPeers) { + w.uint32(74) + Message.Peer.codec().encode(value, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + closerPeers: [], + providerPeers: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = Message.MessageType.codec().decode(reader) + break + case 10: + obj.clusterLevelRaw = reader.int32() + break + case 2: + obj.key = reader.bytes() + break + case 3: + obj.record = reader.bytes() + break + case 8: + obj.closerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + break + case 9: + obj.providerPeers.push(Message.Peer.codec().decode(reader, reader.uint32())) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/kad-dht/src/message/index.ts b/packages/kad-dht/src/message/index.ts new file mode 100644 index 0000000000..ab946ab90c --- /dev/null +++ b/packages/kad-dht/src/message/index.ts @@ -0,0 +1,110 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { multiaddr } from '@multiformats/multiaddr' +import { Message as PBMessage } from './dht.js' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Uint8ArrayList } from 'uint8arraylist' + +export const MESSAGE_TYPE = PBMessage.MessageType +export const CONNECTION_TYPE = PBMessage.ConnectionType +export const MESSAGE_TYPE_LOOKUP = Object.keys(MESSAGE_TYPE) + +interface PBPeer { + id: Uint8Array + addrs: Uint8Array[] + connection: PBMessage.ConnectionType +} + +/** + * Represents a single DHT control message. + */ +export class Message { + public type: PBMessage.MessageType + public key: Uint8Array + private clusterLevelRaw: number + public closerPeers: PeerInfo[] + public providerPeers: PeerInfo[] + public record?: Libp2pRecord + + constructor (type: PBMessage.MessageType, key: Uint8Array, level: number) { + if (!(key instanceof Uint8Array)) { + throw new Error('Key must be a Uint8Array') + } + + this.type = type + this.key = key + this.clusterLevelRaw = level + this.closerPeers = [] + this.providerPeers = [] + this.record = undefined + } + + /** + * @type {number} + */ + get clusterLevel (): number { + const level = this.clusterLevelRaw - 1 + if (level < 0) { + return 0 + } + + return level + } + + set clusterLevel (level) { + this.clusterLevelRaw = level + } + + /** + * Encode into protobuf + */ + serialize (): Uint8Array { + return PBMessage.encode({ + key: this.key, + type: this.type, + clusterLevelRaw: this.clusterLevelRaw, + closerPeers: this.closerPeers.map(toPbPeer), + providerPeers: this.providerPeers.map(toPbPeer), + record: this.record == null ? undefined : this.record.serialize().subarray() + }) + } + + /** + * Decode from protobuf + */ + static deserialize (raw: Uint8ArrayList | Uint8Array): Message { + const dec = PBMessage.decode(raw) + + const msg = new Message(dec.type ?? PBMessage.MessageType.PUT_VALUE, dec.key ?? Uint8Array.from([]), dec.clusterLevelRaw ?? 0) + msg.closerPeers = dec.closerPeers.map(fromPbPeer) + msg.providerPeers = dec.providerPeers.map(fromPbPeer) + + if (dec.record?.length != null) { + msg.record = Libp2pRecord.deserialize(dec.record) + } + + return msg + } +} + +function toPbPeer (peer: PeerInfo): PBPeer { + const output: PBPeer = { + id: peer.id.toBytes(), + addrs: (peer.multiaddrs ?? []).map((m) => m.bytes), + connection: CONNECTION_TYPE.CONNECTED + } + + return output +} + +function fromPbPeer (peer: PBMessage.Peer): PeerInfo { + if (peer.id == null) { + throw new Error('Invalid peer in message') + } + + return { + id: peerIdFromBytes(peer.id), + multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)), + protocols: [] + } +} diff --git a/packages/kad-dht/src/network.ts b/packages/kad-dht/src/network.ts new file mode 100644 index 0000000000..ca525af221 --- /dev/null +++ b/packages/kad-dht/src/network.ts @@ -0,0 +1,206 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { abortableDuplex } from 'abortable-iterator' +import drain from 'it-drain' +import first from 'it-first' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Message } from './message/index.js' +import { + dialPeerEvent, + sendQueryEvent, + peerResponseEvent, + queryErrorEvent +} from './query/events.js' +import type { KadDHTComponents, QueryEvent, QueryOptions } from './index.js' +import type { Stream } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface NetworkInit { + protocol: string + lan: boolean +} + +interface NetworkEvents { + 'peer': CustomEvent +} + +/** + * Handle network operations for the dht + */ +export class Network extends EventEmitter implements Startable { + private readonly log: Logger + private readonly protocol: string + private running: boolean + private readonly components: KadDHTComponents + + /** + * Create a new network + */ + constructor (components: KadDHTComponents, init: NetworkInit) { + super() + + const { protocol, lan } = init + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:network`) + this.running = false + this.protocol = protocol + } + + /** + * Start the network + */ + async start (): Promise { + if (this.running) { + return + } + + this.running = true + } + + /** + * Stop all network activity + */ + async stop (): Promise { + this.running = false + } + + /** + * Is the network online? + */ + isStarted (): boolean { + return this.running + } + + /** + * Send a request and record RTT for latency measurements + */ + async * sendRequest (to: PeerId, msg: Message, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + return + } + + this.log('sending %s to %p', msg.type, to) + yield dialPeerEvent({ peer: to }, options) + yield sendQueryEvent({ to, type: msg.type }, options) + + let stream: Stream | undefined + + try { + const connection = await this.components.connectionManager.openConnection(to, options) + const stream = await connection.newStream(this.protocol, options) + + const response = await this._writeReadMessage(stream, msg.serialize(), options) + + yield peerResponseEvent({ + from: to, + messageType: response.type, + closer: response.closerPeers, + providers: response.providerPeers, + record: response.record + }, options) + } catch (err: any) { + yield queryErrorEvent({ from: to, error: err }, options) + } finally { + if (stream != null) { + stream.close() + } + } + } + + /** + * Sends a message without expecting an answer + */ + async * sendMessage (to: PeerId, msg: Message, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + return + } + + this.log('sending %s to %p', msg.type, to) + yield dialPeerEvent({ peer: to }, options) + yield sendQueryEvent({ to, type: msg.type }, options) + + let stream: Stream | undefined + + try { + const connection = await this.components.connectionManager.openConnection(to, options) + const stream = await connection.newStream(this.protocol, options) + + await this._writeMessage(stream, msg.serialize(), options) + + yield peerResponseEvent({ from: to, messageType: msg.type }, options) + } catch (err: any) { + yield queryErrorEvent({ from: to, error: err }, options) + } finally { + if (stream != null) { + stream.close() + } + } + } + + /** + * Write a message to the given stream + */ + async _writeMessage (stream: Duplex, Source>, msg: Uint8Array | Uint8ArrayList, options: AbortOptions): Promise { + if (options.signal != null) { + stream = abortableDuplex(stream, options.signal) + } + + await pipe( + [msg], + (source) => lp.encode(source), + stream, + drain + ) + } + + /** + * Write a message and read its response. + * If no response is received after the specified timeout + * this will error out. + */ + async _writeReadMessage (stream: Duplex, Source>, msg: Uint8Array | Uint8ArrayList, options: AbortOptions): Promise { + if (options.signal != null) { + stream = abortableDuplex(stream, options.signal) + } + + const res = await pipe( + [msg], + (source) => lp.encode(source), + stream, + (source) => lp.decode(source), + async source => { + const buf = await first(source) + + if (buf != null) { + return buf + } + + throw new CodeError('No message received', 'ERR_NO_MESSAGE_RECEIVED') + } + ) + + const message = Message.deserialize(res) + + // tell any listeners about new peers we've seen + message.closerPeers.forEach(peerData => { + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + message.providerPeers.forEach(peerData => { + this.dispatchEvent(new CustomEvent('peer', { + detail: peerData + })) + }) + + return message + } +} diff --git a/packages/kad-dht/src/peer-list/index.ts b/packages/kad-dht/src/peer-list/index.ts new file mode 100644 index 0000000000..7298a70c2b --- /dev/null +++ b/packages/kad-dht/src/peer-list/index.ts @@ -0,0 +1,54 @@ +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * A list of unique peers. + */ +export class PeerList { + private readonly list: PeerId[] + + constructor () { + this.list = [] + } + + /** + * Add a new peer. Returns `true` if it was a new one + */ + push (peerId: PeerId): boolean { + if (!this.has(peerId)) { + this.list.push(peerId) + + return true + } + + return false + } + + /** + * Check if this PeerInfo is already in here + */ + has (peerId: PeerId): boolean { + const match = this.list.find((i) => i.equals(peerId)) + return Boolean(match) + } + + /** + * Get the list as an array + */ + toArray (): PeerId[] { + return this.list.slice() + } + + /** + * Remove the last element + */ + pop (): PeerId | undefined { + return this.list.pop() + } + + /** + * The length of the list + */ + get length (): number { + return this.list.length + } +} diff --git a/packages/kad-dht/src/peer-list/peer-distance-list.ts b/packages/kad-dht/src/peer-list/peer-distance-list.ts new file mode 100644 index 0000000000..5cccd9a51d --- /dev/null +++ b/packages/kad-dht/src/peer-list/peer-distance-list.ts @@ -0,0 +1,92 @@ +import { compare as uint8ArrayCompare } from 'uint8arrays/compare' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import * as utils from '../utils.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +interface PeerDistance { + peerId: PeerId + distance: Uint8Array +} + +/** + * Maintains a list of peerIds sorted by distance from a DHT key. + */ +export class PeerDistanceList { + /** + * The DHT key from which distance is calculated + */ + private readonly originDhtKey: Uint8Array + + /** + * The maximum size of the list + */ + private readonly capacity: number + + private peerDistances: PeerDistance[] + + constructor (originDhtKey: Uint8Array, capacity: number) { + this.originDhtKey = originDhtKey + this.capacity = capacity + this.peerDistances = [] + } + + /** + * The length of the list + */ + get length (): number { + return this.peerDistances.length + } + + /** + * The peerIds in the list, in order of distance from the origin key + */ + get peers (): PeerId[] { + return this.peerDistances.map(pd => pd.peerId) + } + + /** + * Add a peerId to the list. + */ + async add (peerId: PeerId): Promise { + if (this.peerDistances.find(pd => pd.peerId.equals(peerId)) != null) { + return + } + + const dhtKey = await utils.convertPeerId(peerId) + const el = { + peerId, + distance: uint8ArrayXor(this.originDhtKey, dhtKey) + } + + this.peerDistances.push(el) + this.peerDistances.sort((a, b) => uint8ArrayCompare(a.distance, b.distance)) + this.peerDistances = this.peerDistances.slice(0, this.capacity) + } + + /** + * Indicates whether any of the peerIds passed as a parameter are closer + * to the origin key than the furthest peerId in the PeerDistanceList. + */ + async anyCloser (peerIds: PeerId[]): Promise { + if (peerIds.length === 0) { + return false + } + + if (this.length === 0) { + return true + } + + const dhtKeys = await Promise.all(peerIds.map(utils.convertPeerId)) + const furthestDistance = this.peerDistances[this.peerDistances.length - 1].distance + + for (const dhtKey of dhtKeys) { + const keyDistance = uint8ArrayXor(this.originDhtKey, dhtKey) + + if (uint8ArrayCompare(keyDistance, furthestDistance) < 0) { + return true + } + } + + return false + } +} diff --git a/packages/kad-dht/src/peer-routing/index.ts b/packages/kad-dht/src/peer-routing/index.ts new file mode 100644 index 0000000000..ab0af91cb6 --- /dev/null +++ b/packages/kad-dht/src/peer-routing/index.ts @@ -0,0 +1,317 @@ +import { keys } from '@libp2p/crypto' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { verifyRecord } from '@libp2p/record/validators' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { PeerDistanceList } from '../peer-list/peer-distance-list.js' +import { + queryErrorEvent, + finalPeerEvent, + valueEvent +} from '../query/events.js' +import * as utils from '../utils.js' +import type { KadDHTComponents, DHTRecord, DialPeerEvent, FinalPeerEvent, QueryEvent, Validators } from '../index.js' +import type { Network } from '../network.js' +import type { QueryManager, QueryOptions } from '../query/manager.js' +import type { QueryFunc } from '../query/types.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Logger } from '@libp2p/logger' + +export interface PeerRoutingInit { + routingTable: RoutingTable + network: Network + validators: Validators + queryManager: QueryManager + lan: boolean +} + +export class PeerRouting { + private readonly components: KadDHTComponents + private readonly log: Logger + private readonly routingTable: RoutingTable + private readonly network: Network + private readonly validators: Validators + private readonly queryManager: QueryManager + + constructor (components: KadDHTComponents, init: PeerRoutingInit) { + const { routingTable, network, validators, queryManager, lan } = init + + this.components = components + this.routingTable = routingTable + this.network = network + this.validators = validators + this.queryManager = queryManager + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:peer-routing`) + } + + /** + * Look if we are connected to a peer with the given id. + * Returns its id and addresses, if found, otherwise `undefined`. + */ + async findPeerLocal (peer: PeerId): Promise { + let peerData + const p = await this.routingTable.find(peer) + + if (p != null) { + this.log('findPeerLocal found %p in routing table', peer) + + try { + peerData = await this.components.peerStore.get(p) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (peerData == null) { + try { + peerData = await this.components.peerStore.get(peer) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (peerData != null) { + this.log('findPeerLocal found %p in peer store', peer) + + return { + id: peerData.id, + multiaddrs: peerData.addresses.map((address) => address.multiaddr), + protocols: [] + } + } + + return undefined + } + + /** + * Get a value via rpc call for the given parameters + */ + async * _getValueSingle (peer: PeerId, key: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + const msg = new Message(MESSAGE_TYPE.GET_VALUE, key, 0) + yield * this.network.sendRequest(peer, msg, options) + } + + /** + * Get the public key directly from a node + */ + async * getPublicKeyFromNode (peer: PeerId, options: AbortOptions = {}): AsyncGenerator { + const pkKey = utils.keyForPublicKey(peer) + + for await (const event of this._getValueSingle(peer, pkKey, options)) { + yield event + + if (event.name === 'PEER_RESPONSE' && event.record != null) { + const recPeer = await peerIdFromKeys(keys.marshalPublicKey({ bytes: event.record.value })) + + // compare hashes of the pub key + if (!recPeer.equals(peer)) { + throw new CodeError('public key does not match id', 'ERR_PUBLIC_KEY_DOES_NOT_MATCH_ID') + } + + if (recPeer.publicKey == null) { + throw new CodeError('public key missing', 'ERR_PUBLIC_KEY_MISSING') + } + + yield valueEvent({ from: peer, value: recPeer.publicKey }, options) + } + } + + throw new CodeError(`Node not responding with its public key: ${peer.toString()}`, 'ERR_INVALID_RECORD') + } + + /** + * Search for a peer with the given ID + */ + async * findPeer (id: PeerId, options: QueryOptions = {}): AsyncGenerator { + this.log('findPeer %p', id) + + // Try to find locally + const pi = await this.findPeerLocal(id) + + // already got it + if (pi != null) { + this.log('found local') + yield finalPeerEvent({ + from: this.components.peerId, + peer: pi + }, options) + return + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const findPeerQuery: QueryFunc = async function * ({ peer, signal }) { + const request = new Message(MESSAGE_TYPE.FIND_NODE, id.toBytes(), 0) + + for await (const event of self.network.sendRequest(peer, request, { + ...options, + signal + })) { + yield event + + if (event.name === 'PEER_RESPONSE') { + const match = event.closer.find((p) => p.id.equals(id)) + + // found the peer + if (match != null) { + yield finalPeerEvent({ from: event.from, peer: match }, options) + } + } + } + } + + let foundPeer = false + + for await (const event of this.queryManager.run(id.toBytes(), findPeerQuery, options)) { + if (event.name === 'FINAL_PEER') { + foundPeer = true + } + + yield event + } + + if (!foundPeer) { + yield queryErrorEvent({ from: this.components.peerId, error: new CodeError('Not found', 'ERR_NOT_FOUND') }, options) + } + } + + /** + * Kademlia 'node lookup' operation on a key, which could be a the + * bytes from a multihash or a peer ID + */ + async * getClosestPeers (key: Uint8Array, options: QueryOptions = {}): AsyncGenerator { + this.log('getClosestPeers to %b', key) + const id = await utils.convertBuffer(key) + const tablePeers = this.routingTable.closestPeers(id) + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + const peers = new PeerDistanceList(id, this.routingTable.kBucketSize) + await Promise.all(tablePeers.map(async peer => { await peers.add(peer) })) + + const getCloserPeersQuery: QueryFunc = async function * ({ peer, signal }) { + self.log('closerPeersSingle %s from %p', uint8ArrayToString(key, 'base32'), peer) + const request = new Message(MESSAGE_TYPE.FIND_NODE, key, 0) + + yield * self.network.sendRequest(peer, request, { + ...options, + signal + }) + } + + for await (const event of this.queryManager.run(key, getCloserPeersQuery, options)) { + yield event + + if (event.name === 'PEER_RESPONSE') { + await Promise.all(event.closer.map(async peerData => { await peers.add(peerData.id) })) + } + } + + this.log('found %d peers close to %b', peers.length, key) + + for (const peerId of peers.peers) { + try { + const peer = await this.components.peerStore.get(peerId) + + yield finalPeerEvent({ + from: this.components.peerId, + peer: { + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + } + }, options) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + } + + /** + * Query a particular peer for the value for the given key. + * It will either return the value or a list of closer peers. + * + * Note: The peerStore is updated with new addresses found for the given peer. + */ + async * getValueOrPeers (peer: PeerId, key: Uint8Array, options: AbortOptions = {}): AsyncGenerator { + for await (const event of this._getValueSingle(peer, key, options)) { + if (event.name === 'PEER_RESPONSE') { + if (event.record != null) { + // We have a record + try { + await this._verifyRecordOnline(event.record) + } catch (err: any) { + const errMsg = 'invalid record received, discarded' + this.log(errMsg) + + yield queryErrorEvent({ from: event.from, error: new CodeError(errMsg, 'ERR_INVALID_RECORD') }, options) + continue + } + } + } + + yield event + } + } + + /** + * Verify a record, fetching missing public keys from the network. + * Throws an error if the record is invalid. + */ + async _verifyRecordOnline (record: DHTRecord): Promise { + if (record.timeReceived == null) { + throw new CodeError('invalid record received', 'ERR_INVALID_RECORD') + } + + await verifyRecord(this.validators, new Libp2pRecord(record.key, record.value, record.timeReceived)) + } + + /** + * Get the nearest peers to the given query, but if closer + * than self + */ + async getCloserPeersOffline (key: Uint8Array, closerThan: PeerId): Promise { + const id = await utils.convertBuffer(key) + const ids = this.routingTable.closestPeers(id) + const output: PeerInfo[] = [] + + for (const peerId of ids) { + if (peerId.equals(closerThan)) { + continue + } + + try { + const peer = await this.components.peerStore.get(peerId) + + output.push({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + if (output.length > 0) { + this.log('getCloserPeersOffline found %d peer(s) closer to %b than %p', output.length, key, closerThan) + } else { + this.log('getCloserPeersOffline could not find peer closer to %b than %p', key, closerThan) + } + + return output + } +} diff --git a/packages/kad-dht/src/providers.ts b/packages/kad-dht/src/providers.ts new file mode 100644 index 0000000000..46e2703792 --- /dev/null +++ b/packages/kad-dht/src/providers.ts @@ -0,0 +1,286 @@ +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import cache from 'hashlru' +import { Key } from 'interface-datastore/key' +import Queue from 'p-queue' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import varint from 'varint' +import { + PROVIDERS_CLEANUP_INTERVAL, + PROVIDERS_VALIDITY, + PROVIDERS_LRU_CACHE_SIZE, + PROVIDER_KEY_PREFIX +} from './constants.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Datastore } from 'interface-datastore' +import type { CID } from 'multiformats' + +const log = logger('libp2p:kad-dht:providers') + +export interface ProvidersInit { + cacheSize?: number + /** + * How often invalid records are cleaned. (in seconds) + */ + cleanupInterval?: number + /** + * How long is a provider valid for. (in seconds) + */ + provideValidity?: number +} + +export interface ProvidersComponents { + datastore: Datastore +} + +/** + * This class manages known providers. + * A provider is a peer that we know to have the content for a given CID. + * + * Every `cleanupInterval` providers are checked if they + * are still valid, i.e. younger than the `provideValidity`. + * If they are not, they are deleted. + * + * To ensure the list survives restarts of the daemon, + * providers are stored in the datastore, but to ensure + * access is fast there is an LRU cache in front of that. + */ +export class Providers implements Startable { + private readonly components: ProvidersComponents + private readonly cache: ReturnType + private readonly cleanupInterval: number + private readonly provideValidity: number + private readonly syncQueue: Queue + private started: boolean + private cleaner?: NodeJS.Timer + + constructor (components: ProvidersComponents, init: ProvidersInit = {}) { + const { cacheSize, cleanupInterval, provideValidity } = init + + this.components = components + this.cleanupInterval = cleanupInterval ?? PROVIDERS_CLEANUP_INTERVAL + this.provideValidity = provideValidity ?? PROVIDERS_VALIDITY + this.cache = cache(cacheSize ?? PROVIDERS_LRU_CACHE_SIZE) + this.syncQueue = new Queue({ concurrency: 1 }) + this.started = false + } + + isStarted (): boolean { + return this.started + } + + /** + * Start the provider cleanup service + */ + async start (): Promise { + if (this.started) { + return + } + + this.started = true + + this.cleaner = setInterval( + () => { + this._cleanup().catch(err => { + log.error(err) + }) + }, + this.cleanupInterval + ) + } + + /** + * Release any resources. + */ + async stop (): Promise { + this.started = false + + if (this.cleaner != null) { + clearInterval(this.cleaner) + this.cleaner = undefined + } + } + + /** + * Check all providers if they are still valid, and if not delete them + */ + async _cleanup (): Promise { + await this.syncQueue.add(async () => { + const start = Date.now() + + let count = 0 + let deleteCount = 0 + const deleted = new Map>() + const batch = this.components.datastore.batch() + + // Get all provider entries from the datastore + const query = this.components.datastore.query({ prefix: PROVIDER_KEY_PREFIX }) + + for await (const entry of query) { + try { + // Add a delete to the batch for each expired entry + const { cid, peerId } = parseProviderKey(entry.key) + const time = readTime(entry.value).getTime() + const now = Date.now() + const delta = now - time + const expired = delta > this.provideValidity + + log('comparing: %d - %d = %d > %d %s', now, time, delta, this.provideValidity, expired ? '(expired)' : '') + + if (expired) { + deleteCount++ + batch.delete(entry.key) + const peers = deleted.get(cid) ?? new Set() + peers.add(peerId) + deleted.set(cid, peers) + } + count++ + } catch (err: any) { + log.error(err.message) + } + } + + // Commit the deletes to the datastore + if (deleted.size > 0) { + log('deleting %d / %d entries', deleteCount, count) + await batch.commit() + } else { + log('nothing to delete') + } + + // Clear expired entries from the cache + for (const [cid, peers] of deleted) { + const key = makeProviderKey(cid) + const provs = this.cache.get(key) + + if (provs != null) { + for (const peerId of peers) { + provs.delete(peerId) + } + + if (provs.size === 0) { + this.cache.remove(key) + } else { + this.cache.set(key, provs) + } + } + } + + log('Cleanup successful (%dms)', Date.now() - start) + }) + } + + /** + * Get the currently known provider peer ids for a given CID + */ + async _getProvidersMap (cid: CID): Promise> { + const cacheKey = makeProviderKey(cid) + let provs: Map = this.cache.get(cacheKey) + + if (provs == null) { + provs = await loadProviders(this.components.datastore, cid) + this.cache.set(cacheKey, provs) + } + + return provs + } + + /** + * Add a new provider for the given CID + */ + async addProvider (cid: CID, provider: PeerId): Promise { + await this.syncQueue.add(async () => { + log('%p provides %s', provider, cid) + const provs = await this._getProvidersMap(cid) + + log('loaded %s provs', provs.size) + const now = new Date() + provs.set(provider.toString(), now) + + const dsKey = makeProviderKey(cid) + this.cache.set(dsKey, provs) + + await writeProviderEntry(this.components.datastore, cid, provider, now) + }) + } + + /** + * Get a list of providers for the given CID + */ + async getProviders (cid: CID): Promise { + return this.syncQueue.add(async () => { + log('get providers for %s', cid) + const provs = await this._getProvidersMap(cid) + + return [...provs.keys()].map(peerIdStr => { + return peerIdFromString(peerIdStr) + }) + }, { + // no timeout is specified for this queue so it will not + // throw, but this is required to get the right return + // type since p-queue@7.3.4 + throwOnTimeout: true + }) + } +} + +/** + * Encode the given key its matching datastore key + */ +function makeProviderKey (cid: CID | string): string { + const cidStr = typeof cid === 'string' ? cid : uint8ArrayToString(cid.multihash.bytes, 'base32') + + return `${PROVIDER_KEY_PREFIX}/${cidStr}` +} + +/** + * Write a provider into the given store + */ +async function writeProviderEntry (store: Datastore, cid: CID, peer: PeerId, time: Date): Promise { + const dsKey = [ + makeProviderKey(cid), + '/', + peer.toString() + ].join('') + + const key = new Key(dsKey) + const buffer = Uint8Array.from(varint.encode(time.getTime())) + + await store.put(key, buffer) +} + +/** + * Parse the CID and provider peer id from the key + */ +function parseProviderKey (key: Key): { cid: string, peerId: string } { + const parts = key.toString().split('/') + + if (parts.length !== 5) { + throw new Error(`incorrectly formatted provider entry key in datastore: ${key.toString()}`) + } + + return { + cid: parts[3], + peerId: parts[4] + } +} + +/** + * Load providers for the given CID from the store + */ +async function loadProviders (store: Datastore, cid: CID): Promise> { + const providers = new Map() + const query = store.query({ prefix: makeProviderKey(cid) }) + + for await (const entry of query) { + const { peerId } = parseProviderKey(entry.key) + providers.set(peerId, readTime(entry.value)) + } + + return providers +} + +function readTime (buf: Uint8Array): Date { + return new Date(varint.decode(buf)) +} diff --git a/packages/kad-dht/src/query-self.ts b/packages/kad-dht/src/query-self.ts new file mode 100644 index 0000000000..300a364c02 --- /dev/null +++ b/packages/kad-dht/src/query-self.ts @@ -0,0 +1,163 @@ +import { setMaxListeners } from 'events' +import { logger, type Logger } from '@libp2p/logger' +import { anySignal } from 'any-signal' +import length from 'it-length' +import { pipe } from 'it-pipe' +import take from 'it-take' +import pDefer from 'p-defer' +import { pEvent } from 'p-event' +import { QUERY_SELF_INTERVAL, QUERY_SELF_TIMEOUT, K, QUERY_SELF_INITIAL_INTERVAL } from './constants.js' +import type { PeerRouting } from './peer-routing/index.js' +import type { RoutingTable } from './routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { DeferredPromise } from 'p-defer' + +export interface QuerySelfInit { + lan: boolean + peerRouting: PeerRouting + routingTable: RoutingTable + count?: number + interval?: number + initialInterval?: number + queryTimeout?: number + initialQuerySelfHasRun: DeferredPromise +} + +export interface QuerySelfComponents { + peerId: PeerId +} + +/** + * Receives notifications of new peers joining the network that support the DHT protocol + */ +export class QuerySelf implements Startable { + private readonly log: Logger + private readonly components: QuerySelfComponents + private readonly peerRouting: PeerRouting + private readonly routingTable: RoutingTable + private readonly count: number + private readonly interval: number + private readonly initialInterval: number + private readonly queryTimeout: number + private started: boolean + private timeoutId?: NodeJS.Timer + private controller?: AbortController + private initialQuerySelfHasRun?: DeferredPromise + private querySelfPromise?: DeferredPromise + + constructor (components: QuerySelfComponents, init: QuerySelfInit) { + const { peerRouting, lan, count, interval, queryTimeout, routingTable } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:query-self`) + this.started = false + this.peerRouting = peerRouting + this.routingTable = routingTable + this.count = count ?? K + this.interval = interval ?? QUERY_SELF_INTERVAL + this.initialInterval = init.initialInterval ?? QUERY_SELF_INITIAL_INTERVAL + this.queryTimeout = queryTimeout ?? QUERY_SELF_TIMEOUT + this.initialQuerySelfHasRun = init.initialQuerySelfHasRun + } + + isStarted (): boolean { + return this.started + } + + start (): void { + if (this.started) { + return + } + + this.started = true + clearTimeout(this.timeoutId) + this.timeoutId = setTimeout(() => { + this.querySelf() + .catch(err => { + this.log.error('error running self-query', err) + }) + }, this.initialInterval) + } + + stop (): void { + this.started = false + + if (this.timeoutId != null) { + clearTimeout(this.timeoutId) + } + + if (this.controller != null) { + this.controller.abort() + } + } + + async querySelf (): Promise { + if (!this.started) { + this.log('skip self-query because we are not started') + return + } + + if (this.querySelfPromise != null) { + this.log('joining existing self query') + return this.querySelfPromise.promise + } + + this.querySelfPromise = pDefer() + + if (this.routingTable.size === 0) { + // wait to discover at least one DHT peer + await pEvent(this.routingTable, 'peer:add') + } + + if (this.started) { + this.controller = new AbortController() + const signal = anySignal([this.controller.signal, AbortSignal.timeout(this.queryTimeout)]) + + // this controller will get used for lots of dial attempts so make sure we don't cause warnings to be logged + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, signal) + } + } catch {} // fails on node < 15.4 + + try { + this.log('run self-query, look for %d peers timing out after %dms', this.count, this.queryTimeout) + + const found = await pipe( + this.peerRouting.getClosestPeers(this.components.peerId.toBytes(), { + signal, + isSelfQuery: true + }), + (source) => take(source, this.count), + async (source) => length(source) + ) + + this.log('self-query ran successfully - found %d peers', found) + + if (this.initialQuerySelfHasRun != null) { + this.initialQuerySelfHasRun.resolve() + this.initialQuerySelfHasRun = undefined + } + } catch (err: any) { + this.log.error('self-query error', err) + } finally { + signal.clear() + } + } + + this.querySelfPromise.resolve() + this.querySelfPromise = undefined + + if (!this.started) { + return + } + + this.timeoutId = setTimeout(() => { + this.querySelf() + .catch(err => { + this.log.error('error running self-query', err) + }) + }, this.interval) + } +} diff --git a/packages/kad-dht/src/query/events.ts b/packages/kad-dht/src/query/events.ts new file mode 100644 index 0000000000..d838e2b47e --- /dev/null +++ b/packages/kad-dht/src/query/events.ts @@ -0,0 +1,149 @@ +import { CustomEvent } from '@libp2p/interfaces/events' +import { MESSAGE_TYPE_LOOKUP } from '../message/index.js' +import type { SendQueryEvent, PeerResponseEvent, DialPeerEvent, AddPeerEvent, ValueEvent, ProviderEvent, QueryErrorEvent, FinalPeerEvent, QueryOptions } from '../index.js' +import type { Message } from '../message/dht.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Libp2pRecord } from '@libp2p/record' + +export interface QueryEventFields { + to: PeerId + type: Message.MessageType +} + +export function sendQueryEvent (fields: QueryEventFields, options: QueryOptions = {}): SendQueryEvent { + const event: SendQueryEvent = { + ...fields, + name: 'SEND_QUERY', + type: 0, + messageName: fields.type, + messageType: MESSAGE_TYPE_LOOKUP.indexOf(fields.type.toString()) + } + + options.onProgress?.(new CustomEvent('kad-dht:query:send-query', { detail: event })) + + return event +} + +export interface PeerResponseEventField { + from: PeerId + messageType: Message.MessageType + closer?: PeerInfo[] + providers?: PeerInfo[] + record?: Libp2pRecord +} + +export function peerResponseEvent (fields: PeerResponseEventField, options: QueryOptions = {}): PeerResponseEvent { + const event: PeerResponseEvent = { + ...fields, + name: 'PEER_RESPONSE', + type: 1, + messageName: fields.messageType, + closer: (fields.closer != null) ? fields.closer : [], + providers: (fields.providers != null) ? fields.providers : [] + } + + options.onProgress?.(new CustomEvent('kad-dht:query:peer-response', { detail: event })) + + return event +} + +export interface FinalPeerEventFields { + from: PeerId + peer: PeerInfo +} + +export function finalPeerEvent (fields: FinalPeerEventFields, options: QueryOptions = {}): FinalPeerEvent { + const event: FinalPeerEvent = { + ...fields, + name: 'FINAL_PEER', + type: 2 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:final-peer', { detail: event })) + + return event +} + +export interface ErrorEventFields { + from: PeerId + error: Error +} + +export function queryErrorEvent (fields: ErrorEventFields, options: QueryOptions = {}): QueryErrorEvent { + const event: QueryErrorEvent = { + ...fields, + name: 'QUERY_ERROR', + type: 3 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:query-error', { detail: event })) + + return event +} + +export interface ProviderEventFields { + from: PeerId + providers: PeerInfo[] +} + +export function providerEvent (fields: ProviderEventFields, options: QueryOptions = {}): ProviderEvent { + const event: ProviderEvent = { + ...fields, + name: 'PROVIDER', + type: 4 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:provider', { detail: event })) + + return event +} + +export interface ValueEventFields { + from: PeerId + value: Uint8Array +} + +export function valueEvent (fields: ValueEventFields, options: QueryOptions = {}): ValueEvent { + const event: ValueEvent = { + ...fields, + name: 'VALUE', + type: 5 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:value', { detail: event })) + + return event +} + +export interface PeerEventFields { + peer: PeerId +} + +export function addPeerEvent (fields: PeerEventFields, options: QueryOptions = {}): AddPeerEvent { + const event: AddPeerEvent = { + ...fields, + name: 'ADD_PEER', + type: 6 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:add-peer', { detail: event })) + + return event +} + +export interface DialPeerEventFields { + peer: PeerId +} + +export function dialPeerEvent (fields: DialPeerEventFields, options: QueryOptions = {}): DialPeerEvent { + const event: DialPeerEvent = { + ...fields, + name: 'DIAL_PEER', + type: 7 + } + + options.onProgress?.(new CustomEvent('kad-dht:query:dial-peer', { detail: event })) + + return event +} diff --git a/packages/kad-dht/src/query/manager.ts b/packages/kad-dht/src/query/manager.ts new file mode 100644 index 0000000000..9eb2fe0210 --- /dev/null +++ b/packages/kad-dht/src/query/manager.ts @@ -0,0 +1,227 @@ +import { setMaxListeners } from 'events' +import { AbortError } from '@libp2p/interfaces/errors' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { PeerSet } from '@libp2p/peer-collections' +import { anySignal } from 'any-signal' +import merge from 'it-merge' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { + ALPHA, K, DEFAULT_QUERY_TIMEOUT +} from '../constants.js' +import { convertBuffer } from '../utils.js' +import { queryPath } from './query-path.js' +import type { QueryFunc } from './types.js' +import type { QueryEvent, QueryOptions as RootQueryOptions } from '../index.js' +import type { RoutingTable } from '../routing-table/index.js' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { DeferredPromise } from 'p-defer' + +export interface CleanUpEvents { + 'cleanup': CustomEvent +} + +export interface QueryManagerInit { + lan?: boolean + disjointPaths?: number + alpha?: number + initialQuerySelfHasRun: DeferredPromise + routingTable: RoutingTable +} + +export interface QueryManagerComponents { + peerId: PeerId + metrics?: Metrics +} + +export interface QueryOptions extends RootQueryOptions { + queryFuncTimeout?: number + isSelfQuery?: boolean +} + +/** + * Keeps track of all running queries + */ +export class QueryManager implements Startable { + private readonly components: QueryManagerComponents + private readonly lan: boolean + public disjointPaths: number + private readonly alpha: number + private readonly shutDownController: AbortController + private running: boolean + private queries: number + private metrics?: { + runningQueries: Metric + queryTime: Metric + } + + private readonly routingTable: RoutingTable + private initialQuerySelfHasRun?: DeferredPromise + + constructor (components: QueryManagerComponents, init: QueryManagerInit) { + const { lan = false, disjointPaths = K, alpha = ALPHA } = init + + this.components = components + this.disjointPaths = disjointPaths ?? K + this.running = false + this.alpha = alpha ?? ALPHA + this.lan = lan + this.queries = 0 + this.initialQuerySelfHasRun = init.initialQuerySelfHasRun + this.routingTable = init.routingTable + + // allow us to stop queries on shut down + this.shutDownController = new AbortController() + // make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, this.shutDownController.signal) + } + } catch {} // fails on node < 15.4 + } + + isStarted (): boolean { + return this.running + } + + /** + * Starts the query manager + */ + async start (): Promise { + this.running = true + + if (this.components.metrics != null && this.metrics == null) { + this.metrics = { + runningQueries: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_running_queries`), + queryTime: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_query_time_seconds`) + } + } + } + + /** + * Stops all queries + */ + async stop (): Promise { + this.running = false + + this.shutDownController.abort() + } + + async * run (key: Uint8Array, queryFunc: QueryFunc, options: QueryOptions = {}): AsyncGenerator { + if (!this.running) { + throw new Error('QueryManager not started') + } + + const stopQueryTimer = this.metrics?.queryTime.timer() + + if (options.signal == null) { + // don't let queries run forever + options.signal = AbortSignal.timeout(DEFAULT_QUERY_TIMEOUT) + + // this signal will get listened to for network requests, etc + // so make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, options.signal) + } + } catch {} // fails on node < 15.4 + } + + const signal = anySignal([this.shutDownController.signal, options.signal]) + + // this signal will get listened to for every invocation of queryFunc + // so make sure we don't make a lot of noise in the logs + try { + if (setMaxListeners != null) { + setMaxListeners(Infinity, signal) + } + } catch {} // fails on node < 15.4 + + const log = logger(`libp2p:kad-dht:${this.lan ? 'lan' : 'wan'}:query:` + uint8ArrayToString(key, 'base58btc')) + + // query a subset of peers up to `kBucketSize / 2` in length + const startTime = Date.now() + const cleanUp = new EventEmitter() + + try { + if (options.isSelfQuery !== true && this.initialQuerySelfHasRun != null) { + log('waiting for initial query-self query before continuing') + + await Promise.race([ + new Promise((resolve, reject) => { + signal.addEventListener('abort', () => { + reject(new AbortError('Query was aborted before self-query ran')) + }) + }), + this.initialQuerySelfHasRun.promise + ]) + + this.initialQuerySelfHasRun = undefined + } + + log('query:start') + this.queries++ + this.metrics?.runningQueries.update(this.queries) + + const id = await convertBuffer(key) + const peers = this.routingTable.closestPeers(id) + const peersToQuery = peers.slice(0, Math.min(this.disjointPaths, peers.length)) + + if (peers.length === 0) { + log.error('Running query with no peers') + return + } + + // make sure we don't get trapped in a loop + const peersSeen = new PeerSet() + + // Create query paths from the starting peers + const paths = peersToQuery.map((peer, index) => { + return queryPath({ + key, + startingPeer: peer, + ourPeerId: this.components.peerId, + signal, + query: queryFunc, + pathIndex: index, + numPaths: peersToQuery.length, + alpha: this.alpha, + cleanUp, + queryFuncTimeout: options.queryFuncTimeout, + log, + peersSeen, + onProgress: options.onProgress + }) + }) + + // Execute the query along each disjoint path and yield their results as they become available + for await (const event of merge(...paths)) { + yield event + + if (event.name === 'QUERY_ERROR') { + log('error', event.error) + } + } + } catch (err: any) { + if (!this.running && err.code === 'ERR_QUERY_ABORTED') { + // ignore query aborted errors that were thrown during query manager shutdown + } else { + throw err + } + } finally { + signal.clear() + + this.queries-- + this.metrics?.runningQueries.update(this.queries) + + if (stopQueryTimer != null) { + stopQueryTimer() + } + + cleanUp.dispatchEvent(new CustomEvent('cleanup')) + log('query:done in %dms', Date.now() - startTime) + } + } +} diff --git a/packages/kad-dht/src/query/query-path.ts b/packages/kad-dht/src/query/query-path.ts new file mode 100644 index 0000000000..2966b2947d --- /dev/null +++ b/packages/kad-dht/src/query/query-path.ts @@ -0,0 +1,254 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { anySignal } from 'any-signal' +import defer from 'p-defer' +import Queue from 'p-queue' +import { toString } from 'uint8arrays/to-string' +import { xor } from 'uint8arrays/xor' +import { convertPeerId, convertBuffer } from '../utils.js' +import { queryErrorEvent } from './events.js' +import type { CleanUpEvents } from './manager.js' +import type { QueryEvent, QueryOptions } from '../index.js' +import type { QueryFunc } from '../query/types.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Logger } from '@libp2p/logger' +import type { PeerSet } from '@libp2p/peer-collections' + +const MAX_XOR = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') + +export interface QueryPathOptions extends QueryOptions { + /** + * What are we trying to find + */ + key: Uint8Array + + /** + * Where we start our query + */ + startingPeer: PeerId + + /** + * Who we are + */ + ourPeerId: PeerId + + /** + * When to stop querying + */ + signal: AbortSignal + + /** + * The query function to run with each peer + */ + query: QueryFunc + + /** + * How many concurrent node/value lookups to run + */ + alpha: number + + /** + * How many concurrent node/value lookups to run + */ + pathIndex: number + + /** + * How many concurrent node/value lookups to run + */ + numPaths: number + + /** + * will emit a 'cleanup' event if the caller exits the for..await of early + */ + cleanUp: EventEmitter + + /** + * A timeout for queryFunc in ms + */ + queryFuncTimeout?: number + + /** + * Query log + */ + log: Logger + + /** + * Set of peers seen by this and other paths + */ + peersSeen: PeerSet +} + +/** + * Walks a path through the DHT, calling the passed query function for + * every peer encountered that we have not seen before + */ +export async function * queryPath (options: QueryPathOptions): AsyncGenerator { + const { key, startingPeer, ourPeerId, signal, query, alpha, pathIndex, numPaths, cleanUp, queryFuncTimeout, log, peersSeen } = options + // Only ALPHA node/value lookups are allowed at any given time for each process + // https://github.com/libp2p/specs/tree/master/kad-dht#alpha-concurrency-parameter-%CE%B1 + const queue = new Queue({ + concurrency: alpha + }) + + // perform lookups on kadId, not the actual value + const kadId = await convertBuffer(key) + + /** + * Adds the passed peer to the query queue if it's not us and no + * other path has passed through this peer + */ + function queryPeer (peer: PeerId, peerKadId: Uint8Array): void { + if (peer == null) { + return + } + + peersSeen.add(peer) + + const peerXor = BigInt('0x' + toString(xor(peerKadId, kadId), 'base16')) + + queue.add(async () => { + const signals = [signal] + + if (queryFuncTimeout != null) { + signals.push(AbortSignal.timeout(queryFuncTimeout)) + } + + const compoundSignal = anySignal(signals) + + try { + for await (const event of query({ + key, + peer, + signal: compoundSignal, + pathIndex, + numPaths + })) { + if (compoundSignal.aborted) { + return + } + + // if there are closer peers and the query has not completed, continue the query + if (event.name === 'PEER_RESPONSE') { + for (const closerPeer of event.closer) { + if (peersSeen.has(closerPeer.id)) { // eslint-disable-line max-depth + log('already seen %p in query', closerPeer.id) + continue + } + + if (ourPeerId.equals(closerPeer.id)) { // eslint-disable-line max-depth + log('not querying ourselves') + continue + } + + const closerPeerKadId = await convertPeerId(closerPeer.id) + const closerPeerXor = BigInt('0x' + toString(xor(closerPeerKadId, kadId), 'base16')) + + // only continue query if closer peer is actually closer + if (closerPeerXor > peerXor) { // eslint-disable-line max-depth + log('skipping %p as they are not closer to %b than %p', closerPeer.id, key, peer) + continue + } + + log('querying closer peer %p', closerPeer.id) + queryPeer(closerPeer.id, closerPeerKadId) + } + } + queue.emit('completed', event) + } + } catch (err: any) { + if (!signal.aborted) { + return queryErrorEvent({ + from: peer, + error: err + }, options) + } + } finally { + compoundSignal.clear() + } + }, { + // use xor value as the queue priority - closer peers should execute first + // subtract it from MAX_XOR because higher priority values execute sooner + + // @ts-expect-error this is supposed to be a Number but it's ok to use BigInts + // as long as all priorities are BigInts since we won't mix BigInts and Number + // values in arithmetic operations + priority: MAX_XOR - peerXor + }).catch(err => { + log.error(err) + }) + } + + // begin the query with the starting peer + queryPeer(startingPeer, await convertPeerId(startingPeer)) + + // yield results as they come in + yield * toGenerator(queue, signal, cleanUp, log) +} + +async function * toGenerator (queue: Queue, signal: AbortSignal, cleanUp: EventEmitter, log: Logger): AsyncGenerator { + let deferred = defer() + let running = true + const results: QueryEvent[] = [] + + const cleanup = (): void => { + if (!running) { + return + } + + log('clean up queue, results %d, queue size %d, pending tasks %d', results.length, queue.size, queue.pending) + + running = false + queue.clear() + results.splice(0, results.length) + } + + queue.on('completed', result => { + results.push(result) + deferred.resolve() + }) + queue.on('error', err => { + log('queue error', err) + cleanup() + deferred.reject(err) + }) + queue.on('idle', () => { + log('queue idle') + running = false + deferred.resolve() + }) + + // clear the queue and throw if the query is aborted + signal.addEventListener('abort', () => { + log('abort queue') + const wasRunning = running + cleanup() + + if (wasRunning) { + deferred.reject(new CodeError('Query aborted', 'ERR_QUERY_ABORTED')) + } + }) + + // the user broke out of the loop early, ensure we resolve the deferred result + // promise and clear the queue of any remaining jobs + cleanUp.addEventListener('cleanup', () => { + cleanup() + deferred.resolve() + }) + + while (running) { // eslint-disable-line no-unmodified-loop-condition + await deferred.promise + deferred = defer() + + // yield all available results + while (results.length > 0) { + const result = results.shift() + + if (result != null) { + yield result + } + } + } + + // yield any remaining results + yield * results +} diff --git a/packages/kad-dht/src/query/types.ts b/packages/kad-dht/src/query/types.ts new file mode 100644 index 0000000000..6c43d4ba72 --- /dev/null +++ b/packages/kad-dht/src/query/types.ts @@ -0,0 +1,22 @@ +import type { QueryEvent } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +export interface QueryContext { + // the key we are looking up + key: Uint8Array + // the current peer being queried + peer: PeerId + // if this signal emits an 'abort' event, any long-lived processes or requests started as part of this query should be terminated + signal: AbortSignal + // which disjoint path we are following + pathIndex: number + // the total number of disjoint paths being executed + numPaths: number +} + +/** + * Query function + */ +export interface QueryFunc { + (context: QueryContext): AsyncIterable +} diff --git a/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts b/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts new file mode 100644 index 0000000000..1f667e07db --- /dev/null +++ b/packages/kad-dht/src/routing-table/generated-prefix-list-browser.ts @@ -0,0 +1,1026 @@ +export default [ + 77591, 22417, 43971, 28421, 740, 29829, 71467, 228973, 196661, 78537, 27689, 36431, 44415, 14362, 19456, 106025, + 96308, 2882, 49509, 21149, 87173, 131409, 75844, 23676, 121838, 30291, 17492, 2953, 7564, 110620, 129477, 127283, + 53113, 72417, 165166, 109690, 21200, 102125, 24049, 71504, 90342, 25307, 72039, 26812, 26715, 32264, 133800, 71161, + 88956, 171987, 51779, 24425, 16671, 30251, 186294, 247761, 14202, 2121, 8465, 35024, 4876, 85917, 169730, 3638, + 256836, 96184, 943, 18678, 6583, 52907, 35807, 112254, 214097, 18796, 11595, 9243, 23554, 887, 268203, 382004, + 24590, 111335, 11625, 16619, 29039, 102425, 69006, 97976, 92362, 32552, 63717, 41433, 128974, 137630, 59943, 10019, + 13986, 35430, 33665, 108037, 43799, 43280, 38195, 29078, 58629, 18265, 14425, 46832, 235538, 40830, 77881, 110717, + 58937, 3463, 325358, 51300, 47623, 117252, 19007, 10170, 20540, 91237, 294813, 4951, 79841, 56232, 36270, 128547, + 69209, 66275, 100156, 32063, 73531, 34439, 80937, 28892, 44466, 88595, 216307, 32583, 49620, 16605, 82127, 45807, + 21630, 78726, 20235, 40163, 111007, 96926, 5567, 72083, 21665, 58844, 39419, 179767, 48328, 42662, 51550, 5251, + 37811, 49608, 81056, 50854, 55513, 20922, 18891, 197409, 164656, 32593, 71449, 220474, 58919, 85682, 67854, 13758, + 35066, 3565, 61905, 214793, 119572, 141419, 21504, 10302, 27354, 67003, 46131, 32668, 15165, 64871, 34450, 17821, + 2757, 11452, 34189, 5160, 12257, 85523, 560, 53385, 65887, 119549, 135620, 312353, 115979, 122356, 10867, 193231, + 124537, 54783, 90675, 120791, 4715, 142253, 50943, 17271, 43358, 25331, 4917, 120566, 34580, 12878, 33786, 160528, + 32523, 4869, 301307, 104817, 81491, 23276, 8832, 97911, 31265, 52065, 7998, 49622, 9715, 43998, 34091, 84587, + 20664, 69041, 29419, 53205, 10838, 58288, 116145, 6185, 5154, 141795, 35924, 21307, 144738, 43730, 12085, 8279, + 10002, 119, 133779, 199668, 72938, 31768, 39176, 67875, 38453, 9700, 44144, 4121, 116048, 41733, 12868, 82669, + 92308, 128, 34262, 11332, 7712, 90764, 36141, 13553, 71312, 77470, 117314, 96549, 49135, 23602, 54468, 28605, + 6327, 62308, 17171, 67531, 21319, 14105, 894, 107722, 46157, 8503, 51069, 100472, 45138, 15246, 14577, 35609, + 191464, 1757, 13364, 161349, 32067, 91705, 81144, 52339, 5408, 91066, 21983, 14157, 100545, 4372, 26630, 129112, + 1423, 29676, 213626, 4397, 88436, 99190, 6877, 49958, 26122, 114348, 60661, 29818, 293118, 50042, 179738, 16400, + 163423, 89627, 31040, 43973, 36638, 45952, 5153, 1894, 109322, 1898, 134021, 12402, 112077, 68309, 190269, 69866, + 31938, 107383, 11522, 105232, 11248, 14868, 39852, 71707, 186525, 16530, 38162, 106212, 11700, 5130, 16608, 26998, + 59586, 108399, 230033, 43683, 48135, 82179, 2073, 5015, 196684, 189293, 16378, 23452, 8301, 35640, 11632, 214551, + 29240, 57644, 33137, 91949, 55157, 52384, 117313, 5090, 17717, 89668, 49363, 82238, 241035, 66216, 29066, 184088, + 97206, 62820, 26595, 4241, 135635, 173672, 8202, 459, 71355, 146294, 29587, 3008, 135385, 141203, 14803, 6634, + 45094, 69362, 50925, 546, 51884, 62011, 83296, 234584, 44515, 56050, 89476, 87751, 19373, 12691, 149923, 19794, + 13833, 35846, 87557, 58339, 2884, 19145, 25647, 12224, 11024, 77338, 64608, 122297, 53025, 7205, 36189, 36294, + 170779, 21750, 7739, 173883, 75192, 35664, 224240, 113121, 30181, 26267, 27036, 117827, 92015, 106516, 55628, 203549, + 67949, 60462, 60844, 35911, 20457, 1820, 920, 19773, 8738, 73173, 181993, 38521, 98254, 76257, 46008, 92796, + 5384, 26868, 151566, 22124, 2411, 15919, 186872, 180021, 28099, 152961, 78811, 80237, 62352, 102653, 74259, 184890, + 16792, 123702, 224945, 29940, 19512, 75283, 14059, 112691, 92811, 233329, 20411, 138569, 53341, 109802, 50600, 134528, + 66747, 5529, 166531, 31578, 64732, 67189, 1596, 126357, 967, 167999, 206598, 109752, 119431, 207825, 78791, 91938, + 10301, 27311, 24233, 252343, 28831, 32812, 66002, 112267, 90895, 8786, 8095, 16824, 22866, 21813, 60507, 174833, + 19549, 130985, 117051, 52110, 6938, 81923, 123864, 38061, 919, 18680, 53534, 46739, 112893, 161529, 85429, 26761, + 11900, 81121, 91968, 15390, 217947, 56524, 1713, 6654, 37089, 85630, 138866, 61850, 16491, 75577, 16884, 98296, + 73523, 6140, 44645, 6062, 36366, 29844, 57946, 37932, 42472, 5266, 20834, 19309, 33753, 127182, 134259, 35810, + 41805, 45878, 312001, 14881, 47757, 49251, 120050, 44252, 3708, 25856, 107864, 120347, 1228, 36550, 41682, 34496, + 47025, 8393, 173365, 246526, 12894, 161607, 35670, 90785, 126572, 2095, 124731, 157033, 58694, 554, 12786, 9642, + 4817, 16136, 47864, 174698, 66992, 4639, 69284, 10625, 40710, 27763, 51738, 30404, 264105, 137904, 109882, 52487, + 42824, 57514, 2740, 10479, 146799, 107390, 16586, 88038, 174951, 9410, 16185, 44158, 5568, 40658, 46108, 12763, + 97385, 26175, 108859, 664, 230732, 67470, 46663, 14395, 50750, 141320, 93140, 15361, 47997, 55784, 6791, 307840, + 118569, 107326, 18056, 58281, 260415, 54691, 8790, 73332, 45633, 7511, 45674, 143373, 14031, 11799, 94491, 35646, + 96544, 14560, 26049, 32983, 25791, 83814, 42094, 231370, 63955, 139212, 2359, 169908, 3108, 183486, 105867, 28197, + 32941, 124968, 26402, 88267, 149768, 23053, 3078, 19091, 52924, 25383, 19209, 111548, 97361, 3959, 24880, 235061, + 9099, 24921, 161254, 151405, 20508, 7159, 34381, 20133, 11434, 74036, 19974, 34769, 36585, 1076, 22454, 17354, + 38727, 235160, 111547, 96454, 117448, 156940, 91330, 37299, 7310, 26915, 117060, 51369, 22620, 61861, 322264, 106850, + 111694, 15091, 2624, 40345, 300446, 177064, 1707, 27389, 54792, 327783, 132669, 183543, 59003, 17744, 20603, 151134, + 106923, 53084, 71803, 279424, 319816, 11579, 21946, 16728, 38274, 72711, 5085, 83391, 88646, 40159, 25027, 34680, + 10752, 12988, 54126, 30365, 18338, 100445, 230674, 44874, 84974, 143877, 123253, 139372, 28082, 91477, 144002, 13096, + 219729, 46016, 50029, 42377, 14601, 6660, 58244, 58978, 23918, 88206, 113611, 64452, 17541, 41032, 10942, 12021, + 49189, 10978, 40175, 37156, 10947, 71709, 106894, 112538, 57007, 137486, 150608, 152719, 40615, 7746, 279716, 13101, + 19524, 28708, 40578, 72320, 1096, 182051, 94527, 51275, 22833, 45164, 81917, 77519, 48508, 5421, 140302, 37845, + 149830, 5587, 27579, 5357, 428725, 248187, 6326, 206760, 39814, 32585, 89923, 44341, 288753, 284443, 96368, 31201, + 94189, 119504, 20359, 52073, 103216, 179, 27934, 32801, 96035, 34111, 34309, 101326, 18198, 20704, 210266, 37643, + 27880, 141873, 106000, 19414, 56614, 167714, 66483, 107885, 86602, 4379, 20796, 75467, 4987, 5017, 118857, 26003, + 34308, 114428, 29198, 6686, 29697, 73632, 3739, 69795, 16798, 41504, 7207, 30722, 21436, 36735, 28067, 28545, + 3239, 11221, 36031, 41889, 100010, 19247, 317673, 29495, 174554, 6424, 129725, 53845, 94986, 7955, 59676, 2604, + 191497, 19735, 102214, 62954, 23844, 11872, 179525, 261436, 34492, 428, 78404, 142035, 16747, 17246, 27578, 37021, + 33672, 57944, 26056, 135760, 2369, 61674, 122066, 31327, 19374, 157065, 40553, 130982, 69619, 71290, 38855, 72100, + 92903, 95940, 51422, 165999, 65713, 57873, 50726, 7288, 20272, 2081, 42326, 22624, 81120, 57914, 79352, 19447, + 1684, 72302, 11774, 302559, 161481, 96396, 13692, 414988, 3721, 79066, 56627, 46883, 21150, 11747, 12184, 5856, + 113458, 176117, 84416, 52079, 27933, 3354, 59765, 141359, 2212, 216309, 2555, 23458, 196722, 142463, 45701, 44548, + 28798, 19418, 215, 29916, 9396, 10574, 114226, 84475, 13520, 18694, 34056, 4524, 90302, 62930, 13539, 19407, + 77209, 7728, 38088, 9535, 2263, 23875, 183945, 17750, 26274, 67172, 10585, 28042, 22199, 7478, 51331, 66030, + 26774, 192929, 31434, 25850, 50197, 52926, 178158, 4679, 181256, 70184, 229600, 9959, 105594, 72158, 73974, 2726, + 35085, 78087, 23284, 35568, 51713, 155676, 5401, 27254, 11966, 17569, 223253, 71993, 103357, 111477, 55722, 30504, + 26034, 46774, 35392, 36285, 214814, 41143, 163465, 1051, 16094, 81044, 6636, 76489, 179102, 20712, 39178, 35683, + 125177, 54219, 30617, 52994, 25324, 50123, 2543, 87529, 58995, 10688, 125199, 12388, 60158, 125481, 131646, 7642, + 133350, 65874, 3438, 97277, 101450, 10075, 56344, 116821, 50778, 60547, 98016, 106135, 13859, 14255, 16300, 77373, + 173521, 8285, 45932, 37426, 4054, 114295, 55947, 7703, 39114, 52, 51119, 128135, 19714, 60715, 9554, 50492, + 88180, 2823, 118271, 52993, 122625, 97919, 23859, 37895, 25040, 33614, 32102, 20431, 3577, 9275, 15686, 43031, + 157741, 110358, 1884, 40291, 125391, 13736, 5008, 64881, 87336, 77381, 70711, 43032, 49155, 118587, 70494, 4318, + 10168, 30126, 12580, 10524, 280104, 104001, 145413, 2862, 84140, 6603, 106005, 13566, 12780, 11251, 42830, 571, + 179910, 82443, 13146, 469, 42714, 32591, 265217, 424024, 92553, 54721, 134100, 6007, 15242, 114681, 59030, 16718, + 85465, 200214, 85982, 55174, 165013, 23493, 56964, 82529, 109150, 32706, 27568, 82442, 5350, 14976, 13165, 44890, + 60021, 21343, 33978, 17264, 4655, 22328, 27819, 75730, 16567, 55483, 14510, 17926, 45827, 150609, 3704, 7385, + 272531, 161543, 76904, 122163, 52405, 2039, 19165, 41623, 14423, 228354, 3369, 176360, 85491, 7122, 35789, 303724, + 4465, 13628, 2233, 55311, 118771, 20713, 10006, 221519, 45115, 71021, 35650, 29775, 7337, 10864, 20665, 21142, + 1746, 15080, 1624, 32449, 10905, 105743, 229797, 7701, 3940, 22997, 178467, 57208, 389057, 39683, 59403, 63344, + 63125, 54847, 69691, 18336, 56448, 3362, 37202, 18282, 29648, 138224, 35867, 10495, 5911, 28814, 26653, 31514, + 176702, 26550, 45621, 11734, 4525, 40543, 73944, 121080, 27858, 155561, 14887, 44670, 30742, 8796, 107455, 113472, + 56369, 75581, 183777, 240095, 133699, 153299, 8768, 160464, 26058, 49078, 103971, 21875, 71486, 44888, 17156, 9678, + 89541, 123019, 102337, 3972, 83930, 21245, 87852, 109660, 287918, 183019, 686, 10100, 39177, 283941, 11274, 24736, + 26793, 26214, 25995, 77011, 141580, 4070, 23742, 46285, 46632, 30700, 26669, 19056, 35951, 115575, 174034, 56097, + 35463, 87425, 24575, 44245, 38701, 82317, 85922, 281616, 100333, 147697, 61503, 7730, 84330, 8530, 59917, 61597, + 17173, 9092, 32658, 90288, 193136, 39023, 20381, 56654, 31132, 7779, 1919, 1375, 117128, 30819, 11169, 40938, + 23935, 115201, 101155, 151034, 4835, 11231, 74550, 89388, 59951, 91704, 107312, 167882, 115062, 12732, 72738, 88703, + 464019, 158267, 57995, 60496, 737, 14371, 123867, 4174, 243339, 159946, 7568, 16025, 134556, 110916, 38103, 191, + 80226, 88794, 29688, 27230, 10454, 76308, 57647, 77409, 113483, 66864, 14745, 19808, 12023, 46583, 84805, 16015, + 17102, 2231, 20611, 3547, 95740, 250131, 34559, 108894, 8498, 15853, 159169, 148920, 20942, 2813, 93160, 45188, + 210613, 45531, 52587, 149062, 39782, 28194, 57849, 60965, 84954, 89766, 84453, 100927, 16501, 27658, 165311, 103841, + 54192, 207341, 19558, 20084, 319622, 5672, 205467, 98462, 61849, 36279, 13609, 147177, 24726, 165015, 209489, 59591, + 31157, 6551, 117580, 75060, 141146, 277310, 21072, 22023, 106474, 63041, 137443, 122965, 68371, 5383, 42146, 98961, + 113467, 30863, 23794, 4843, 99630, 30392, 82679, 13699, 241612, 33601, 93146, 24319, 18643, 32155, 95669, 40440, + 15333, 34089, 67799, 142144, 58245, 38633, 114531, 117400, 77861, 188726, 5507, 2568, 8853, 10987, 107222, 2663, + 2421, 11530, 13345, 30075, 41785, 118661, 104786, 17459, 12490, 16281, 71936, 193555, 17431, 5944, 71758, 26485, + 77317, 20803, 367167, 158, 7362, 93430, 11735, 172445, 46002, 11532, 54482, 930, 62911, 2235, 23004, 179236, + 4764, 101859, 208113, 22477, 55163, 95579, 14098, 67320, 162556, 90709, 156949, 3826, 57492, 4025, 34092, 87442, + 104565, 6718, 186015, 28214, 14209, 10039, 107186, 233912, 58877, 81637, 55265, 39828, 6194, 145813, 50831, 105849, + 4974, 88319, 122296, 10272, 197216, 95714, 51540, 72418, 23324, 91555, 8743, 140452, 250249, 51666, 34124, 7229, + 38592, 129641, 78169, 174242, 22464, 149964, 51450, 14034, 10026, 95376, 26190, 120062, 14401, 8700, 265, 31386, + 143573, 7203, 229889, 61567, 4227, 140981, 2466, 72052, 10787, 10062, 30958, 6099, 38471, 30103, 23202, 208101, + 70847, 467, 58934, 32271, 32984, 36637, 24107, 30771, 17109, 73353, 13650, 2098, 157040, 67366, 66904, 106018, + 265380, 107238, 18535, 44025, 32681, 144983, 62505, 91295, 56120, 3082, 77508, 10322, 63023, 36700, 81885, 224127, + 16721, 45023, 239261, 111272, 13852, 7866, 149243, 204199, 32309, 22084, 42029, 38316, 126644, 104973, 14406, 43454, + 67322, 61310, 15789, 40285, 24026, 181047, 6301, 70927, 23319, 115823, 27248, 66693, 115875, 278566, 63007, 146844, + 56841, 59007, 87368, 180001, 22370, 42114, 80605, 12022, 10374, 308, 25079, 14689, 12618, 63368, 7936, 264973, + 212291, 136713, 95999, 105801, 18965, 32075, 48700, 52230, 35119, 96912, 32992, 8586, 16606, 101333, 101812, 14969, + 39930, 759, 193090, 27387, 42914, 12937, 5058, 62646, 64528, 38624, 25743, 37502, 3716, 4435, 30352, 178687, + 26461, 132611, 42002, 138442, 35833, 59582, 16345, 8048, 60319, 49349, 309, 47800, 49739, 90482, 26405, 34470, + 63786, 32479, 85028, 39866, 47846, 11649, 23934, 29466, 2816, 42864, 31828, 7410, 74885, 49632, 47629, 111801, + 90749, 19536, 18767, 105764, 59606, 21223, 10746, 76298, 22220, 39408, 7190, 79654, 64856, 11602, 82156, 272765, + 17079, 70089, 245473, 51813, 184407, 384678, 1576, 122249, 5064, 27481, 6188, 25790, 74361, 27541, 318284, 45430, + 31488, 620, 93579, 45723, 192118, 22670, 51913, 4162, 70244, 35966, 26397, 16199, 50899, 209613, 121702, 287507, + 2993, 36101, 132229, 67345, 33062, 76295, 118628, 78705, 52316, 34375, 107083, 107454, 44863, 127561, 33964, 3073, + 154010, 190914, 55967, 39074, 6272, 31047, 5550, 41123, 26154, 98638, 47110, 19998, 148091, 50229, 31329, 59900, + 195442, 19106, 61347, 73497, 70015, 682, 45850, 25776, 38022, 148951, 6288, 37411, 232526, 109277, 27286, 32342, + 9262, 5220, 16651, 23175, 46740, 129438, 78614, 121925, 66914, 88710, 127952, 5563, 21500, 34521, 10739, 14863, + 191006, 62956, 17359, 16749, 67027, 56284, 69134, 43301, 35039, 58883, 54466, 60823, 404451, 75743, 59856, 86979, + 7923, 34273, 83785, 32142, 7693, 268986, 197428, 282681, 17049, 22346, 22990, 92245, 107180, 3357, 37104, 96724, + 49153, 7683, 31197, 43267, 82231, 164276, 23696, 20848, 188364, 22309, 24821, 158707, 1018, 22514, 70922, 27792, + 45589, 59709, 10765, 736, 35218, 63479, 51987, 24275, 63588, 55361, 92929, 81964, 4658, 20122, 12330, 44058, + 13065, 311456, 72224, 8337, 211229, 38979, 22590, 138478, 52757, 32595, 133600, 8838, 31549, 94412, 43391, 90056, + 1585, 94802, 127271, 6223, 31889, 137038, 132910, 2165, 57616, 230152, 6080, 10748, 36737, 74579, 134062, 50525, + 180532, 119270, 34556, 76155, 82394, 52595, 29258, 31435, 87820, 67996, 26943, 183878, 38007, 2410, 13526, 180297, + 69856, 3503, 187396, 167700, 7838, 16701, 9199, 56267, 3661, 37407, 65994, 23767, 5708, 62508, 221700, 67088, + 86978, 46776, 84434, 32088, 5612, 9149, 88244, 21685, 95151, 46750, 189612, 2979, 506311, 2594, 3628, 40074, + 105039, 78243, 28523, 6651, 38058, 71999, 30992, 12764, 68261, 108991, 6165, 26450, 61961, 13400, 22426, 7490, + 60890, 109623, 2070, 12958, 50355, 67979, 257096, 7213, 42578, 52121, 35716, 65461, 7516, 124758, 39268, 302, + 64712, 14977, 1467, 219452, 2840, 34229, 11121, 21602, 19270, 63574, 8024, 1532, 17331, 79839, 78885, 52029, + 180767, 57957, 6069, 91265, 61380, 55767, 8927, 32881, 287603, 22149, 35029, 68876, 6428, 199567, 46926, 13412, + 104132, 21434, 366616, 45060, 110046, 81924, 128910, 45886, 52821, 130416, 29416, 77342, 21762, 67329, 121432, 79924, + 11724, 38625, 81006, 102033, 28338, 13326, 3250, 82056, 82526, 38212, 21112, 12382, 111495, 3263, 7414, 86274, + 93490, 40844, 30224, 45212, 24019, 48411, 71367, 24941, 76729, 57776, 3769, 38114, 202019, 197745, 31953, 237533, + 33270, 201580, 255648, 100798, 44741, 32241, 98468, 106931, 10085, 15090, 170358, 33154, 66787, 18819, 69760, 25061, + 234005, 82660, 6295, 131975, 16874, 9076, 4094, 25005, 17740, 40908, 19533, 220019, 44330, 99792, 50040, 19619, + 13950, 55228, 24423, 31253, 95308, 103177, 184795, 28590, 82285, 5059, 3210, 75525, 49894, 70007, 56178, 10580, + 36051, 139681, 21617, 98736, 3555, 106306, 164189, 37352, 63915, 47824, 24883, 145530, 61904, 28444, 11483, 19837, + 145446, 30420, 112972, 85939, 11835, 191233, 2262, 20705, 58630, 1753, 148334, 1197, 144714, 6887, 11223, 107667, + 60879, 77914, 4151, 57417, 81594, 96681, 169430, 1784, 20444, 95138, 254041, 27038, 596, 7117, 72808, 13759, + 3353, 126776, 21074, 55322, 27081, 36942, 39547, 139830, 179275, 4453, 713, 8722, 71399, 19204, 25785, 22794, + 23923, 104114, 11291, 25458, 102309, 88396, 75288, 230440, 206396, 104551, 58447, 130857, 37247, 94734, 31548, 176529, + 226077, 65159, 20104, 10096, 66881, 94191, 237909, 27109, 37404, 1520, 27421, 25220, 113003, 23423, 24884, 50585, + 6286, 231877, 150800, 11789, 3226, 90004, 60642, 5053, 202400, 61442, 132531, 175329, 57138, 30116, 103847, 9973, + 75367, 16452, 32360, 59119, 21246, 10191, 164804, 23305, 61051, 37348, 154530, 13214, 5468, 50403, 66754, 130976, + 50559, 80515, 14436, 155492, 84017, 5472, 43107, 41240, 2890, 90431, 70188, 382, 76234, 48040, 50211, 281038, + 237007, 32115, 142178, 1536, 22761, 96429, 1811, 31243, 1679, 49143, 55209, 17402, 235054, 61494, 7462, 77030, + 34925, 87609, 78002, 9499, 9027, 73289, 201078, 101379, 63544, 27666, 5469, 10642, 30029, 49816, 132979, 95620, + 58086, 351930, 116300, 2110, 2043, 30845, 6154, 11279, 16727, 4122, 2277, 27281, 4971, 3650, 39060, 61970, + 65951, 39674, 75686, 38151, 11370, 130809, 177895, 32665, 63725, 122267, 7857, 39618, 118483, 44792, 157755, 178624, + 136994, 24260, 41308, 22471, 12404, 21707, 12486, 30473, 52781, 50246, 20247, 39065, 909, 56825, 103158, 128603, + 31542, 1089, 41935, 32744, 12428, 37963, 84420, 33134, 72921, 208449, 42622, 168151, 127335, 147107, 46699, 38216, + 12591, 94342, 85814, 31423, 24944, 2605, 87542, 67473, 192551, 4496, 56321, 91819, 17630, 6300, 256183, 114569, + 202090, 33209, 35289, 34897, 24967, 40520, 43470, 5344, 10199, 34810, 14283, 10381, 10017, 62923, 49924, 23233, + 64539, 13051, 35686, 19698, 11570, 135555, 120868, 44924, 87065, 52318, 52335, 47586, 140906, 245885, 109834, 78668, + 9065, 46990, 25258, 72022, 61243, 40838, 4545, 146387, 10537, 11557, 17470, 36930, 68104, 46711, 24264, 79401, + 81043, 18225, 120488, 24746, 84338, 81652, 28266, 13776, 21878, 46973, 1047, 230465, 73357, 95777, 24973, 210160, + 62210, 58404, 110633, 169651, 6937, 41870, 9909, 26822, 191062, 76553, 27519, 96256, 239070, 2478, 205678, 67955, + 58532, 20601, 50120, 19148, 78501, 195724, 110740, 8249, 109665, 27446, 30568, 57631, 31425, 49752, 32820, 65504, + 50079, 3663, 102256, 219898, 23849, 211315, 14645, 4359, 91767, 9528, 12449, 49366, 7941, 49763, 107848, 8930, + 27086, 50686, 9744, 10447, 81935, 39513, 46514, 1670, 29229, 6172, 22312, 137280, 97759, 9806, 14445, 22976, + 56458, 73391, 34983, 93760, 174219, 52573, 33149, 59747, 2429, 136277, 75123, 165263, 91040, 7446, 57632, 48633, + 97140, 246081, 84766, 151684, 79918, 93268, 120346, 54059, 54875, 77858, 32996, 103590, 45276, 11968, 19600, 25849, + 17159, 132907, 42828, 16817, 4913, 99462, 103303, 27395, 5737, 74184, 20749, 21160, 14377, 77062, 131403, 158735, + 10999, 27799, 77785, 9320, 34366, 51593, 61070, 33746, 47048, 29268, 36675, 30262, 53297, 9832, 82000, 20188, + 122292, 39917, 7331, 18160, 68301, 185935, 134830, 15031, 4935, 10004, 165845, 185534, 46923, 30109, 44134, 122631, + 18874, 22903, 112790, 26561, 18549, 348902, 82871, 140345, 255565, 135390, 63556, 103747, 145055, 179600, 145662, 296111, + 61661, 211987, 23952, 52342, 126343, 48450, 32919, 44277, 82185, 9591, 62139, 205363, 376969, 394874, 108461, 18040, + 120885, 14798, 39863, 16571, 16794, 58271, 81025, 55206, 14640, 118656, 6361, 44092, 85970, 6262, 153863, 108244, + 180200, 72264, 79947, 38044, 10050, 5735, 61221, 80712, 5471, 115689, 11391, 11661, 184257, 20010, 60116, 30320, + 19327, 134598, 45455, 27542, 18004, 125092, 452272, 1549, 91523, 46567, 180063, 156026, 2608, 11174, 58848, 37788, + 65907, 80194, 30490, 5786, 40775, 119519, 106241, 11323, 156297, 8425, 61495, 2617, 29675, 2425, 59886, 112582, + 49142, 59618, 4863, 50597, 86710, 50650, 168632, 27693, 85641, 83643, 18993, 25768, 84284, 28090, 93592, 36627, + 312804, 43381, 9887, 9402, 100931, 97165, 3311, 173330, 66805, 28935, 4963, 184460, 3201, 78102, 19126, 21607, + 37496, 24938, 22615, 16153, 32862, 134792, 153318, 61120, 6067, 2812, 12826, 12792, 23825, 37559, 64662, 202250, + 102694, 155488, 85881, 149193, 46233, 65383, 15521, 106982, 11358, 176786, 25752, 39717, 34208, 24510, 32464, 77742, + 39371, 72028, 138229, 60688, 71386, 102834, 132477, 2208, 11548, 63670, 271279, 28351, 30338, 38620, 32491, 99845, + 143885, 152266, 13252, 2825, 178663, 108097, 1775, 78201, 14897, 113573, 163346, 62292, 171129, 22183, 96598, 38733, + 64971, 166776, 117445, 9968, 146393, 44677, 74867, 20908, 97328, 12761, 25656, 26785, 9148, 112344, 26115, 99176, + 110121, 22437, 49547, 6180, 79320, 5835, 31392, 43328, 33377, 75870, 119860, 69497, 80273, 7325, 155219, 43167, + 111173, 28347, 20222, 3763, 71752, 55041, 47252, 14618, 28088, 15012, 97805, 194698, 54636, 2036, 41349, 6173, + 96604, 61530, 51859, 43782, 13361, 24334, 22668, 24792, 7070, 23441, 16789, 3209, 36211, 208475, 26242, 32880, + 122181, 182407, 21444, 31060, 88459, 29929, 77907, 12716, 10934, 97005, 20599, 31690, 8403, 58445, 30303, 22700, + 10336, 86731, 103115, 337709, 72556, 46788, 112566, 47684, 67089, 53548, 36874, 56487, 41387, 125985, 26893, 40071, + 106683, 73712, 18787, 40105, 72992, 67246, 137276, 50802, 36790, 70328, 138827, 22466, 39263, 183295, 29858, 50975, + 9322, 57397, 10654, 24364, 30383, 55799, 41600, 23584, 127295, 296610, 129078, 143558, 244131, 86397, 36049, 1085, + 80677, 3820, 108139, 5476, 34767, 24683, 7758, 13060, 7239, 131671, 250593, 59556, 103392, 29810, 4188, 252323, + 39404, 116877, 7651, 43600, 40338, 13554, 157253, 39196, 25978, 144387, 61211, 234, 50104, 6129, 10449, 93777, + 9240, 356378, 274148, 4439, 72970, 3724, 147770, 78680, 62570, 115877, 40027, 40547, 36817, 224392, 64609, 34795, + 165027, 67440, 2477, 37206, 23431, 50754, 164797, 46018, 94995, 170982, 27051, 7957, 22767, 3674, 27900, 56419, + 18930, 60701, 41302, 2692, 84749, 339721, 61996, 111094, 80221, 50129, 1045, 8153, 62945, 19202, 8250, 37208, + 37418, 32560, 79477, 41106, 88569, 33963, 36693, 5892, 30570, 1581, 66471, 49647, 11922, 160717, 29442, 5643, + 114865, 82962, 95982, 132098, 22633, 22838, 94726, 54556, 28566, 205039, 162340, 33216, 16849, 35847, 221339, 94851, + 26533, 71469, 1805, 3804, 12935, 45483, 71020, 36310, 65381, 192960, 34240, 35165, 59773, 1248, 46954, 155332, + 96864, 4246, 388800, 16129, 57133, 74592, 44807, 442014, 38203, 42574, 80818, 91592, 26377, 36424, 65760, 977, + 77387, 22628, 147610, 28018, 30561, 98454, 6969, 119628, 63648, 18170, 36854, 26601, 64018, 22027, 37279, 51395, + 152934, 21153, 9430, 58760, 194742, 5330, 55115, 34158, 28917, 174111, 13171, 122326, 1526, 43896, 66094, 25325, + 4234, 148354, 11450, 275, 18999, 112191, 44365, 22723, 68409, 8733, 57746, 96565, 75007, 14196, 108844, 29475, + 88599, 177563, 100792, 106156, 86323, 93726, 14248, 135341, 194131, 40126, 47099, 14779, 8272, 39597, 95983, 171398, + 65882, 28052, 10393, 47213, 40689, 22120, 72212, 106829, 34964, 109146, 753, 648, 21660, 30047, 17527, 181025, + 5619, 145357, 4085, 216883, 9359, 186951, 24779, 53931, 24545, 36197, 223296, 62628, 168101, 4243, 107313, 30321, + 26642, 13049, 51059, 31027, 107912, 807, 73550, 26551, 84369, 122422, 165872, 49754, 74213, 234264, 33151, 52014, + 33100, 87183, 22365, 52500, 40013, 23302, 5652, 72723, 21404, 26107, 48434, 587, 94049, 168493, 96418, 32871, + 70860, 31709, 25128, 443, 71597, 166253, 15670, 70994, 26341, 133675, 28280, 75491, 54756, 47955, 56028, 26182, + 11952, 113272, 472197, 64640, 110753, 17919, 337, 50642, 22576, 142, 87371, 53391, 93210, 126694, 15285, 19642, + 85667, 14148, 1506, 42092, 52962, 33243, 11970, 20734, 135843, 57044, 58880, 13002, 219134, 22876, 64754, 232519, + 4257, 43120, 321573, 24799, 64526, 124728, 52579, 81472, 70831, 276848, 17403, 74359, 23021, 182101, 74597, 23744, + 148267, 12055, 7976, 5349, 11772, 67540, 167347, 65318, 18720, 127832, 108238, 22828, 90233, 9987, 259080, 118185, + 73209, 79270, 13775, 90100, 137742, 90799, 70569, 15699, 19961, 9087, 67475, 57872, 39731, 8810, 134897, 131868, + 146849, 19898, 3334, 2281, 167061, 91073, 60356, 467742, 74712, 188, 53179, 137679, 92769, 29241, 9537, 132595, + 80119, 1041, 88962, 5976, 40171, 44911, 102859, 139059, 104558, 98987, 47761, 19272, 71472, 113864, 175377, 73338, + 10857, 23402, 23758, 1591, 139864, 5644, 4076, 118760, 16427, 134198, 18853, 20291, 100849, 37423, 22038, 36677, + 19071, 195521, 57445, 11069, 31869, 55718, 66882, 148490, 44, 41296, 75242, 49704, 166810, 9906, 20943, 122258, + 49112, 105667, 15969, 10344, 6408, 187694, 21399, 72742, 58970, 14867, 14376, 81889, 41856, 23225, 15042, 56993, + 16074, 131389, 74276, 72407, 53875, 383108, 53597, 37363, 68993, 44854, 122548, 430927, 198279, 38430, 80409, 12245, + 2981, 628, 2818, 17760, 37437, 238229, 7968, 46892, 2200, 3730, 34190, 65983, 37959, 112291, 87850, 70827, + 6522, 20750, 73913, 111621, 41652, 19587, 2780, 58668, 25916, 85259, 18200, 168962, 95781, 42445, 102050, 7776, + 57662, 103313, 47742, 96358, 41964, 66174, 100396, 29069, 204735, 19679, 27978, 7479, 40264, 22534, 61183, 36081, + 107436, 58223, 14680, 23002, 101311, 24716, 124108, 12908, 5646, 31750, 40380, 14215, 232799, 102772, 14122, 96775, + 61398, 50917, 12096, 149880, 67833, 598749, 124194, 155871, 49216, 790, 14677, 65319, 56917, 7440, 145744, 95701, + 12206, 49405, 129269, 76199, 45732, 9767, 11058, 9047, 210885, 11051, 7392, 26307, 2130, 8132, 147526, 20802, + 232698, 115660, 50060, 59789, 57344, 107623, 80343, 112676, 23291, 9866, 160971, 34032, 118291, 15719, 59730, 164911, + 28975, 2659, 58046, 78480, 21854, 66209, 53863, 109085, 116045, 29021, 46481, 107552, 22130, 18764, 70254, 31272, + 11300, 52460, 43933, 84738, 20721, 53869, 190840, 79673, 105300, 7561, 321817, 66924, 13940, 33281, 101046, 183181, + 32176, 71878, 5678, 62924, 79535, 56646, 40303, 19559, 27703, 93042, 73368, 42187, 3670, 37376, 46440, 7023, + 36816, 109628, 20680, 5940, 276440, 275233, 170848, 112093, 136996, 14984, 20226, 111441, 77693, 112960, 48577, 39370, + 55707, 50314, 123404, 26570, 54281, 61372, 123391, 4857, 35928, 246740, 132507, 106646, 44241, 7196, 92258, 9825, + 37688, 51197, 303141, 5590, 15476, 132986, 10955, 85782, 34486, 26696, 7991, 28813, 18858, 39546, 11703, 11365, + 38185, 5716, 93555, 11925, 40121, 60002, 6985, 10976, 171384, 3887, 43394, 13337, 56346, 6381, 252336, 39573, + 75042, 53711, 1028, 31781, 44295, 95925, 131713, 7214, 68125, 43571, 70954, 213234, 1628, 8760, 13391, 65485, + 17320, 56038, 1710, 25248, 60803, 57399, 19839, 3870, 326, 281556, 50945, 72400, 21460, 316244, 75619, 56246, + 98775, 481, 13513, 55765, 50427, 7388, 123519, 32929, 57908, 27124, 61316, 101097, 57467, 30228, 48792, 10788, + 20402, 37318, 50526, 155730, 34456, 158065, 145305, 17832, 43733, 64052, 4506, 35072, 205355, 177028, 184004, 187081, + 68616, 35938, 83703, 10367, 36892, 93186, 260137, 51934, 89970, 4985, 23445, 26755, 21558, 7948, 78741, 23376, + 124405, 85594, 68596, 57536, 49351, 12619, 56593, 132668, 99924, 109728, 71844, 71935, 196018, 65464, 17617, 14987, + 89701, 143773, 33997, 8687, 22701, 33258, 2914, 4436, 72108, 85610, 9671, 49067, 2327, 82988, 1361, 1672, + 44033, 35777, 30269, 24057, 10605, 82236, 616, 15793, 13919, 47249, 112086, 116698, 9484, 80207, 90574, 33304, + 68624, 93127, 56101, 42210, 160929, 4827, 38995, 38095, 4701, 125119, 5027, 33680, 9236, 231236, 14135, 87837, + 23318, 70261, 78893, 30151, 81482, 14332, 1084, 74256, 27532, 46644, 79185, 3148, 62615, 6981, 55672, 31668, + 36825, 1849, 14536, 37446, 14738, 23779, 43058, 162749, 72199, 1168, 21346, 5592, 85932, 85302, 9668, 18351, + 57135, 150360, 2080, 228015, 77953, 34670, 119302, 151751, 31009, 106725, 84265, 45214, 59289, 74178, 113071, 263206, + 111009, 4021, 44449, 188119, 192629, 123592, 392506, 292847, 114487, 12831, 205858, 9852, 20780, 79648, 75767, 357014, + 97721, 18166, 21005, 67950, 33226, 204009, 16536, 2987, 11335, 66717, 144910, 47950, 17262, 55060, 15063, 2934, + 51038, 26775, 178497, 66008, 3427, 49433, 128592, 20036, 157553, 63861, 3089, 23015, 51210, 28696, 35933, 49942, + 71135, 231518, 99620, 17248, 21835, 176536, 20676, 16944, 38700, 165831, 233253, 295625, 36723, 13023, 52745, 10907, + 19423, 67972, 125868, 95473, 82875, 1183, 108455, 52685, 33417, 64095, 21433, 52438, 33191, 127809, 44505, 211823, + 7810, 2752, 95548, 162031, 7185, 91196, 47563, 61721, 33359, 17897, 23682, 42806, 178101, 22874, 49707, 199897, + 75419, 82456, 8618, 11171, 79712, 116847, 18783, 44190, 46564, 5346, 59046, 95032, 7893, 14916, 3214, 26800, + 24172, 121453, 34362, 10250, 17408, 18888, 4840, 68696, 22831, 13162, 36005, 32512, 14800, 62357, 41723, 45046, + 27247, 37486, 5372, 2564, 34261, 298500, 66509, 133920, 89138, 31305, 117697, 19097, 108304, 81386, 84106, 23802, + 46411, 63304, 946, 51417, 41777, 41041, 19501, 115864, 60743, 294354, 37955, 94165, 18116, 1156, 17937, 20645, + 57114, 90804, 58042, 48643, 92288, 9861, 2557, 88546, 61333, 101008, 12853, 5148, 87856, 4152, 144503, 73841, + 18718, 9789, 147565, 10846, 42085, 12789, 30223, 8993, 56352, 67203, 2448, 28215, 6052, 23540, 126319, 75933, + 36689, 80235, 23231, 23561, 21383, 38800, 77548, 102798, 21234, 31468, 158608, 46188, 63960, 191679, 8051, 67014, + 11185, 170078, 42186, 28827, 34777, 41930, 212079, 12421, 34750, 24111, 110344, 73918, 45171, 70826, 141949, 40063, + 23979, 24254, 37309, 26724, 27179, 24718, 83648, 54938, 14591, 17425, 29525, 102675, 48975, 48654, 12316, 8929, + 60640, 41709, 50168, 63264, 89812, 50716, 48632, 38755, 138583, 160123, 55579, 71829, 24230, 233277, 46322, 39650, + 166388, 34718, 24108, 98252, 7031, 106695, 62498, 18258, 35062, 217827, 78731, 34824, 33354, 19520, 60852, 2432, + 60224, 8587, 2836, 62955, 702, 20227, 42285, 40560, 95592, 62486, 11094, 53035, 143291, 18842, 46177, 77994, + 1770, 9657, 107422, 172915, 32655, 128716, 25886, 25164, 156740, 119928, 165875, 85817, 11007, 89110, 33956, 12652, + 65156, 180266, 8494, 36889, 19958, 20955, 96, 1264, 118288, 135769, 44754, 86671, 5632, 19026, 168220, 289120, + 33569, 93821, 66144, 70635, 7687, 5642, 2714, 55445, 56636, 71545, 184182, 93133, 7332, 37389, 12643, 52315, + 22729, 11014, 158742, 17050, 152889, 50178, 34601, 41945, 52136, 9948, 26914, 63548, 95721, 115951, 40759, 8960, + 158258, 38938, 49232, 48325, 42234, 81523, 253019, 66128, 40978, 20048, 238048, 38760, 62928, 122560, 118532, 43687, + 137472, 163689, 26680, 9878, 17448, 51035, 16211, 60834, 36749, 29178, 14241, 59868, 150086, 2305, 26477, 42422, + 34342, 165341, 83279, 33894, 14257, 29928, 12743, 13957, 125571, 89134, 66712, 10952, 16507, 147839, 30146, 7249, + 16565, 45399, 39874, 114565, 215780, 31990, 230881, 171477, 102, 196546, 44538, 10880, 84948, 281705, 86651, 10617, + 31395, 2342, 453658, 43569, 60561, 132901, 21845, 17727, 58556, 258242, 22262, 58728, 4008, 77997, 11806, 37431, + 30599, 81375, 109137, 185787, 114085, 217292, 97453, 169085, 30593, 60212, 11544, 102056, 65580, 2384, 91655, 4855, + 95725, 7295, 157994, 16228, 20669, 53276, 141590, 105246, 17334, 25440, 76067, 17967, 39321, 38911, 11362, 28559, + 63807, 21627, 26468, 85816, 40120, 1025, 15234, 58319, 69516, 66512, 124548, 75845, 78873, 22137, 46681, 51242, + 85683, 32909, 76747, 35555, 43396, 101465, 1765, 73094, 1077, 2962, 39028, 66777, 57831, 42048, 15828, 13962, + 36041, 63657, 52412, 5242, 58846, 2141, 5506, 219012, 134451, 3936, 182230, 17558, 17153, 152237, 22621, 49377, + 170216, 35257, 68233, 65374, 6510, 11126, 212151, 7184, 2480, 22517, 3437, 33073, 30156, 16557, 3768, 55067, + 86829, 91000, 12350, 148650, 66017, 79424, 70885, 49066, 28250, 21369, 51213, 34533, 11510, 3258, 18176, 18465, + 84413, 6315, 36411, 163765, 4346, 356, 107618, 598, 13727, 285026, 162695, 8749, 14583, 7132, 63521, 184253, + 32378, 25991, 5604, 30961, 53675, 4874, 84693, 5086, 34811, 26978, 56564, 7904, 33519, 51221, 113942, 69253, + 6664, 125563, 22055, 220680, 102008, 742, 51930, 19494, 176108, 44424, 35123, 13025, 75685, 11759, 74335, 22250, + 181453, 131147, 16984, 132115, 154311, 11991, 76452, 52609, 85351, 196, 30969, 9198, 74919, 2529, 56838, 71779, + 29187, 116304, 3504, 62330, 41190, 86153, 28393, 254926, 104228, 105189, 13264, 84359, 3574, 12415, 8534, 57147, + 10175, 188174, 59504, 60932, 66318, 16407, 107921, 17638, 99103, 49278, 28403, 39786, 145865, 8462, 3558, 43406, + 142271, 29139, 21989, 36552, 93955, 72365, 7176, 13556, 106185, 37957, 321774, 17782, 129017, 51154, 27938, 24952, + 1935, 39366, 2791, 33489, 41582, 56078, 24558, 9311, 5449, 218786, 27808, 190429, 68013, 36020, 86003, 29735, + 3404, 87348, 119357, 115714, 2324, 86796, 81973, 40992, 43376, 93621, 28784, 16808, 36367, 2517, 2909, 191926, + 24978, 55303, 53308, 205724, 60068, 3098, 21375, 64784, 23949, 26579, 63121, 12319, 80145, 39967, 97861, 6757, + 70143, 67642, 37082, 34698, 69140, 122883, 46151, 62187, 80934, 429, 19437, 135071, 137885, 222647, 13331, 154065, + 327, 61778, 74257, 40116, 37493, 14855, 85079, 237641, 42342, 102164, 199965, 71204, 4662, 29368, 5042, 113914, + 122214, 8955, 13149, 102503, 43173, 5659, 163787, 69003, 307084, 63392, 171080, 21390, 81918, 86666, 36622, 24126, + 28887, 5736, 28054, 207170, 163428, 79891, 346467, 95363, 38980, 111806, 80828, 9200, 19288, 294896, 114468, 87405, + 111715, 141705, 7015, 72754, 68463, 48738, 243147, 33397, 101210, 37051, 98801, 82847, 20397, 4940, 185559, 18716, + 54718, 83491, 11725, 40803, 1128, 12128, 23060, 5174, 7745, 67007, 46701, 1571, 27807, 180186, 256996, 18975, + 16837, 7877, 212758, 250379, 15440, 87954, 57755, 24719, 124057, 83461, 258, 50864, 8874, 29038, 71289, 31627, + 15429, 9005, 4061, 113851, 107716, 82819, 13651, 79656, 117851, 17539, 111446, 12938, 39724, 190787, 4352, 15402, + 21070, 62708, 8539, 23777, 73853, 13552, 38810, 86117, 16285, 56400, 1718, 75342, 142863, 29033, 378, 110113, + 180321, 32586, 23606, 26393, 160984, 207987, 23783, 8406, 16904, 24596, 47274, 11693, 46539, 60524, 78595, 48423, + 31718, 20170, 9009, 146268, 15183, 191060, 172765, 1349, 138436, 37365, 10970, 40509, 225817, 20021, 70394, 152138, + 21541, 66559, 66544, 89352, 2725, 17258, 91345, 7313, 3815, 115868, 8660, 40362, 4071, 103524, 39388, 118275, + 21950, 6549, 38226, 32754, 209574, 29201, 43495, 18028, 20296, 40597, 18370, 47520, 202450, 24134, 2219, 8195, + 69545, 38041, 136934, 46374, 19041, 159811, 84865, 58620, 846, 98749, 13569, 30714, 97246, 32186, 4479, 27355, + 92973, 35214, 151491, 75963, 37631, 1561, 27200, 238083, 23182, 60756, 12291, 25766, 39355, 102333, 87362, 65741, + 59906, 19538, 201575, 48772, 102938, 24438, 292580, 39964, 66366, 9004, 61379, 50548, 37622, 38732, 28379, 68180, + 76622, 17488, 69849, 5963, 7219, 48143, 43413, 55358, 540, 58691, 29506, 19245, 52193, 48621, 5518, 13048, + 118625, 44755, 191081, 42061, 89197, 2259, 60665, 66994, 71210, 51232, 3585, 142096, 55024, 7892, 8345, 58653, + 463307, 65658, 64319, 137941, 136323, 53499, 12746, 43492, 6978, 95163, 29925, 60175, 5128, 7352, 41463, 184756, + 121146, 20473, 18426, 4598, 5309, 54580, 14277, 121151, 10691, 56711, 43880, 63409, 76682, 11830, 172218, 264898, + 32632, 66536, 81062, 31649, 25788, 92774, 60222, 11100, 63159, 9432, 224657, 25240, 53613, 152, 138620, 163829, + 2397, 85345, 12501, 37507, 64932, 38575, 43522, 65789, 80198, 78796, 35226, 3851, 108891, 73311, 3060, 28391, + 93671, 39663, 46142, 30982, 66041, 37281, 68157, 26553, 71872, 81142, 211527, 39747, 118119, 22695, 2859, 11066, + 20232, 168911, 7933, 197005, 17066, 111071, 44434, 133994, 120798, 12766, 227798, 45756, 132852, 29917, 36076, 55352, + 65281, 129800, 41958, 18944, 84678, 18580, 168093, 132621, 39997, 54092, 27740, 32354, 3770, 114118, 103242, 43918, + 15899, 18574, 145944, 3190, 123469, 219903, 24169, 100571, 62403, 16776, 92779, 14535, 17168, 16475, 14304, 37231, + 1712, 28218, 242754, 61688, 28980, 1318, 51359, 222657, 99200, 67989, 31772, 23932, 35351, 201251, 49041, 27306, + 19128, 40135, 3986, 77333, 19649, 120683, 151927, 21081, 7076, 78375, 77501, 101599, 8011, 89585, 96715, 58179, + 5378, 102138, 106793, 26051, 217276, 4197, 16297, 27014, 46721, 13322, 22806, 5278, 29629, 70632, 9647, 71519, + 58818, 40603, 128530, 8903, 36770, 56900, 31483, 26935, 43845, 34265, 34920, 87658, 6114, 84767, 64250, 47318, + 50720, 19264, 162514, 33357, 13117, 6705, 46696, 75032, 71054, 87004, 42035, 69138, 11903, 99854, 102328, 19611, + 34525, 69312, 6431, 49842, 101600, 133178, 108751, 41829, 89939, 225664, 48916, 99556, 9195, 130387, 5960, 36857, + 116724, 53518, 94002, 39077, 53996, 6945, 22261, 64291, 8314, 152785, 57588, 16522, 9091, 5048, 87671, 35441, + 39509, 1945, 12423, 158923, 178413, 37549, 14095, 1475, 73188, 62878, 4819, 24012, 68534, 42606, 4010, 120809, + 57497, 59564, 101758, 103718, 32701, 80116, 12345, 95834, 46918, 21468, 53213, 15665, 31200, 3867, 5140, 96013, + 250744, 21016, 10069, 13968, 35449, 180829, 27683, 39704, 59956, 22893, 3115, 26293, 32785, 75934, 62445, 141162, + 62720, 2018, 83638, 19949, 114012, 95006, 3330, 99829, 130935, 309272, 9565, 55874, 121727, 37017, 23586, 319858, + 40970, 27602, 8625, 112329, 61060, 100088, 118525, 25922, 16232, 1907, 60671, 51583, 44553, 80993, 5262, 94679, + 8676, 940, 20736, 11823, 3020, 16476, 12340, 152600, 97416, 3703, 25744, 66826, 16245, 16876, 46446, 84798, + 74227, 176020, 45192, 61955, 75496, 23946, 23626, 40372, 26036, 6149, 11822, 30582, 16541, 41914, 82385, 232823, + 40921, 80773, 14930, 3631, 7517, 39619, 4348, 36180, 126106, 138939, 62611, 1477, 113512, 47321, 25052, 14546, + 118881, 29060, 23589, 128322, 36795, 18401, 137921, 104699, 267929, 36194, 172791, 18113, 4766, 188215, 30083, 332586, + 94089, 5805, 77909, 22194, 68234, 154976, 43220, 40660, 70001, 184893, 138095, 11128, 103010, 22663, 5108, 212615, + 8485, 5565, 49222, 54614, 26530, 42639, 16319, 55062, 152662, 105595, 21114, 22216, 10294, 68158, 10436, 86950, + 7206, 62115, 3977, 3657, 59874, 456, 118617, 18156, 106663, 112229, 80992, 17442, 8217, 55551, 5133, 34344, + 251927, 51153, 39364, 201321, 7816, 66803, 23057, 156724, 145664, 14276, 95705, 979, 2796, 6875, 13429, 212525, + 50602, 26276, 28284, 3424, 19465, 52397, 46963, 31420, 51399, 206476, 92317, 48851, 637, 100820, 83349, 10317, + 60227, 21972, 6908, 282439, 32857, 224767, 95629, 83882, 42106, 87338, 69757, 29840, 68709, 37665, 45244, 114577, + 49188, 175943, 54009, 186746, 106158, 70168, 3358, 234002, 50555, 9221, 129338, 9562, 20118, 32923, 78479, 118280, + 65752, 4977, 10474, 102174, 60947, 129006, 10570, 83451, 8598, 8078, 159367, 123785, 80438, 16742, 5905, 5281, + 181513, 42402, 6977, 163136, 93179, 42191, 14968, 50421, 112401, 105440, 33456, 57347, 121611, 4221, 94954, 36517, + 24046, 27796, 6255, 33394, 72990, 135408, 116627, 1233, 57874, 25654, 95419, 68156, 401399, 313338, 55208, 45573, + 93124, 119251, 47200, 38196, 11909, 130667, 45391, 73904, 64964, 167846, 4137, 115606, 52036, 62214, 7969, 160925, + 7187, 1132, 134835, 40309, 73195, 64494, 80472, 444841, 61111, 26500, 45323, 40743, 53625, 52797, 22659, 15631, + 29739, 36706, 28841, 39147, 102836, 26794, 10536, 14845, 87305, 45874, 12241, 127587, 83833, 57183, 79722, 30844, + 41304, 84655, 20825, 92500, 3722, 25655, 27811, 10157, 81634, 31362, 34088, 92487, 70123, 22190, 185100, 72658, + 139035, 192523, 88241, 2078, 230490, 44528, 85638, 100198, 22088, 29982, 291233, 241062, 13865, 4445, 137791, 37835, + 107218, 31726, 19718, 38234, 72528, 23046, 19177, 66695, 5109, 17251, 28077, 5617, 21554, 47839, 72425, 133825, + 1486, 73065, 181275, 141508, 21768, 62971, 63082, 2512, 34200, 9904, 120309, 6392, 91243, 68416, 268253, 41199, + 116757, 138551, 185526, 41246, 28986, 4093, 19057, 17295, 4148, 245766, 122360, 35356, 112075, 20301, 75441, 10998, + 7977, 19769, 62922, 937, 63547, 100196, 26427, 157820, 20983, 236696, 22935, 8140, 90315, 156004, 47204, 140973, + 7726, 45097, 52725, 22636, 23436, 257282, 105247, 522, 88389, 216031, 202204, 46812, 211666, 19693, 68828, 81691, + 45925, 11256, 30292, 372, 5236, 167826, 88328, 232776, 151611, 5360, 82104, 18841, 80393, 25465, 18285, 20320, + 72377, 31730, 33160, 45803, 38715, 27705, 37379, 24163, 18360, 103586, 4015, 32305, 269494, 91252, 20080, 36567, + 54650, 7797, 57073, 12650, 31164, 42209, 6375, 261663, 105528, 81661, 106002, 2800, 5375, 17247, 43151, 4442, + 15727, 194619, 100855, 144898, 62320, 78465, 39929, 16454, 1967, 28311, 61363, 17219, 9395, 8745, 121445, 76939, + 80385, 162380, 22009, 54191, 44248, 16299, 122830, 48151, 74429, 78291, 64755, 14238, 44966, 2511, 17712, 67954, + 93583, 829, 105899, 49935, 84750, 11591, 33185, 85447, 42717, 27409, 208542, 28965, 62052, 52525, 5597, 25694, + 65594, 16343, 63224, 276188, 12475, 9331, 127507, 38522, 57287, 24128, 133161, 79723, 105548, 133695, 48917, 27558, + 43278, 46520, 13778, 141954, 110785, 83366, 17715, 46317, 105763, 66298, 147013, 41086, 94180, 16478, 220447, 44611, + 730, 19722, 78975, 117889, 125643, 26254, 16574, 18480, 65006, 15806, 38549, 246418, 46052, 36056, 8440, 34984, + 30170, 3163, 59800, 4458, 115442, 4283, 41970, 33507, 104078, 1653, 22, 121158, 276486, 3655, 6338, 24048, + 133421, 23641, 2161, 24422, 36006, 8086, 10675, 181474, 12307, 29514, 59143, 14729, 52509, 87128, 122470, 19446, + 80852, 33314, 24573, 119864, 14237, 9652, 57779, 6612, 51851, 15284, 98871, 90581, 124466, 156831, 21190, 22015, + 71380, 161906, 87247, 69201, 18392, 17908, 108470, 72962, 40719, 14338, 17911, 95260, 43339, 20610, 78916, 20710, + 72451, 11315, 31448, 17263, 58853, 178878, 48111, 116002, 45497, 80506, 82605, 85880, 36300, 121755, 25215, 36118, + 301929, 88728, 405223, 276136, 553, 34704, 212438, 49970, 78329, 922, 20711, 25036, 257130, 38295, 145369, 18128, + 15385, 30829, 55656, 48345, 8012, 3561, 28004, 122041, 192900, 58338, 112508, 41085, 29976, 87040, 47117, 23905, + 4336, 92061, 138880, 97407, 42083, 172121, 6256, 25192, 172671, 5, 93568, 1420, 12677, 31605, 56743, 40620, + 6015, 78415, 231077, 31298, 80026, 13902, 19048, 24924, 170586, 32955, 176119, 87859, 36731, 6773, 27711, 24658, + 26475, 115216, 133207, 93250, 95820, 88522, 8317, 5714, 124047, 55219, 86860, 19677, 23961, 22928, 162209, 8904, + 225992, 359835, 56084, 96201, 29392, 96558, 86071, 93643, 55114, 13347, 8183, 95129, 82012, 2017, 123336, 34219, + 115554, 157159, 47747, 101684, 41008, 18735, 193781, 104151, 226906, 7552, 179874, 124113, 31159, 21162, 44010, 14771, + 51268, 166128, 31382, 73124, 77438, 92830, 205709, 12113, 1292, 38937, 13114, 1334, 2118, 15597, 69581, 14449, + 21934, 76618, 48728, 67038, 14967, 51495, 24243, 87736, 147249, 26720, 11119, 46063, 43749, 5843, 44147, 152629, + 133428, 65703, 14269, 45604, 57982, 28672, 55616, 45957, 8438, 95433, 37698, 220862, 132034, 39456, 61870, 4161, + 26501, 73560, 56418, 9845, 4654, 20916, 10456, 88920, 119358, 9015, 65931, 96507, 48029, 38534, 21676, 109081, + 43078, 34943, 25089, 6131, 28766, 23665, 5477, 10255, 16695, 67, 45778, 42443, 42770, 29534, 23733, 100513, + 62617, 42630, 48746, 14191, 43753, 50295, 26007, 8792, 57243, 43119, 54725, 164253, 58250, 112304, 131796, 25165, + 4651, 3188, 24831, 47748, 3705, 19540, 13211, 102095, 5593, 18699, 23666, 32005, 117571, 33541, 60584, 74573, + 86311, 99443, 25172, 27222, 168938, 7143, 11853, 53560, 18834, 19960, 86522, 28217, 53266, 117700, 72989, 34323, + 18721, 66450, 34346, 74056, 47217, 202002, 46269, 9429, 68582, 75458, 37823, 82843, 96652, 32549, 145144, 27958, + 19820, 158086, 31955, 201406, 135379, 31207, 192545, 12950, 51704, 9094, 248263, 76147, 64028, 110009, 79407, 89345, + 99284, 223492, 47966, 26848, 15359, 201137, 2861, 110507, 71231, 72297, 31851, 118777, 71039, 151051, 240855, 16333, + 50766, 14727, 7939, 4149, 80908, 418780, 88378, 59276, 1327, 7284, 38576, 79814, 65820, 42199, 84860, 49574, + 62596, 12396, 70598, 40117, 8648, 7994, 16836, 7630, 14047, 359699, 106878, 525, 29037, 28064, 13380, 11675, + 50669, 74216, 103539, 180314, 27449, 56299, 172344, 19274, 7301, 246099, 32043, 19422, 36506, 129317, 6806, 30140, + 4614, 46639, 66926, 932, 86600, 6322, 27847, 233103, 10541, 39025, 34887, 3517, 12972, 26220, 2031, 66561, + 115015, 48658, 47596, 12714, 33845, 3893, 16165, 35237, 89983, 14769, 11962, 147224, 47018, 29977, 27979, 5552, + 82338, 86023, 131368, 1218, 24853, 237840, 132193, 15455, 40873, 3668, 65351, 53388, 15229, 59889, 272245, 47934, + 11858, 34347, 18038, 90853, 86981, 300602, 19343, 114181, 29362, 84921, 6095, 106059, 79472, 38015, 1206, 48741, + 6208, 80000, 21916, 17423, 6002, 108083, 24479, 34931, 56661, 9511, 26995, 100694, 163853, 35997, 81254, 58321, + 18919, 171890, 86877, 91341, 74503, 70477, 53412, 7027, 59281, 39892, 131302, 5864, 15947, 61301, 67466, 162369, + 47956, 27874, 35624, 282324, 21270, 111847, 102548, 41482, 30955, 116737, 28264, 8592, 55458, 22301, 75090, 29821, + 30697, 51709, 3041, 19208, 8038, 24634, 30467, 87509, 126428, 19389, 18814, 152686, 20701, 83474, 45832, 80891, + 105808, 11378, 153223, 120770, 98186, 150633, 49838, 9141, 12755, 30962, 5260, 74490, 21256, 31678, 65062, 33326, + 289838, 187831, 20595, 89768, 2805, 58535, 10844, 70085, 12090, 2451, 138068, 98544, 24461, 4511, 6754, 41684, + 28203, 3383, 65355, 82833, 30161, 83924, 234361, 128424, 28921, 222594, 33975, 125491, 34069, 11508, 67464, 144226, + 41850, 98703, 34371, 7901, 21254, 38398, 65651, 23549, 53883, 213340, 123269, 12028, 71764, 177701, 28758, 2623, + 68395, 11549, 15232, 68603, 9660, 63116, 36079, 57093, 31198, 20475, 48467, 89984, 35619, 186847, 107469, 31389, + 43631, 73867, 41949, 68841, 114250, 1605, 30564, 63403, 17588, 27680, 99533, 12641, 70325, 50428, 73426, 78379, + 11855, 91651, 72081, 91720, 60198, 15743, 12065, 83398, 140046, 6761, 46598, 45900, 5068, 886, 62448, 148968, + 37347, 19405, 9680, 15819, 43496, 63370, 75667, 163700, 37639, 3633, 22774, 34341, 183131, 134335, 37200, 23915, + 7054, 14194, 12970, 26438, 13350, 285521, 25594, 8219, 104410, 91039, 168804, 138480, 149734, 15907, 33818, 61132, + 60082, 4622, 110187, 56736, 13551, 73571, 3945, 73463, 65498, 17758, 263266, 17593, 2710, 27585, 54469, 38200, + 45367, 63754, 28881, 3473, 12791, 98287, 31895, 65787, 4463, 94536, 24951, 36332, 59901, 28803, 52130, 86403, + 7668, 181822, 74831, 18977, 9850, 177206, 145485, 109798, 7292, 31421, 26280, 77211, 58511, 12507, 127004, 11113, + 147, 8729, 56208, 43066, 79926, 129937, 31345, 83947, 39915, 46146, 98763, 42566, 1337, 13192, 18323, 105163, + 80570, 117753, 16555, 72883, 11077, 159438, 40764, 70933, 83329, 26066, 12276, 72059, 21655, 173836, 126713, 69454, + 153482, 91585, 70644, 102558, 110483, 6764, 127864, 190133, 3961, 101798, 20945, 71138, 82402, 90884, 69669, 44753, + 923, 16939, 59700, 164258, 25969, 27082, 31399, 43846, 6306, 246093, 51342, 6153, 151581, 202801, 182731, 56475, + 162188, 89426, 141356, 14355, 121815, 27536, 28023, 65257, 77523, 106668, 127314, 24947, 12790, 38796, 169698, 23555, + 10725, 44573, 183083, 42088, 62716, 43265, 105958, 32050, 44067, 50118, 1668, 3874, 6243, 318411, 16599, 1691, + 94999, 52378, 28671, 216728, 123258, 2059, 34969, 69225, 5913, 136280, 171443, 141515, 91662, 22175, 135282, 80020, + 92270, 1663, 4808, 4482, 3495, 34691, 5226, 109830, 108512, 17342, 107488, 11606, 123190, 100247, 29666, 146527, + 113014, 15794, 30894, 13224, 39585, 243192, 22351, 9903, 7836, 47699, 11078, 25468, 122291, 48821, 26780, 122679, + 75521, 81450, 630, 4895, 92900, 55074, 74293, 17441, 3563, 111657, 103102, 51613, 12318, 52370, 36191, 68245, + 34269, 40445, 41354, 122901, 168604, 182500, 62012, 42557, 11259, 24428, 115113, 86345, 12362, 3909, 78430, 86852, + 134602, 20459, 47853, 93879, 22577, 7659, 3688, 38555, 13349, 17381, 56715, 91639, 12493, 10895, 92438, 3142, + 37057, 28928, 2004, 36427, 32268, 34222, 209974, 10432, 67436, 41989, 173518, 107930, 27079, 62729, 30908, 55558, + 5828, 45031, 14902, 53546, 8204, 144263, 60255, 14520, 88212, 86582, 109589, 69356, 8064, 47449, 8505, 66558, + 16886, 4844, 52817, 111260, 215129, 12941, 91118, 650, 20770, 6273, 73089, 40618, 62790, 2873, 35002, 14023, + 97208, 19386, 102646, 36993, 143736, 135457, 35385, 113601, 17893, 32627, 84439, 100619, 56016, 6581, 57264, 172160, + 45452, 111710, 203627, 70131, 24100, 322787, 1996, 35665, 70078, 22358, 90922, 83658, 4097, 63200, 58499, 14542, + 99153, 52159, 6615, 12414, 63415, 31986, 16823, 1579, 65405, 137809, 8841, 16898, 48082, 259, 33014, 42375, + 12260, 179850, 73667, 91389, 98882, 29532, 17311, 326251, 41092, 5928, 20742, 44964, 48019, 43505, 9317, 49265, + 6643, 192712, 48424, 163487, 19861, 20113, 70848, 31928, 105333, 23685, 78563, 14638, 54755, 7158, 24142, 44018, + 20774, 125255, 20331, 24280, 10163, 1285, 2336, 39851, 4299, 117269, 46714, 63816, 87779, 159624, 11731, 9971, + 990, 137317, 108831, 50994, 74554, 162680, 23640, 131597, 146962, 170620, 34829, 91205, 21184, 1913, 63616, 18427, + 93136, 156592, 17519, 67565, 115882, 138220, 78622, 88535, 18115, 2711, 33554, 109492, 54298, 971, 24914, 25863, + 36363, 45715, 27099, 194995, 14299, 178181, 111488, 72395, 322385, 157719, 130787, 11897, 81843, 83999, 11369, 49280, + 118604, 40922, 61332, 110343, 53407, 75639, 40582, 300440, 54722, 25637, 13694, 48248, 48278, 194521, 56203, 52779, + 48783, 72627, 10953, 376, 16733, 280238, 26351, 230789, 15132, 25168, 137270, 3588, 63704, 73376, 94031, 74284, + 19443, 159557, 9697, 39901, 13351, 119050, 15406, 146455, 3460, 29556, 75195, 37673, 102524, 92329, 47289, 98413, + 15311, 100684, 56345, 7116, 95480, 11590, 7200, 167, 23610, 58426, 17730, 136656, 27944, 53151, 2701, 8824, + 103124, 3017, 90744, 113588, 53216, 79736, 65940, 26931, 498, 29568, 80540, 143543, 21292, 1740, 59268, 16561, + 180816, 42323, 50174, 40890, 52866, 10703, 57169, 4700, 17191, 4424, 93511, 49698, 166650, 26972, 48631, 165169, + 82879, 69326, 202970, 4007, 2376, 231325, 139592, 22119, 62851, 37504, 68816, 58345, 67398, 186643, 43331, 277416, + 53749, 15746, 23102, 17432, 4793, 151138, 48822, 54265, 48203, 198688, 14305, 54287, 2291, 18018, 113378, 123260, + 7180, 97549, 87027, 120085, 2920, 76080, 8190, 102005, 5641, 64580, 14955, 59802, 54028, 58884, 19367, 81779, + 412567, 85957, 97053, 103637, 78871, 29364, 27637, 141728, 4767, 30686, 112738, 130146, 42745, 12730, 105040, 14844, + 232, 210944, 36581, 152317, 135543, 29744, 3129, 55647, 58149, 46319, 27265, 17499, 28005, 59948, 7170, 34138, + 5702, 293047, 110892, 408, 91760, 218674, 18469, 46095, 81403, 14389, 4610, 35672, 73060, 11006, 74848, 104820, + 118143, 190357, 20043, 105358, 141735, 5115, 27093, 45924, 123073, 52599, 29433, 9616, 238350, 78610, 24851, 58858, + 26769, 31969, 24613, 18294, 4982, 32735, 39639, 143563, 112073, 202205, 12567, 4873, 88601, 44897, 81503, 101648, + 81362, 34662, 85277, 17574, 48173, 21435, 221188, 40215, 39576, 80786, 26544, 64668, 81841, 10731, 37733, 247986, + 149188, 127703, 495, 18382, 54388, 72446, 43071, 30974, 198723, 89608, 41360, 190, 33045, 8386, 31658, 19992, + 237838, 119015, 137622, 50890, 100913, 6460, 116233, 267230, 26621, 104129, 65114, 14190, 41542, 14888, 85962, 23342, + 23041, 26453, 43725, 71809, 45186, 4770, 46452, 53894, 56616, 221286, 18973, 9038, 109299, 55365, 19366, 26863, + 18808, 60909, 69353, 41738, 83463, 12100, 68561, 72860, 3980, 13796, 49340, 12332, 31311, 27418, 4255, 53430, + 18976, 45523, 510, 14224, 30477, 26581, 4530, 3651, 101663, 139840, 22709, 150861, 31996, 63923, 120623, 262522, + 3076, 10528, 2929, 14672, 130238, 18087, 9816, 121894, 100308, 25085, 55111, 14565, 18952, 53293, 2042, 369988, + 23674, 61789, 133529, 28783, 108293, 35477, 47119, 36448, 71049, 40015, 33055, 78598, 198442, 1833, 159937, 40654, + 77444, 189245, 113153, 8621, 18599, 38553, 35223, 166072, 2375, 11659, 21786, 89523, 6032, 12116, 63046, 159398, + 18454, 3678, 32521, 47626, 11411, 103527, 38896, 42946, 15696, 26370, 10185, 8413, 37080, 165583, 4331, 63555, + 14907, 72220, 50056, 6623, 62236, 36565, 49783, 10049, 17503, 100581, 55951, 146244, 24724, 9626, 17969, 25524, + 109300, 173965, 99994, 101056, 46459, 43647, 53737, 277968, 8347, 123521, 74858, 33829, 44762, 77574, 877, 81377, + 222525, 123532, 30602, 43881, 53145, 2973, 16284, 81940, 61281, 127044, 63620, 9875, 14756, 114829, 19032, 9202, + 52759, 119141, 23928, 120551, 19607, 3599, 33401, 76821, 73233, 117430, 39968, 36539, 7071, 5446, 121735, 194059, + 15206, 45283, 6706, 15603, 65615, 1207, 165723, 92275, 34773, 104447, 8396, 32353, 205240, 164323, 13600, 60555, + 79205, 25532, 22907, 33410, 57480, 107111, 69630, 32137, 47832, 70913, 33161, 20321, 2371, 117348, 10714, 86246, + 1625, 11763, 17900, 268, 78457, 99175, 97940, 101092, 86660, 32221, 14041, 128504, 125080, 53744, 124263, 31017, + 13897, 403, 31859, 21964, 5633, 111630, 5547, 77329, 17961, 18241, 84995, 25984, 12983, 67491, 62168, 47262, + 5241, 297, 51191, 7351, 8967, 147212, 82060, 16821, 782, 11033, 82431, 62957, 5026, 43459, 77963, 203477, + 53528, 6247, 191852, 87774, 74164, 215654, 13467, 1522, 219964, 28589, 244104, 16242, 117821, 67725, 72570, 156792, + 17186, 15979, 26990, 44128, 193014, 35276, 57125, 16212, 166451, 68017, 6905, 77608, 16364, 53777, 75921, 76426, + 37975, 26203, 269296, 64099, 84122, 12077, 38533, 830, 4407, 20139, 963, 43028, 38902, 42911, 37503, 83343, + 85045, 16979, 1165, 60835, 137387, 58380, 86990, 110066, 134540, 56331, 193845, 81238, 17922, 163093, 38744, 110641, + 12502, 56404, 34862, 26865, 125964, 12965, 111648, 25547, 7771, 27196, 136980, 9555, 29551, 107158, 57885, 18831, + 37705, 35505, 101742, 13970, 102109, 62548, 124657, 23328, 11124, 89592, 146376, 248050, 6241, 22033, 18337, 80685, + 29898, 11908, 216623, 67721, 106162, 146610, 21377, 15085, 91552, 42041, 62560, 122532, 125336, 102365, 121537, 142559, + 29693, 223919, 11515, 110495, 18776, 22494, 5895, 185059, 103592, 229351, 51220, 100102, 37027, 257855, 29359, 54123, + 36066, 106493, 12244, 79258, 32002, 432, 56205, 94836, 90182, 6726, 14762, 29391, 48938, 26864, 38083, 60364, + 3310, 60192, 14766, 205567, 57504, 110760, 22649, 24666, 46333, 21517, 3430, 13135, 28873, 27052, 158809, 11597, + 20529, 6695, 23138, 22960, 37137, 45574, 6545, 305877, 43423, 26153, 24769, 59844, 14501, 10430, 134352, 56169, + 13213, 103432, 49523, 35181, 13435, 12408, 129475, 64620, 230854, 77390, 51990, 15653, 83248, 33466, 44571, 117828, + 51481, 2187, 10559, 68019, 18021, 54895, 48247, 18354, 33737, 4554, 108595, 37288, 39767, 116707, 9175, 3726, + 108877, 21616, 83684, 49862, 1938, 8543, 276466, 20134, 108498, 48770, 102254, 31914, 131520, 185291, 100559, 51890, + 209, 19526, 76471, 50544, 71814, 99351, 8172, 198526, 28816, 20419, 9109, 98389, 136777, 76479, 75596, 30635, + 165417, 48216, 120220, 25955, 211071, 39314, 24308, 32164, 2559, 146280, 43403, 9233, 17947, 90585, 1786, 86920, + 125662, 2457, 64741, 32152, 32918, 122882, 78538, 44001, 31723, 56426, 23375, 103172, 88177, 145697, 52506, 49319, + 68016, 31664, 41488, 18486, 110400, 7030, 28241, 986, 109199, 19900, 42147, 56864, 65287, 49183, 7858, 24000, + 30453, 840, 16673, 25907, 68916, 89927, 6309, 158335, 36407, 199737, 130464, 13137, 59603, 201778, 195292, 21015, + 42466, 179062, 172561, 89492, 11075, 180407, 31868, 72493, 20998, 60217, 9865, 19530, 39274, 130266, 54539, 21623, + 12535, 13505, 40641, 73375, 4087, 85633, 2153, 3117, 70680, 55788, 92096, 47509, 98493, 37490, 271936, 151475, + 3032, 16171, 96642, 34106, 78425, 125761, 19591, 3366, 19316, 54508, 24183, 50786, 194248, 91528, 33253, 34622, + 108355, 41741, 705, 3814, 3883, 108929, 13203, 67831, 10142, 59754, 68208, 29128, 84820, 56880, 38794, 24972, + 48571, 40821, 40476, 18137, 164254, 24064, 236309, 79181, 11282, 395, 39169, 2013, 51587, 28551, 9645, 701, + 109513, 115899, 113566, 12762, 62045, 58322, 103726, 41343, 40866, 244102, 143816, 2490, 70346, 40973, 52618, 15412, + 30720, 104315, 38917, 42027, 93676, 17513, 107418, 20706, 123890, 13399, 97727, 24044, 87962, 65606, 44250, 98044, + 65276, 74790, 101473, 19350, 91570, 1326, 87790, 172042, 7577, 100813, 86896, 85891, 41512, 108130, 27794, 14875, + 71431, 12835, 156250, 58135, 3759, 22476, 42176, 115873, 34686, 56523, 73643, 108505, 51491, 20838, 12721, 32863, + 45700, 29496, 13700, 34294, 55360, 29206, 155942, 123812, 7706, 163234, 203, 132720, 49358, 144431, 8130, 175788, + 35818, 3270, 76832, 25710, 54095, 97274, 28779, 94621, 74396, 19092, 128242, 58067, 20885, 14670, 93255, 15107, + 63291, 23654, 126900, 129421, 59294, 262659, 9798, 3251, 67344, 28600, 44629, 50672, 29072, 26999, 31526, 23183, + 49175, 165843, 175455, 17282, 175411, 32022, 45989, 30298, 90690, 78118, 83156, 23749, 35636, 31317, 7069, 80381, + 94561, 133756, 14960, 97404, 6138, 41065, 78041, 32843, 16601, 34123, 9559, 146529, 123377, 96395, 54441, 42012, + 84257, 123541, 10745, 22139, 106459, 11720, 150883, 172651, 154996, 110538, 4728, 53447, 25704, 2009, 71152, 119354, + 21166, 66604, 1429, 216162, 8637, 122250, 63520, 27180, 29172, 36124, 276428, 107787, 77184, 4680, 14952, 104903, + 24418, 14793, 51561, 52931, 8371, 26342, 48526, 7118, 92066, 67280, 40653, 8847, 34597, 105438, 14198, 50163, + 61188, 146286, 50315, 41205, 170829, 161496, 585, 197359, 95056, 1687, 365794, 91349, 48507, 5804, 49263, 5146, + 104902, 96365, 117343, 132222, 46084, 96919, 16875, 8073, 262381, 79982, 52663, 13928, 16056, 153908, 15145, 109256, + 132308, 18763, 24904, 167644, 13618, 40750, 18686, 147124, 114709, 150038, 52849, 2938, 12568, 48617, 8778, 5459, + 44202, 44591, 74914, 17183, 248689, 13878, 7822, 80060, 23116, 194037, 18487, 2067, 7798, 43077, 33678, 244028, + 31320, 74273, 2794, 19466, 8218, 36280, 183997, 48124, 19416, 29656, 19280, 98734, 7715, 18311, 30701, 133602, + 150307, 126956, 7378, 2933, 79903, 13178, 12593, 86571, 26604, 92446, 13574, 44205, 65699, 427599, 21118, 8245, + 14407, 27877, 47936, 33542, 7916, 26460, 117762, 21596, 37818, 2249, 127359, 209394, 60044, 47677, 308089, 36791, + 154971, 31417, 6998, 150042, 174360, 12255, 43009, 29335, 48739, 3912, 101398, 53340, 2580, 146939, 151295, 45360, + 125275, 15273, 45383, 27456, 48761, 23314, 8750, 60801, 85823, 104759, 27894, 123685, 66968, 39480, 26917, 55290, + 83305, 2696, 98390, 57569, 145853, 340733, 4919, 20024, 52268, 30884, 7413, 203685, 70989, 112855, 4129, 50536, + 349518, 68205, 332641, 159581, 135361, 236026, 37563, 176404, 64899, 6578, 122033, 63871, 1850, 85234, 82089, 66124, + 74145, 121098, 107351, 12687, 36881, 117334, 13136, 14698, 85933, 93866, 18047, 32620, 310, 15094, 46000, 88451, + 23632, 36645, 27940, 87618, 80520, 58892, 20976, 27702, 140090, 96075, 67841, 103292, 238964, 87778, 107338, 17019, + 83427, 67522, 7302, 8261, 47570, 116787, 8730, 80484, 61772, 174422, 56005, 131193, 52875, 14588, 28471, 59817, + 9586, 15720, 158155, 51307, 109734, 15196, 11025, 59331, 3884, 52626, 102602, 84797, 25158, 27314, 4437, 20488, + 76214, 189248, 35023, 114952, 157376, 2827, 62439, 102878, 129749, 36405, 10329, 109339, 108633, 36662, 1254, 13267, + 5470, 87105, 58004, 15397, 10434, 159667, 21864, 52022, 179464, 3013, 32147, 31496, 116832, 18494, 105502, 129227, + 107267, 50033, 13481, 9954, 24267, 22141, 16257, 116154, 36185, 950, 115685, 11305, 176708, 2048, 178671, 112573, + 287867, 162328, 497663, 95170, 50979, 193861, 50987, 30368, 136257, 31830, 46549, 15119, 169876, 23788, 17462, 249887, + 57377, 1949, 35448, 14791, 43769, 210091, 3783, 34612, 282103, 88380, 245190, 5457, 20491, 98908, 11402, 86899, + 117916, 16028, 162584, 60644, 320177, 156096, 31065, 55876, 22000, 77655, 9992, 23397, 13757, 317623, 63978, 215255, + 2443, 17648, 93231, 27388, 104529, 93807, 55505, 140477, 12046, 112040, 70887, 40152, 94365, 112353, 25063, 114679, + 266061, 71248, 119555, 15589, 2244, 617, 14129, 211431, 70110, 100652, 7777, 4383, 85911, 89221, 21010, 120615, + 58357, 86405, 37554, 41647, 18, 15143, 69662, 60491, 14714, 186134, 148344, 42347, 5410, 168175, 44535, 42449, + 343894, 129417, 99682, 20659, 27272, 140483, 63455, 222159, 17536, 13722, 42637, 62324, 11976, 114691, 148109, 2283, + 32057, 182393, 4295, 147364, 33705, 2075, 44303, 30274, 28331, 63740, 69740, 29148, 10346, 44862, 33716, 73937, + 153333, 12930, 38784, 247159, 2515, 41053, 20256, 83368, 256189, 54639, 115240, 5096, 24661, 175419, 153552, 26516, + 141, 138176, 63885, 34115, 47222, 55709, 2765, 28479, 38875, 236608, 12229, 22921, 77291, 54426, 45388, 2860, + 57787, 114579, 295139, 105782, 17826, 71066, 19119, 54364, 69385, 16568, 12323, 28057, 33346, 34919, 124763, 155533, + 101386, 31644, 8627, 49001, 303600, 29868, 63213, 9103, 77280, 71333, 9696, 138789, 37059, 24823, 5057, 21352, + 32368, 114208, 56803, 19424, 10445, 58514, 8661, 209508, 26187, 171838, 10460, 63454, 14016, 122504, 41328, 21329, + 46618, 32493, 38225, 7855, 31763, 7945, 29876, 8734, 6438, 24205, 97490, 139977, 130740, 47323, 33195, 85390, + 57194, 13813, 60600, 21313, 96251, 7699, 27584, 170521, 139271, 1363, 4402, 336738, 129223, 84983, 69150, 13147, + 3590, 163929, 207225, 155260, 55916, 20288, 4503, 8398, 98490, 11773, 27512, 37113, 84976, 86558, 28365, 11756, + 116005, 182148, 13733, 115313, 47644, 67208, 85069, 9347, 14995, 226141, 14704, 101835, 41159, 35314, 13113, 63526, + 214039, 29978, 50446, 83339, 17440, 129441, 72522, 118641, 97816, 24907, 73844, 15717, 118884, 167255, 96509, 162793, + 30847, 36849, 51297, 78974, 77793, 10427, 1873, 2972, 9999, 35074, 28190, 64297, 146836, 46298, 60038, 163007, + 108919, 61219, 2403, 75022, 127339, 4233, 110389, 69022, 9833, 128097, 88016, 79390, 222936, 22570, 94657, 28462, + 56956, 38803, 81536, 30474, 152794, 19566, 16481, 147408, 74574, 81895, 20731, 1918, 1366, 76367, 187321, 54494, + 24366, 21690, 61696, 33283, 107477, 77499, 31112, 414383, 74362, 18463, 218441, 120929, 59848, 258629, 201924, 69269, + 454, 19989, 13054, 59894, 3623, 58908, 20681, 35723, 78523, 102680, 38988, 184112, 108087, 50944, 132704, 52966, + 21699, 18860, 96349, 201411, 82697, 85395, 95658, 5093, 6427, 177894, 44191, 32755, 26961, 155739, 6249, 31310, + 81030, 26574, 84311, 120155, 86730, 113535, 7424, 48888, 13516, 45747, 98098, 20077, 183995, 81945, 43210, 26704, + 40420, 75831, 45648, 11180, 6855, 57927, 65528, 124096, 34851, 2598, 156633, 107572, 127352, 38169, 123845, 60142, + 62722, 105584, 232364, 23211, 68120, 1601, 22169, 89299, 747, 258039, 80572, 7258, 152249, 11862, 101204, 8834, + 121434, 33761, 19175, 133142, 46343, 40178, 48723, 3589, 41977, 30210, 38868, 62257, 10087, 82658, 87827, 90646, + 16415, 47552, 351723, 28298, 72225, 91146, 272760, 1701, 11295, 1652, 109651, 300747, 51863, 198800, 29446, 11794, + 32345, 37538, 22356, 33102, 37590, 113544, 37970, 11478, 179743, 25454, 103417, 59905, 221970, 105196, 145604, 7817, + 164809, 102360, 16974, 75840, 255333, 56902, 6659, 1954, 645, 59400, 67769, 7689, 18675, 5215, 13793, 20536, + 27852, 3387, 29523, 259718, 16860, 94625, 43143, 29245, 15848, 233581, 22685, 63631, 78557, 22836, 133302, 84513, + 1348, 51826, 47129, 98836, 58284, 1830, 1749, 94642, 10933, 6145, 12506, 10975, 13879, 103781, 144434, 10268, + 28409, 32346, 52968, 121567, 107374, 77268, 23686, 35097, 10501, 155275, 15303, 47136, 21102, 168741, 55332, 90385, + 15996, 84817, 681, 137803, 25054, 142275, 6163, 38175, 8056, 124296, 240642, 65621, 4934, 178205, 16101, 62803, + 60964, 18230, 100622, 76465, 44689, 14545, 9543, 47514, 16852, 93380, 28048, 12047, 107106, 37575, 101485, 77047, + 57326, 34819, 96137, 76916, 6469, 46264, 115983, 75768, 87668, 69942, 13027, 165, 8373, 114231, 26434, 52844, + 42799, 182044, 23580, 146254, 38081, 43236, 33883, 146220, 382894, 14606, 46035, 36481, 166621, 35417, 95382, 2957, + 59384, 60428, 36358, 66343, 75378, 22267, 22950, 83528, 17577, 56474, 25285, 4619, 179691, 75355, 95836, 53295, + 34588, 171410, 4487, 14679, 84208, 44015, 18562, 109133, 54101, 11531, 86052, 174479, 303157, 28095, 9953, 35642, + 14564, 39802, 16145, 77606, 117406, 53038, 121117, 53624, 22062, 1212, 7632, 127157, 237292, 189087, 10478, 127345, + 102515, 181997, 86752, 87623, 10966, 121602, 68783, 68681, 83042, 114380, 138349, 191305, 67176, 50085, 39016, 1427, + 42384, 1412, 67118, 122616, 72389, 25260, 2237, 13576, 137346, 19938, 20304, 2191, 68759, 5373, 61364, 238507, + 75814, 23931, 69565, 38993, 131741, 38364, 12528, 87762, 5679, 129853, 5310, 186831, 32653, 90338, 260176, 389531, + 108118, 26843, 43985, 50175, 30563, 25106, 56965, 18130, 140428, 4542, 165503, 117991, 24219, 229605, 1819, 129663, + 1240, 3797, 76093, 18398, 71339, 51919, 93043, 27175, 47060, 216257, 6483, 35051, 1217, 16512, 80798, 129064, + 13225, 69339, 8548, 237079, 72298, 2575, 34280, 51379, 117910, 55671, 53345, 247552, 29486, 39328, 140821, 34681, + 57045, 60177, 5004, 90269, 78522, 2479, 322607, 48474, 61296, 13057, 31558, 4678, 59271, 6699, 27044, 31988, + 35944, 12503, 83480, 4389, 136508, 3781, 114121, 70279, 4488, 155829, 42214, 2898, 68191, 75695, 305850, 45041, + 74344, 106509, 30087, 17429, 93292, 12477, 290, 23080, 114802, 35714, 18751, 26554, 105424, 17775, 2144, 2412, + 100610, 65192, 113975, 52975, 180272, 135050, 129815, 76238, 106483, 21440, 63186, 4260, 46189, 9711, 28249, 4169, + 23429, 23390, 8324, 141585, 63809, 67668, 38457, 38063, 39226, 59972, 1189, 203916, 62368, 14403, 16949, 61767, + 85801, 1739, 40147, 35049, 76757, 33124, 62102, 15780, 103593, 103009, 53484, 22952, 67973, 114645, 6566, 5245, + 50462, 7601, 8288, 3513, 194571, 80276, 1908, 54592, 5124, 58571, 2513, 6800, 273997, 193904, 1119, 17991, + 117245, 2508, 129156, 82366, 26278, 71465, 63341, 56943, 39662, 106116, 94966, 156875, 9736, 2204, 122308, 94418, + 27134, 1280, 24539, 49022, 45314, 3764, 50904, 46424, 30699, 28087, 293839, 9400, 33646, 40165, 822, 147499, + 50263, 116179, 29085, 11863, 31314, 5578, 17797, 5104, 12454, 1604, 15342, 219206, 10232, 67800, 94261, 25872, + 13565, 90339, 78971, 75377, 26649, 41184, 47695, 11514, 35369, 20767, 14227, 41953, 309396, 148270, 147938, 33074, + 14453, 27499, 109019, 39018, 25738, 240196, 158931, 52820, 8612, 95853, 21524, 137010, 84901, 70869, 70021, 116794, + 48404, 38771, 6732, 1070, 70990, 187297, 49140, 5238, 576, 3564, 253975, 16027, 16483, 2811, 37775, 19034, + 25259, 4053, 2000, 70083, 95774, 19713, 33431, 92703, 91314, 42381, 288770, 48194, 95985, 3991, 77418, 13406, + 241328, 245086, 56533, 35275, 62725, 9246, 51924, 70181, 95331, 16163, 31410, 79016, 39312, 120878, 119371, 275987, + 80124, 27712, 9186, 220, 23598, 146167, 85209, 68238, 282190, 57048, 31273, 30555, 80913, 17594, 75779, 59160, + 135002, 101219, 189377, 29225, 96735, 60126, 62522, 104000, 27620, 86814, 17240, 147533, 11001, 5425, 43682, 410, + 49460, 87270, 69480, 46315, 59448, 1816, 76201, 9431, 11788, 87960, 29063, 65539, 47347, 11678, 33846, 7008, + 196704, 9895, 6753, 8633, 120892, 59970, 572824, 115934, 6646, 202559, 892, 48351, 37611, 251282, 57823, 67263, + 57750, 26527, 34485, 90747, 7685, 88370, 6144, 64182, 1709, 41969, 21458, 62327, 181657, 49247, 225330, 122600, + 114574, 107124, 85361, 111833, 63243, 71420, 15655, 191178, 72430, 18063, 51425, 54002, 12364, 53225, 86557, 18193, + 97580, 41232, 138398, 67821, 128724, 8944, 233212, 101353, 52099, 42127, 14006, 120107, 32789, 32132, 3498, 18123, + 33758, 56058, 5779, 128760, 59888, 98869, 18445, 84702, 51911, 13234, 218379, 20093, 39031, 8074, 70195, 20708, + 23462, 24355, 131384, 60189, 26390, 10403, 41060, 7140, 10781, 49410, 42261, 87202, 82566, 41663, 43105, 60276, + 2768, 5733, 74176, 28329, 2297, 145430, 131632, 83615, 122915, 105441, 655, 224102, 5284, 136426, 67763, 16294, + 188511, 32538, 61049, 27893, 3394, 13951, 159099, 28542, 17930, 145360, 9492, 190122, 32285, 78855, 26440, 13570, + 58648, 73908, 4239, 124561, 2444, 74172, 53131, 11468, 10794, 73566, 11623, 35343, 64710, 30481, 4163, 10328, + 38309, 29901, 10538, 154377, 76132, 92405, 24839, 11679, 3465, 13449, 11637, 7824, 2337, 57754, 1260, 14458, + 41118, 19878, 38661, 13416, 159180, 37074, 163164, 54137, 28627, 52134, 184900, 8520, 40385, 29546, 30502, 22386, + 66527, 107458, 6850, 24022, 47983, 30603, 35083, 8934, 304066, 39500, 9, 28261, 33026, 77251, 9374, 44833, + 116312, 34990, 29236, 63563, 125639, 135405, 165398, 159055, 55690, 88141, 69643, 236964, 31983, 25572, 20436, 36746, + 60896, 31850, 16179, 11828, 5888, 3043, 66368, 9750, 31167, 7915, 53111, 36430, 1333, 64344, 93659, 20061, + 60596, 180191, 51630, 6792, 30244, 43509, 101058, 22409, 420, 44210, 109783, 43223, 27030, 72477, 72831, 32679, + 29235, 7675, 47556, 12258, 39907, 149412, 84926, 118247, 24692, 71717, 105038, 86009, 45941, 41189, 89453, 29856, + 52543, 30627, 226798, 67303, 59230, 67415, 34408, 1367, 99685, 16867, 128419, 52147, 4111, 125381, 117881, 16173, + 44093, 102224, 31575, 23234, 24870, 83790, 127407, 239098, 3200, 994, 1255, 100903, 242275, 117266, 55116, 38205, + 16140, 29662, 11307, 40414, 208793, 123355, 56470, 4862, 75600, 30119, 58218, 70828, 24075, 26974, 7802, 192353, + 4851, 5475, 78720, 66596, 3409, 28573, 64396, 30381, 30690, 59859, 88256, 5406, 99945, 103064, 34463, 37727, + 24238, 86643, 60088, 4057, 23741, 5967, 162904, 38240, 28356, 93858, 25510, 122879, 6897, 3278, 7057, 11971, + 4400, 35461, 211413, 21395, 59615, 39471, 87233, 55795, 128426, 3051, 22470, 41950, 14705, 3974, 180108, 80476, + 78442, 204996, 91987, 15634, 67610, 139015, 142373, 35611, 51134, 10387, 4353, 153456, 57749, 181039, 14183, 68447, + 151532, 21107, 36452, 20551, 3186, 46247, 46383, 129666, 88736, 140662, 146243, 2066, 8360, 7978, 64818, 106963, + 17896, 47801, 10723, 114821, 223295, 74192, 3293, 3393, 16987, 74064, 11277, 91622, 4270, 29828, 27951, 387869, + 103235, 1374, 61988, 120083, 477, 145892, 128378, 11779, 211263, 61354, 18221, 17869, 46530, 83061, 108538, 157981, + 90608, 67199, 95080, 49064, 195814, 12302, 66307, 10348, 231346, 160732, 112859, 63633, 146558, 21271, 31037, 198802, + 47622, 12862, 95710, 3910, 77850, 73961, 85585, 34752, 61000, 4082, 24595, 103679, 71107, 8208, 79568, 150019, + 16615, 24961, 139857, 32664, 197366, 4559, 54735, 32696, 4126, 162019, 75698, 13916, 70108, 159638, 19834, 9349, + 24675, 175560, 49643, 18206, 52459, 27992, 10809, 88865, 401975, 133172, 29000, 34558, 30915, 3658, 25834, 42430, + 36562, 125265, 18182, 10155, 40149, 97082, 208980, 19575, 60853, 90529, 66545, 9600, 789, 46420, 2317, 88593, + 55595, 98980, 115302, 5742, 169155, 1073, 177901, 3472, 11189, 63711, 78643, 65472, 50459, 127979, 93, 42202, + 67053, 21720, 157650, 11145, 141378, 42033, 22824, 85705, 79114, 35584, 15974, 1510, 54172, 28562, 12451, 104226, + 19190, 97151, 73024, 20948, 5151, 81741, 21499, 29006, 84183, 198074, 54003, 45120, 170125, 26240, 35177, 28389, + 64863, 79974, 60778, 176915, 232183, 45342, 2038, 80253, 41564, 40703, 32689, 5430, 100689, 5366, 23007, 134279, + 14266, 26712, 73993, 24934, 64242, 52113, 102887, 61801, 46415, 201049, 54251, 62133, 122757, 164883, 30815, 139966, + 2319, 30842, 766, 13362, 10287, 134518, 86111, 81665, 82440, 28333, 43019, 18963, 8804, 161944, 23439, 102144, + 101145, 80029, 39052, 248708, 30350, 117340, 11878, 128467, 974, 138625, 63961, 5237, 74778, 61834, 67040, 43814, + 13690, 65947, 33809, 232476, 115258, 181745, 28824, 94013, 9510, 10246, 93722, 81976, 7217, 114383, 3493, 16014, + 69045, 72692, 12145, 80981, 9507, 6692, 1620, 60820, 330444, 35474, 33962, 4797, 7053, 295463, 46445, 27026, + 12491, 77988, 49524, 35675, 90947, 29114, 166705, 101385, 133782, 32704, 6186, 84595, 176031, 185623, 45966, 151302, + 63069, 1699, 107491, 947, 15458, 74452, 196212, 6046, 10498, 12163, 10239, 35191, 243951, 9277, 9090, 29539, + 54460, 22820, 26514, 112549, 60372, 51753, 48756, 21812, 70861, 260326, 41, 44222, 10441, 16961, 48148, 138771, + 216194, 5914, 52153, 53400, 212036, 56519, 26245, 10117, 45888, 15294, 138019, 90913, 26368, 43842, 42111, 23348, + 6082, 194845, 161089, 156206, 51546, 11647, 30759, 302912, 262094, 8635, 78876, 26535, 35283, 54183, 31183, 85484, + 147873, 12989, 5197, 6356, 72894, 65347, 20150, 27370, 73787, 1493, 45918, 12366, 190217, 20724, 13858, 10981, + 67449, 81213, 7553, 14115, 72242, 271517, 11842, 48310, 88743, 143726, 22177, 3290, 243231, 58452, 62937, 12592, + 1654, 40066, 33477, 13751, 9921, 128442, 15868, 7106, 75236, 83773, 10775, 36938, 10482, 170465, 17368, 17469, + 161508, 32752, 98340, 800, 19824, 264456, 3901, 87319, 2867, 26782, 9630, 113102, 185815, 24197, 44584, 86366, + 40224, 3636, 140916, 31731, 267731, 9567, 53678, 72984, 29389, 27963, 17106, 50282, 284911, 60170, 8322, 12608, + 23374, 89652, 5268, 39044, 229766, 8869, 151350, 31436, 177342, 12269, 183212, 120418, 116270, 2843, 78888, 69192, + 7865, 184099, 1086, 129897, 18383, 70508, 20242, 18508, 229924, 124569, 35749, 50589, 55626, 9884, 83115, 40971, + 30671, 18135, 14452, 38861, 17844, 201826, 5549, 26413, 17189, 13561, 38539, 10679, 143331, 3314, 36785, 171194, + 49685, 187713, 67506, 4618, 104039, 17060, 195080, 50648, 33159, 19238, 67559, 134840, 28599, 157523, 17130, 38064, + 117398, 94355, 31918, 13575, 34538, 40326, 13997, 3494, 348283, 62481, 26862, 3603, 104426, 244363, 153709, 112487, + 304612, 199674, 41239, 35545, 54869, 293005, 28223, 26277, 26899, 4533, 18518, 15492, 38587, 80488, 70485, 160395, + 263, 60162, 11382, 222152, 4696, 250751, 51921, 182609, 10707, 48463, 46243, 1227, 49111, 111564, 46502, 33342, + 56846, 68541, 63559, 858, 139927, 16654, 229375, 76759, 26478, 33205, 95828, 23399, 92945, 2637, 35630, 28470, + 143992, 50214, 14174, 21456, 166191, 65665, 1711, 21594, 78019, 97599, 111701, 36, 147151, 110246, 189022, 43021, + 30397, 40757, 131935, 42065, 73335, 48039, 26596, 28984, 15102, 2361, 7421, 202167, 69744, 43766, 52826, 3642, + 83304, 33873, 75140, 63169, 192389, 36551, 92748, 13039, 123959, 233220, 21738, 84447, 77230, 20228, 187852, 19095, + 25799, 92136, 108774, 29237, 53947, 2299, 118106, 2687, 8830, 42331, 202924, 33667, 2023, 73763, 30704, 19363, + 19779, 16737, 35629, 48081, 24068, 101013, 162338, 291912, 13749, 24745, 328289, 167679, 70086, 48299, 23306, 16732, + 17801, 43322, 54589, 3586, 63653, 43624, 53474, 925, 109177, 251316, 43805, 13082, 19511, 86565, 142182, 92461, + 17117, 101033, 103319, 64589, 4022, 4351, 235897, 5352, 82705, 107142, 46391, 156084, 5860, 61365, 10558, 13045, + 7717, 18357, 33922, 12590, 33065, 6928, 46993, 783, 46937, 67846, 8952, 26295, 6107, 119656, 18799, 17458, + 50747, 4229, 179559, 112727, 118080, 20683, 41464, 125468, 51560, 49749, 44231, 7359, 35339, 62988, 136487, 67015, + 5208, 29150, 24956, 105186, 48858, 6143, 18097, 6972, 16404, 73489, 58742, 97196, 36357, 164616, 5834, 32267, + 13746, 147733, 15113, 132091, 34127, 106298, 39729, 106426, 22294, 9780, 15602, 36213, 71502, 42808, 66802, 599, + 60755, 5851, 39120, 67363, 108623, 126368, 72770, 91263, 32486, 30596, 151717, 7951, 52002, 43103, 11768, 68942, + 40901, 39344, 24037, 127500, 116890, 48403, 16926, 86750, 17745, 48648, 159545, 34460, 58419, 5634, 114317, 67865, + 31462, 23352, 24010, 98185, 125708, 69686, 68337, 13610, 26271, 70691, 2980, 4768, 27225, 102402, 75453, 28106, + 8104, 6931, 1176, 6274, 6475, 112635, 22498, 6176, 238686, 26832, 28893, 90319, 14441, 15682, 15087, 39517, + 45270, 109134, 104440, 45965, 47645, 81772, 7876, 52683, 87720, 12898, 4505, 185665, 2769, 113401, 15664, 57592, + 105229, 137381, 97059, 119268, 6876, 43309, 33886, 128363, 35476, 144249, 67013, 143587, 83367, 25703, 91436, 59347, + 53236, 2289, 16519, 19844, 46309, 58558, 99834, 23313, 218816, 231303, 36388, 51333, 183535, 109792, 139277, 54306, + 90139, 18235, 8275, 32710, 37677, 82464, 86025, 92204, 88842, 117723, 37570, 128723, 234242, 76350, 73795, 34896, + 148247, 58424, 11105, 11744, 45746, 63372, 17118, 49772, 199520, 81902, 38004, 22911, 33752, 3125, 1995, 53792, + 4689, 26909, 108150, 146062, 69674, 41811, 161444, 84855, 8999, 28561, 16731, 93937, 3189, 21967, 24890, 22943, + 1356, 145300, 51569, 28802, 517, 118679, 31703, 40607, 48098, 108854, 25003, 10233, 73969, 177495, 5248, 24516, + 215347, 146192, 48712, 60626, 69188, 40735, 5866, 586, 101541, 6509, 47590, 52129, 5969, 222045, 110933, 25733, + 24223, 65339, 62812, 2414, 155418, 35819, 16022, 78423, 43138, 20995, 128255, 240673, 46745, 236093, 72176, 57085, + 97841, 61248, 107, 36068, 193177, 105427, 55726, 215229, 20446, 47228, 100420, 87091, 14429, 121708, 23605, 21157, + 187721, 21880, 2997, 203976, 99166, 95068, 25877, 7724, 98925, 83401, 4829, 13182, 18229, 13718, 239662, 38653, + 116505, 153497, 30589, 89029, 38962, 181302, 43853, 78872, 180301, 4786, 248240, 7401, 106136, 112590, 77745, 19731, + 60880, 77789, 125748, 135487, 5975, 48627, 34084, 12419, 215770, 47557, 254582, 10364, 106495, 21856, 67539, 88981, + 38805, 21428, 48732, 42316, 12149, 16078, 52808, 25327, 51322, 33850, 51147, 12253, 122354, 46077, 56483, 254553, + 115417, 81834, 150991, 94662, 86668, 7381, 12841, 100650, 18218, 15741, 22372, 68294, 50705, 15535, 84660, 61887, + 22553, 72299, 31361, 24824, 17743, 46820, 64288, 31582, 77006, 111674, 116384, 30760, 80920, 86149, 77192, 51979, + 79691, 60342, 122805, 103800, 240873, 160744, 233114, 78962, 54920, 8608, 3484, 316104, 72548, 24337, 5088, 230040, + 21926, 10172, 36838, 26, 86221, 83458, 102176, 12062, 17571, 41929, 41170, 28428, 68239, 41750, 103930, 2634, + 18313, 53019, 34825, 97837, 63115, 24606, 73157, 152474, 14715, 91439, 37033, 109806, 140259, 30668, 174760, 380, + 135597, 95673, 136073, 65073, 134249, 13829, 17279, 122305, 4420, 46444, 10237, 64848, 203623, 70728, 10349, 182885, + 65075, 24519, 25783, 40318, 34139, 22222, 63394, 55266, 102764, 41422, 20126, 65100, 90408, 53640, 35128, 48932, + 11192, 38935, 96839, 34782, 39492, 19396, 41332, 6250, 5511, 19492, 51304, 25936, 104466, 54099, 73771, 86115, + 5080, 7669, 30891, 111700, 13931, 25276, 72289, 135447, 14820, 258641, 25265, 31005, 281179, 75286, 393, 95359, + 14623, 13584, 6680, 101227, 80173, 44933, 76666, 54542, 13244, 39348, 458, 25379, 109451, 134348, 81143, 6959, + 65554, 12027, 51311, 8716, 57589, 140731, 28467, 23316, 17272, 30458, 25980, 55229, 77197, 83798, 28302, 114784, + 7428, 34548, 26241, 14712, 39336, 103304, 18928, 54080, 12870, 334, 87722, 15208, 16895, 142098, 114262, 39820, + 83913, 57817, 28682, 7721, 14900, 108672, 11250, 62246, 42849, 415188, 1724, 26555, 24549, 25505, 26443, 107450, + 145899, 61035, 43528, 6901, 60726, 65906, 267741, 21338, 147590, 42079, 18924, 73017, 135236, 15393, 5206, 4026, + 84185, 1531, 5988, 113890, 82647, 303391, 7386, 69844, 71611, 189865, 76523, 31877, 13315, 19314, 198575, 32821, + 1928, 67641, 25913, 104475, 103489, 3297, 70391, 18406, 15446, 113347, 19295, 93790, 27856, 1792, 167471, 116449, + 8541, 4408, 41757, 63233, 25765, 86680, 64501, 27034, 24816, 34975, 6079, 4486, 49693, 36229, 16917, 21581, + 62426, 27862, 11612, 54284, 35702, 194034, 355, 24277, 48262, 87411, 70504, 310164, 118018, 12516, 47559, 43502, + 57433, 107139, 9290, 66533, 80863, 14634, 34312, 91725, 28606, 21342, 67241, 72355, 43244, 375789, 37402, 174015, + 105070, 8342, 44167, 67494, 1890, 16365, 11723, 271002, 1865, 47918, 8350, 45564, 27742, 25110, 125803, 8553, + 49504, 81925, 62211, 4534, 15491, 19011, 80373, 206920, 667, 102405, 128623, 245524, 5553, 113309, 192739, 65766, + 19567, 22832, 261958, 29679, 21293, 71134, 20962, 105123, 24721, 860, 21752, 33448, 18372, 157167, 94822, 35770, + 173224, 232737, 75729, 28937, 46828, 28062, 25453, 5207, 140366, 36665, 30652, 6169, 67920, 150458, 92040, 23186, + 184604, 92330, 20891, 176492, 49427, 27828, 38305, 42495, 143982, 49560, 25503, 90043, 29747, 65328, 47830, 12932, + 11068, 77721, 9003, 25213, 94205, 140426, 46090, 89945, 138173, 192691, 33329, 112232, 129905, 35709, 27514, 1841, + 19957, 31411, 127476, 53572, 17497, 173549, 55063, 175135, 19841, 69314, 5192, 237921, 117660, 150697, 4060, 273045, + 50414, 98940, 65348, 153665, 164423, 58804, 156695, 48994, 213928, 86036, 28608, 8355, 39574, 34540, 16927, 135680, + 18374, 151587, 10830, 53805, 16878, 16623, 4282, 48030, 8537, 14986, 46102, 13062, 72897, 72, 33050, 108227, + 39451, 45935, 651, 113320, 40535, 95176, 57450, 48843, 5003, 19019, 10407, 211163, 3848, 1068, 4988, 32091, + 30095, 41692, 15099, 43602, 107434, 50744, 7627, 171349, 16313, 150832, 352665, 207750, 33937, 38256, 51091, 156000, + 87889, 90663, 84175, 24908, 114900, 50365, 31494, 83829, 5398, 169342, 47521, 54818, 18935, 8356, 43094, 41212, + 174536, 10082, 92550, 6678, 60614, 23355, 69721, 14796, 34149, 128830, 58187, 3179, 208, 40325, 28399, 225029, + 401412, 51150, 31580, 207268, 6657, 10993, 69818, 64282, 289845, 23308, 12961, 38447, 6681, 52944, 31855, 2572, + 47646, 120728, 179148, 37240, 45196, 218274, 4816, 3695, 21961, 50084, 35209, 18073, 51452, 27004, 6100, 33941, + 1377, 84831, 171214, 85, 141510, 9078, 99227, 32610, 6417, 11718, 49868, 65579, 87902, 73018, 49062, 46280, + 61742, 21512, 40862, 107733, 15941, 29168, 157765, 144919, 14487, 5767, 158014, 140070, 7241, 573, 71584, 16921, + 223566, 40331, 179473, 35081, 47926, 140885, 41508, 52104, 59180, 42310, 32811, 29048, 123517, 102413, 80208, 10104, + 14746, 12649, 153641, 126022, 37965, 113017, 4171, 83, 142592, 2809, 6362, 50416, 71323, 116894, 260776, 16204, + 1524, 5760, 30351, 12658, 20703, 54403, 36083, 45408, 74772, 4946, 14485, 50759, 111222, 10890, 2195, 167147, + 92962, 130534, 16283, 177256, 35016, 15472, 210156, 151187, 73922, 117691, 43250, 52051, 37392, 24811, 24358, 30830, + 5775, 818, 21969, 1476, 127322, 151783, 58392, 31021, 106913, 65215, 89407, 90802, 28531, 11690, 20234, 95249, + 44602, 37256, 18707, 11928, 5161, 4410, 26571, 51903, 49768, 22008, 25252, 65780, 209499, 68769, 203726, 13249, + 137363, 48845, 86823, 6658, 5674, 31881, 1083, 1823, 108676, 34518, 166752, 13791, 14287, 91576, 91429, 8665, + 11529, 26401, 16191, 91972, 30964, 5254, 28486, 54697, 79613, 66520, 18447, 22870, 45203, 194466, 22822, 51703, + 12278, 76716, 44595, 73455, 33546, 12235, 144843, 36154, 51247, 11116, 33040, 3180, 225753, 60864, 1972, 28469, + 12891, 28879, 10338, 144157, 56294, 353058, 38302, 41447, 87532, 110616, 27065, 168438, 6557, 1213, 50804, 144643, + 24817, 2390, 136531, 38174, 247513, 16190, 4059, 122791, 131994, 137430, 39506, 57650, 16305, 5188, 54309, 106128, + 20628, 88071, 67394, 395446, 250285, 66176, 91254, 1399, 114196, 43915, 60230, 44853, 27206, 106353, 43013, 18733, + 345105, 226453, 51202, 16607, 57106, 117175, 35492, 10476, 89598, 127439, 15187, 39624, 13688, 61570, 10615, 31111, + 59370, 6238, 175252, 32143, 224492, 41388, 95408, 34384, 148238, 78307, 38959, 9340, 160091, 61443, 15737, 11216, + 41244, 170, 38299, 102443, 113097, 26382, 14027, 33707, 3957, 76300, 66160, 19431, 18900, 6952, 1717, 108656, + 82206, 188021, 257335, 27295, 43999, 41210, 31777, 46956, 57457, 12657, 11489, 15697, 48060, 204748, 53583, 82422, + 284790, 30503, 137341, 8120, 19615, 220311, 15991, 10217, 63424, 9808, 67431, 70976, 98221, 4491, 15177, 28535, + 144789, 751, 13230, 2394, 1504, 33977, 132104, 30316, 22230, 931, 97193, 185240, 24826, 22687, 174322, 15307, + 22988, 1390, 188745, 180325, 29580, 59068, 74903, 18994, 29195, 79, 15436, 7622, 38462, 11566, 138710, 44828, + 45774, 37768, 99236, 68137, 84083, 19282, 22698, 17134, 74807, 126662, 173497, 46248, 16938, 119735, 3212, 28292, + 213652, 49013, 9975, 32180, 45660, 86250, 4801, 68788, 95490, 77482, 113751, 11994, 44624, 94452, 46839, 128497, + 100316, 5798, 58588, 73184, 202987, 65417, 37790, 88524, 1606, 43156, 97964, 105717, 34947, 11203, 100060, 37742, + 130074, 93653, 107799, 94311, 196106, 41347, 8035, 10780, 16390, 27883, 118236, 167395, 1979, 25006, 19375, 31628, + 18916, 144723, 78502, 114047, 103107, 86492, 107686, 5844, 20934, 206963, 23556, 22591, 16562, 146333, 20167, 10471, + 117434, 33085, 2863, 9740, 36669, 41849, 37271, 22790, 18209, 28979, 8231, 12952, 54408, 21731, 25130, 45208, + 55748, 138120, 75826, 414, 29593, 9925, 292865, 25999, 683, 123149, 7036, 92159, 86055, 61827, 103680, 23176, + 54918, 58466, 57578, 13305, 5709, 86479, 16697, 31064, 17660, 200919, 10770, 49793, 33423, 32370, 52047, 16488, + 62555, 6459, 8426, 83493, 7763, 59725, 82812, 18628, 67760, 79405, 68557, 9612, 7673, 28102, 56517, 69620, + 171797, 32458, 29541, 15870, 81109, 32080, 207644, 71495, 21202, 11039, 91036, 61230, 2810, 130800, 32260, 4613, + 60590, 37112, 75214, 33979, 126402, 155062, 30642, 63875, 12810, 194463, 82799, 47664, 16725, 36685, 43367, 61099, + 449, 172150, 102867, 21691, 301838, 36745, 7130, 18671, 57316, 34852, 38034, 54182, 35578, 65900, 99486, 19771, + 3456, 2658, 16914, 99866, 28390, 28109, 8262, 21147, 34353, 20006, 4228, 137085, 1675, 203023, 283196, 198286, + 214375, 163329, 290603, 152574, 40471, 83506, 30068, 14730, 23177, 131539, 34759, 27668, 32178, 71896, 104799, 116305, + 85430, 119262, 42860, 25160, 8911, 23428, 49437, 105322, 6519, 16203, 6349, 74711, 1230, 38045, 8540, 75165, + 44736, 25909, 51026, 317034, 4984, 32281, 91312, 27060, 44431, 17817, 45363, 155937, 239085, 35697, 59784, 91993, + 29531, 126740, 213757, 76560, 167776, 285273, 24262, 8237, 65030, 41160, 74437, 48804, 118916, 13159, 37842, 1031, + 75349, 1478, 11655, 108777, 23435, 277425, 101734, 67469, 70231, 124711, 43532, 28514, 65526, 54956, 1000, 21882, + 17728, 25302, 40952, 52214, 149632, 1999, 2111, 3259, 63362, 89961, 220561, 39777, 26335, 9063, 10572, 12416, + 34551, 34623, 38604, 24723, 5947, 15588, 69927, 66252, 119177, 69173, 46629, 28714, 70715, 212408, 20521, 406913, + 74380, 11716, 50659, 50862, 37009, 88460, 130101, 7210, 53853, 538, 65120, 151950, 55806, 163748, 52837, 13153, + 21100, 16674, 64536, 6091, 138201, 44837, 58547, 3723, 163, 2177, 32288, 85454, 34033, 8497, 14282, 25742, + 10535, 10741, 79559, 117493, 243787, 49337, 100718, 79495, 40139, 42956, 7551, 55433, 15421, 31509, 23034, 45081, + 547, 61176, 53434, 328001, 8470, 36263, 30145, 4519, 74173, 53935, 11845, 73774, 60211, 78025, 3, 4102, + 73782, 109293, 315332, 48412, 26683, 13714, 6865, 20128, 18490, 104141, 325, 39470, 171970, 115860, 15707, 7268, + 73301, 74336, 31370, 2368, 111827, 107757, 136231, 142844, 97138, 96638, 84053, 38691, 23801, 1588, 10573, 122098, + 77039, 240, 186135, 146101, 11996, 18143, 112963, 46171, 155836, 348769, 47795, 121213, 116266, 132515, 3344, 144804, + 31286, 99187, 255838, 129694, 35894, 48779, 55235, 148582, 71967, 65282, 15174, 13920, 47080, 6147, 108242, 157593, + 125025, 7136, 1286, 28957, 127956, 28402, 98813, 20805, 7532, 109417, 40610, 5041, 32958, 15142, 18408, 108596, + 33543, 50517, 27748, 80114, 233434, 91447, 487, 37094, 100048, 30541, 43477, 10639, 89862, 155868, 37667, 8726, + 60684, 237903, 73408, 99589, 12190, 38739, 97348, 3914, 13594, 2680, 149016, 13907, 30171, 28343, 23530, 115225, + 61104, 35821, 147679, 14337, 4297, 244282, 24085, 326976, 56428, 7851, 21303, 131620, 71446, 83253, 68692, 111870, + 5224, 15813, 38197, 49026, 45057, 13660, 3306, 76345, 40671, 27905, 91072, 996, 68527, 62085, 91351, 122634, + 55109, 168209, 2024, 27560, 112707, 17352, 8306, 167115, 169921, 166958, 5031, 46020, 11844, 67284, 19130, 76185, + 6920, 32849, 5450, 14610, 22451, 21002, 17392, 31872, 66682, 84796, 13709, 40210, 59898, 12029, 8719, 53564, + 21462, 91884, 21647, 88379, 194428, 12754, 37797, 132826, 160016, 22567, 54383, 53186, 77611, 31107, 8339, 4694, + 19185, 90355, 23597, 17222, 140675, 28442, 23668, 55977, 9128, 61555, 28774, 155229, 17658, 9390, 24379, 69357, + 15752, 127381, 239631, 62460, 93181, 55913, 45133, 140155, 18676, 25249, 33164, 29581, 82837, 67223, 22362, 29975, + 7317, 52813, 1943, 29613, 20012, 207130, 49617, 49651, 5636, 15334, 36313, 29226, 28084, 95247, 72072, 19000, + 224932, 15811, 114, 32127, 38097, 37508, 88507, 37225, 27359, 91626, 12193, 69279, 20608, 11055, 88156, 92808, + 2152, 57259, 55275, 72789, 24475, 104414, 1708, 9882, 3818, 48661, 66897, 1631, 34806, 227930, 85815, 87753, + 18321, 250664, 72733, 25107, 206797, 50891, 8082, 196411, 92596, 96764, 152823, 65514, 22819, 387277, 62176, 51225, + 40329, 15563, 189, 3659, 73670, 64357, 51793, 275136, 33482, 86653, 74615, 67058, 11318, 125720, 15388, 22388, + 8267, 1730, 102663, 170910, 40784, 7144, 85373, 13040, 7088, 94309, 583, 44224, 140424, 77439, 18496, 164026, + 36578, 4722, 9151, 5824, 63365, 26510, 35199, 40500, 79277, 32495, 44614, 35233, 9566, 203293, 152144, 7097, + 2330, 183480, 98629, 13423, 330887, 44130, 68600, 30939, 97829, 31012, 345465, 56747, 94879, 4939, 160027, 149761, + 99423, 46099, 32251, 15332, 8761, 96094, 128555, 5763, 235318, 222223, 55729, 30241, 55420, 201746, 3987, 81382, + 8259, 49325, 23287, 7719, 24633, 251100, 92311, 18591, 110533, 64759, 170260, 393860, 7175, 21144, 132887, 3593, + 75346, 101277, 91109, 16387, 259187, 11627, 57459, 173829, 44694, 55780, 49797, 89192, 120443, 62622, 3904, 14814, + 23887, 1027, 112258, 64955, 99800, 11132, 66353, 36202, 48624, 18158, 88481, 96882, 43059, 11040, 2455, 7077, + 21651, 181159, 99126, 100434, 61388, 68186, 19161, 110468, 120052, 8819, 55324, 41494, 7014, 37689, 3618, 87729, + 92615, 207943, 9823, 128657, 12587, 15857, 6379, 67628, 51216, 71775, 157617, 63244, 1503, 3864, 218754, 110864, + 5769, 21492, 7243, 1192, 87921, 85529, 31512, 18537, 42698, 35350, 73510, 84474, 34301, 8991, 21013, 35034, + 566, 38832, 19838, 35586, 37216, 39413, 55006, 12178, 59742, 856, 84563, 6900, 25632, 17437, 49786, 30723, + 13847, 70845, 4044, 7843, 23944, 235976, 55530, 48942, 6518, 20939, 73769, 192653, 52936, 95207, 23895, 132542, + 142982, 22632, 87452, 48042, 54018, 178468, 10728, 26230, 23559, 363, 81269, 142012, 5718, 346258, 31456, 84333, + 246476, 51018, 66692, 101804, 120570, 39962, 30373, 70593, 2864, 60541, 19425, 54209, 104092, 7201, 31545, 48018, + 25865, 15442, 46257, 40443, 8328, 6451, 111782, 47527, 97754, 33046, 470, 245116, 31095, 39, 91934, 87208, + 73470, 36708, 36521, 12801, 70624, 36272, 8892, 79768, 12427, 55454, 103756, 5908, 52390, 62962, 22720, 141138, + 94634, 41689, 128402, 126390, 6628, 106394, 35527, 134394, 82727, 254651, 194502, 148064, 89549, 3202, 28359, 957, + 21954, 27906, 49840, 142747, 8307, 24206, 48978, 1186, 71728, 133038, 71474, 91306, 6333, 110959, 74600, 70387, + 18983, 62609, 56057, 22970, 1147, 135850, 1321, 28834, 3578, 59715, 102227, 32827, 81415, 99952, 55636, 257598, + 390, 22702, 35701, 85872, 402916, 39216, 189795, 14929, 19467, 10112, 144422, 61514, 5279, 63421, 134686, 41436, + 8424, 51925, 10598, 132295, 124416, 4604, 194739, 210929, 57866, 31829, 51626, 50007, 9976, 91878, 61906, 56168, + 81906, 60918, 61859, 40017, 23059, 16887, 40927, 62064, 12785, 32893, 32913, 21782, 93965, 20169, 44387, 79084, + 38463, 11457, 93950, 27127, 157050, 2697, 337088, 5116, 54128, 48255, 33279, 8821, 27352, 25515, 124022, 65710, + 28906, 38557, 33390, 1722, 104435, 72215, 38551, 12094, 30978, 25113, 6671, 37355, 175109, 42862, 98024, 65406, + 221276, 59624, 118012, 64637, 78760, 86697, 21426, 1639, 40350, 12584, 67193, 84144, 31396, 7863, 143011, 69629, + 63112, 9454, 28666, 65798, 46372, 134721, 6314, 51402, 30837, 151922, 2847, 38676, 38008, 92823, 136245, 17540, + 5504, 109295, 205242, 37606, 5211, 214892, 1586, 20670, 208711, 137743, 19328, 40652, 16995, 20023, 14657, 154919, + 34422, 12996, 13918, 38221, 47690, 16398, 2959, 37680, 89122, 6721, 198469, 91876, 172043, 83898, 101992, 26084, + 94570, 3635, 76958, 22853, 76497, 38266, 176590, 168403, 44464, 142840, 79180, 184594, 1984, 41806, 83147, 11985, + 6546, 366068, 59732, 24533, 271505, 8736, 39084, 222992, 93429, 28962, 58985, 86665, 8432, 30028, 14548, 32439, + 54424, 165029, 55175, 27458, 69046, 121277, 46168, 33732, 20661, 24581, 135574, 123110, 37556, 79260, 72611, 16957, + 12939, 46162, 58238, 44907, 72936, 253758, 41324, 32518, 96480, 11949, 124438, 65280, 43256, 34107, 53533, 43531, + 37037, 28366, 45970, 32741, 173438, 6121, 194202, 62969, 26355, 30314, 58370, 28455, 1848, 50519, 82830, 90393, + 21761, 295490, 10936, 256940, 133568, 44050, 20269, 4089, 27457, 21610, 219460, 36743, 14821, 101388, 52005, 13124, + 30979, 140816, 167362, 26054, 18458, 60789, 34917, 40447, 26606, 33422, 9066, 3452, 83614, 5761, 20263, 137238, + 25038, 91310, 101, 52322, 74548, 42572, 38084, 214054, 186568, 31802, 17665, 30620, 141936, 37730, 14420, 4265, + 187218, 49640, 188208, 51441, 55388, 96452, 66659, 40869, 42039, 60967, 221027, 19234, 178581, 29105, 96050, 9165, + 196118, 157335, 3738, 40354, 117436, 2965, 34136, 59659, 15570, 50843, 230035, 31444, 71260, 43886, 18316, 5387, + 38500, 168508, 17406, 32174, 8828, 103373, 143806, 90367, 3560, 18719, 122310, 16508, 26719, 2541, 105429, 6645, + 37998, 73190, 10591, 235916, 49737, 87112, 233941, 53188, 32193, 79154, 4544, 52905, 126477, 7580, 63501, 57314, + 3216, 31337, 6541, 103083, 60846, 49, 9756, 15481, 1355, 43840, 14319, 13743, 27486, 10222, 73114, 230718, + 418644, 16706, 6674, 279748, 23058, 45273, 295831, 86306, 2743, 5535, 88773, 21829, 35253, 120938, 31153, 3169, + 16839, 42847, 8751, 80974, 33942, 36867, 35514, 16485, 26474, 77775, 56877, 5391, 48346, 3882, 108713, 31403, + 27804, 55248, 26235, 43821, 136104, 40118, 175507, 28034, 203908, 18732, 1788, 34030, 106427, 36958, 54359, 7251, + 44936, 15356, 69139, 455, 157915, 22173, 140291, 50348, 43275, 82066, 49621, 54952, 15216, 36226, 96695, 66855, + 6936, 1987, 8227, 196087, 4631, 68827, 99004, 47541, 110265, 17953, 147605, 110242, 58520, 31312, 38724, 329975, + 642, 3155, 34497, 75937, 6207, 73843, 6120, 17249, 51429, 117746, 3218, 910, 68961, 319671, 14938, 29555, + 34700, 1649, 66673, 72268, 9655, 76800, 153087, 6941, 210168, 27130, 35398, 1780, 73242, 3135, 56689, 19556, + 165307, 8765, 35967, 121458, 13333, 70453, 17350, 117253, 22265, 13340, 44265, 39869, 441, 3742, 135025, 23581, + 33309, 16543, 17731, 13291, 157637, 283005, 21408, 101360, 63887, 52312, 83873, 5338, 233779, 23759, 186949, 34531, + 177320, 38069, 156465, 91004, 19353, 59852, 68160, 14891, 1338, 1072, 29823, 1950, 28901, 81407, 313445, 73038, + 84807, 162348, 240257, 37162, 138934, 16111, 58013, 41253, 102951, 16457, 96056, 19541, 56402, 67217, 41638, 94381, + 89674, 29481, 37456, 80815, 151579, 13937, 13683, 132537, 19699, 134545, 67020, 29816, 222341, 141235, 427578, 48868, + 129557, 233342, 23077, 87871, 16213, 18728, 16184, 9469, 37913, 19680, 2798, 171356, 178328, 13216, 50049, 72690, + 71904, 124644, 55455, 7504, 29052, 41036, 266546, 19899, 30391, 188755, 8659, 59469, 16, 104298, 112943, 53865, + 76203, 138226, 68857, 139953, 14125, 107625, 119795, 173133, 4398, 50273, 48808, 54390, 16466, 122086, 31835, 67035, + 50971, 48859, 7508, 46427, 66477, 73021, 84615, 39985, 83076, 46779, 201569, 53336, 36443, 60865, 168164, 143810, + 51393, 25548, 169307, 32896, 24485, 38424, 21837, 29087, 275813, 51674, 6714, 64883, 46169, 187369, 55186, 76192, + 12852, 12018, 62134, 31067, 118303, 16542, 12125, 10579, 4928, 26291, 43854, 7091, 10946, 253716, 109062, 39283, + 17261, 113012, 258512, 47764, 125126, 32646, 55892, 80279, 201623, 149872, 3192, 385, 1208, 48750, 5376, 58738, + 22335, 5427, 82416, 47811, 32435, 143086, 38930, 94128, 59975, 156037, 37977, 38224, 62485, 7698, 50405, 71027, + 16462, 21559, 136153, 34131, 107506, 162069, 63703, 3101, 215029, 40407, 4178, 3774, 9187, 80019, 17880, 97926, + 67579, 2600, 18405, 8351, 47924, 86638, 70820, 92206, 86453, 29610, 42241, 119200, 3198, 15466, 67813, 57863, + 35454, 4779, 99518, 4649, 104641, 144269, 33730, 38073, 65864, 6838, 109456, 193298, 154007, 5623, 45741, 30846, + 182578, 25573, 157224, 1543, 58575, 138703, 146140, 44971, 49356, 18275, 59064, 20300, 13122, 11848, 24453, 11973, + 9797, 86843, 2919, 25530, 49210, 1130, 161220, 76788, 75373, 85604, 34926, 36014, 17777, 17255, 51533, 11676, + 92226, 51845, 119859, 21525, 5936, 18507, 28050, 1140, 31418, 14857, 34207, 47859, 10750, 36382, 32079, 106909, + 59426, 87757, 38393, 110042, 15965, 97104, 33757, 35344, 97993, 53979, 33651, 45407, 41884, 82515, 173089, 7177, + 58371, 35365, 47543, 51927, 35587, 10670, 23544, 29306, 84233, 39976, 76076, 62097, 9007, 8668, 28119, 78281, + 120790, 19835, 143020, 54968, 18670, 64959, 20649, 34469, 42570, 33001, 136570, 87796, 120044, 1106, 58700, 63951, + 127623, 12805, 83057, 40212, 31773, 49850, 7361, 54336, 347524, 101314, 23751, 19569, 48791, 29174, 49369, 20467, + 7465, 75842, 38281, 623, 112457, 60210, 28849, 51003, 94720, 6426, 90047, 85560, 43761, 3579, 85105, 34607, + 90410, 118528, 7224, 42907, 111163, 18168, 6960, 161135, 191298, 5247, 100584, 127552, 171568, 20121, 91173, 12636, + 54615, 20199, 63730, 98105, 2396, 40387, 14438, 125012, 4765, 33235, 12865, 45299, 37728, 82098, 77872, 114037, + 59253, 19675, 24838, 398016, 102561, 11446, 17069, 57508, 178277, 65836, 99941, 26114, 2585, 271882, 136866, 50126, + 11027, 155648, 118367, 14585, 8910, 123015, 335383, 40434, 41016, 53021, 14439, 87098, 176860, 201543, 121888, 2358, + 9286, 5739, 22666, 54270, 37884, 169381, 33984, 93859, 16124, 89364, 72207, 51639, 76366, 99029, 65812, 2198, + 12147, 174891, 194289, 6986, 30252, 88822, 21284, 11445, 288337, 160821, 33034, 100869, 43852, 25761, 52882, 1144, + 103809, 1924, 84458, 86079, 43411, 13542, 139276, 18141, 34978, 41298, 7276, 26481, 173800, 33210, 17951, 142652, + 33616, 33677, 2210, 19941, 98568, 2486, 192414, 80136, 12058, 235883, 50963, 249638, 29572, 27221, 47034, 6124, + 72107, 63346, 97620, 158513, 299699, 40388, 23235, 37176, 224244, 198386, 121323, 67992, 23827, 63170, 17838, 106622, + 158590, 26807, 5345, 23489, 91891, 55474, 74834, 37981, 13058, 5977, 72552, 34706, 26828, 145172, 19904, 21367, + 34043, 960, 77092, 91381, 4733, 47446, 7680, 41697, 5170, 16960, 14741, 46101, 13656, 473, 51842, 37433, + 11103, 11551, 121951, 13191, 97536, 165932, 50397, 51628, 129028, 9069, 44885, 6590, 59195, 47045, 32940, 225472, + 90345, 21833, 13303, 29407, 96615, 141951, 5198, 6028, 18395, 7181, 3861, 14966, 156358, 167182, 36529, 55253, + 25942, 173153, 30959, 27261, 50691, 150176, 162201, 38467, 48462, 80602, 42163, 118482, 168, 108756, 26011, 17166, + 54149, 456538, 22512, 91374, 13816, 90358, 131615, 18132, 226707, 1824, 28139, 26860, 42253, 93877, 77351, 65575, + 8980, 80574, 22020, 27948, 40422, 91324, 76376, 13528, 39281, 91685, 82215, 122541, 144066, 1983, 193851, 17283, + 26320, 2739, 194978, 4790, 26845, 42627, 61300, 65815, 174612, 55133, 4200, 191130, 79771, 158321, 52280, 166796, + 221620, 62461, 11278, 4067, 88152, 83409, 31717, 121367, 13522, 47325, 37945, 10406, 174348, 249321, 154101, 64912, + 29938, 51775, 17220, 15776, 166138, 78890, 84425, 54121, 42861, 16368, 24572, 291647, 10197, 32073, 22651, 11677, + 97509, 26952, 35787, 18424, 41910, 71614, 94977, 72318, 41594, 70024, 275419, 37702, 60199, 7335, 39107, 61315, + 18271, 18394, 33768, 87884, 104277, 123724, 7277, 56288, 71981, 189803, 49320, 3352, 6798, 14240, 8954, 69220, + 94433, 57372, 28620, 68863, 193727, 85575, 42309, 41667, 67689, 42081, 22543, 44824, 12719, 28540, 114236, 101553, + 27638, 27296, 4300, 5353, 4663, 19379, 94098, 3758, 95888, 95144, 80344, 87320, 28447, 259518, 12718, 71391, + 152731, 37063, 24132, 31911, 104896, 15672, 103782, 1521, 4945, 72541, 23717, 122632, 15619, 87175, 206120, 29428, + 189780, 61416, 28350, 44457, 972, 1175, 47233, 198738, 95789, 41907, 21953, 97034, 59341, 22864, 53713, 16873, + 32971, 20693, 20954, 31336, 21477, 16169, 38370, 16412, 9019, 3841, 24599, 21938, 17085, 6484, 81198, 76413, + 5849, 72514, 12320, 65247, 276175, 37234, 59796, 52642, 16312, 57349, 198507, 94148, 46134, 18958, 125552, 1747, + 18725, 151873, 14901, 5490, 68287, 29470, 3689, 64794, 40814, 26018, 25692, 54450, 2703, 88278, 124886, 173087, + 174000, 24159, 179477, 24276, 46004, 201876, 209202, 445, 52876, 31948, 30206, 157610, 39180, 18439, 44124, 50469, + 5774, 96278, 222758, 200216, 50290, 45486, 20435, 46986, 46276, 140133, 142326, 15569, 13363, 47522, 92583, 2182, + 7135, 16853, 22998, 30272, 4952, 63263, 35623, 39096, 53789, 44864, 20053, 110392, 124213, 4630, 16087, 28221, + 127787, 25839, 77481, 44693, 13464, 113146, 6983, 27069, 55717, 50102, 4760, 7107, 26186, 66507, 59145, 36032, + 104182, 71328, 29425, 64317, 50781, 47465, 94298, 69706, 74899, 22754, 120756, 25108, 93077, 56834, 73286, 39928, + 16218, 41699, 176763, 7555, 70819, 50083, 26895, 23315, 26014, 16773, 123079, 41712, 5719, 31516, 90427, 158540, + 85051, 183128, 40864, 27505, 55392, 9058, 45224, 96857, 30901, 136622, 96557, 56304, 120061, 11501, 151448, 5773, + 89743, 7769, 86069, 2935, 18471, 41628, 10114, 33660, 110170, 49479, 26745, 92846, 33221, 26731, 18795, 87076, + 8550, 2100, 29972, 120289, 3077, 72490, 33784, 2630, 208722, 50861, 63483, 79029, 6419, 39467, 14302, 45286, + 64207, 9686, 67513, 44170, 1050, 77246, 59266, 17055, 53801, 7150, 11111, 42432, 4278, 94579, 362117, 36175, + 42902, 41933, 39002, 98489, 22913, 74161, 84773, 57036, 17556, 162288, 74485, 178760, 93867, 73635, 128860, 50362, + 261, 67455, 80001, 46080, 35662, 4368, 25247, 19230, 74393, 22588, 1822, 27682, 235324, 13798, 85998, 13194, + 235067, 23514, 71669, 147632, 23191, 134748, 214683, 105101, 1518, 25489, 247114, 7380, 54842, 26922, 3971, 26361, + 20844, 68642, 170517, 77339, 123255, 8963, 77818, 150998, 48466, 36806, 2732, 23261, 11741, 236162, 18243, 126216, + 28690, 50546, 16385, 92760, 197383, 246558, 201295, 88255, 67588, 71687, 176076, 172653, 169058, 33906, 63747, 24835, + 157621, 43338, 30050, 46152, 132741, 2770, 51371, 94835, 6614, 15112, 11749, 56936, 1250, 19027, 399017, 58036, + 100215, 23388, 55815, 308768, 124152, 94803, 9521, 64186, 8971, 28, 30427, 62163, 7616, 103838, 35079, 29203, + 131235, 7743, 17389, 10882, 37420, 61460, 228512, 85363, 41581, 131077, 62822, 119647, 10130, 54445, 26925, 19968, + 29016, 24446, 74028, 24176, 61448, 67185, 9254, 8563, 119129, 9771, 99184, 37716, 39514, 10532, 221512, 258753, + 218630, 55980, 23394, 32141, 61924, 66749, 32411, 3741, 36475, 26678, 77010, 44946, 91203, 128749, 116953, 20476, + 49625, 53116, 13735, 102335, 29376, 51946, 83407, 67892, 59212, 34685, 21083, 1546, 112982, 32972, 74397, 1078, + 190545, 16082, 86140, 58591, 89611, 101531, 10061, 105104, 76319, 20035, 17551, 52611, 169061, 190842, 100780, 23907, + 90413, 115619, 9675, 34710, 193435, 49443, 129734, 11183, 258877, 16318, 136182, 126808, 44635, 27304, 192375, 2599, + 125648, 47051, 12091, 23814, 721, 58800, 40137, 66726, 97930, 60877, 74487, 7942, 54326, 9841, 41428, 13762, + 8211, 85383, 6950, 99177, 79806, 201786, 296464, 124087, 13144, 29741, 41721, 47634, 55088, 254286, 106408, 17041, + 99064, 12942, 64086, 45233, 14005, 2612, 55827, 255, 7984, 13980, 38574, 12776, 46654, 73499, 249951, 2101, + 26676, 25996, 132326, 116415, 119062, 50449, 31033, 23038, 11589, 179252, 20007, 14860, 129270, 21143, 17796, 144715, + 60106, 70758, 69842, 34674, 282133, 44014, 16774, 57268, 38528, 24053, 46373, 201667, 28327, 471023, 51889, 102667, + 21193, 114909, 84132, 69317, 96723, 67969, 16134, 68145, 15058, 28765, 32035, 2524, 101089, 98664, 25045, 76571, + 14957, 86040, 118506, 262428, 154764, 81573, 39681, 283900, 73287, 127825, 544, 80448, 52347, 38512, 175971, 15180, + 45467, 33086, 46552, 48894, 81107, 43213, 36672, 54025, 76703, 8053, 7608, 13299, 56619, 20752, 238099, 54164, + 105133, 1444, 32942, 953, 37564, 8000, 66316, 119463, 106817, 404, 13667, 149108, 128597, 31267, 10269, 49836, + 106150, 1484, 52330, 76965, 160486, 171648, 38456, 31263, 22424, 37738, 66245, 67467, 143369, 60471, 75610, 20895, + 115528, 86070, 60854, 40796, 49347, 18989, 15030, 11371, 37578, 15779, 79867, 10187, 86462, 46402, 155626, 93200, + 40229, 7090, 57547, 108053, 99598, 11088, 47505, 41218, 206017, 2173, 20988, 30219, 22919, 80563, 57566, 42369, + 93141, 41675, 2407, 182519, 120495, 27154, 16702, 29456, 14349, 7958, 16688, 117177, 140375, 42467, 261919, 74916, + 153569, 10836, 34742, 49526, 7621, 105997, 12212, 2270, 392377, 7755, 17959, 25086, 232152, 138791, 33847, 13860, + 35316, 5811, 1344, 71259, 50452, 207539, 92635, 50359, 5821, 33674, 30255, 2086, 2587, 96264, 17543, 42, + 6029, 9580, 43007, 139248, 82831, 12917, 29607, 25786, 51467, 42137, 85161, 100698, 31561, 88989, 121990, 278500, + 3602, 109344, 37982, 15279, 116442, 28936, 30880, 87894, 58079, 128661, 126731, 67392, 28051, 146885, 4861, 16216, + 97344, 42827, 147561, 153948, 22684, 21335, 47685, 1853, 43349, 15185, 59642, 10229, 25520, 187921, 108972, 5579, + 98037, 24945, 6697, 19193, 63734, 137934, 75056, 89740, 19767, 224268, 56138, 63643, 151661, 39313, 70618, 84031, + 89723, 84074, 13703, 85626, 35460, 8867, 64845, 3439, 57906, 99776, 63968, 49270, 81130, 34356, 16210, 23547, + 36446, 34090, 140028, 72439, 2221, 22163, 57058, 363492, 113754, 18913, 95451, 48663, 54464, 54037, 176097, 68425, + 3023, 34906, 29482, 117389, 341780, 80431, 58330, 16753, 92616, 60907, 94846, 147486, 4498, 48646, 7773, 46801, + 7778, 18946, 464978, 47558, 33223, 177444, 7328, 15626, 63337, 94700, 11743, 9351, 255024, 39098, 16447, 42647, + 96230, 39769, 58840, 10068, 63439, 35800, 65843, 58823, 413844, 9156, 51258, 7434, 61791, 85018, 6872, 3692, + 28096, 7121, 33024, 6009, 75532, 31997, 192535, 9661, 3304, 9547, 14753, 31987, 25314, 55689, 15896, 20430, + 39472, 31340, 99744, 25398, 115569, 54883, 28719, 205423, 23071, 57855, 64638, 149867, 25671, 82403, 37616, 20668, + 39989, 77996, 74948, 140555, 175248, 64810, 36515, 46595, 4958, 248773, 24045, 28728, 136673, 168704, 20804, 114833, + 100325, 27135, 21205, 96151, 153134, 45992, 7093, 13992, 76047, 1980, 19432, 145001, 75159, 87462, 17710, 1013, + 45556, 34297, 144882, 20648, 26061, 11319, 129567, 108555, 18872, 464580, 33386, 22717, 65948, 167189, 5603, 135042, + 79542, 8801, 202632, 18114, 91882, 5973, 5239, 67315, 4431, 60916, 47819, 71693, 32597, 32606, 18183, 45072, + 80329, 76385, 24749, 51305, 40314, 156514, 14693, 130345, 13168, 66214, 18029, 12858, 34801, 27628, 14544, 10823, + 40522, 40185, 33739, 148694, 23548, 9923, 61012, 28859, 17933, 19442, 34364, 99849, 164107, 141167, 30629, 21054, + 6744, 36491, 8096, 42474, 41706, 155060, 30650, 10600, 163442, 1143, 96655, 61390, 52359, 7559, 51568, 64256, + 203854, 4467, 22453, 14504, 436398, 7878, 6980, 8293, 63610, 293747, 16167, 35763, 19627, 147603, 15419, 18032, + 110744, 51346, 33681, 54571, 40472, 48615, 39073, 21604, 13754, 173027, 92560, 11083, 47299, 63062, 11813, 52007, + 29883, 9734, 139722, 15953, 1550, 20651, 13616, 49306, 16113, 90089, 92326, 7584, 30712, 72424, 164858, 6831, + 152871, 55746, 197721, 34167, 196442, 6022, 112107, 55215, 7538, 123381, 4920, 43539, 77165, 8939, 50392, 34192, + 20225, 79762, 22505, 58667, 40770, 29788, 97180, 82835, 4568, 8579, 13273, 363569, 35898, 49983, 436, 36598, + 3237, 131691, 62418, 35591, 8101, 4073, 379438, 65218, 76072, 33887, 2968, 27573, 212619, 288680, 68278, 72851, + 150504, 217896, 6913, 121339, 22017, 35340, 51072, 43616, 75043, 31437, 10833, 81487, 4364, 22968, 41454, 106687, + 85446, 19863, 109625, 149241, 524, 141850, 214404, 54376, 657, 237023, 9401, 108137, 53800, 32474, 49712, 53334, + 126876, 27337, 45552, 177696, 8269, 15036, 12097, 42240, 2328, 125374, 119295, 99715, 2500, 19624, 39441, 27220, + 102691, 60957, 94543, 39101, 18566, 67362, 13975, 78230, 25017, 34017, 239007, 90027, 39351, 41681, 35354, 43822, + 1043, 916, 58587, 141983, 94818, 38799, 75459, 41114, 67432, 16195, 36606, 59568, 22272, 126769, 31424, 68659, + 12287, 134302, 257977, 5756, 207285, 95637, 47248, 117689, 19583, 77451, 22373, 12200, 54993, 117118, 34244, 29386, + 34562, 53819, 71267, 64172, 77665, 49368, 7716, 59301, 25749, 45426, 194789, 17297, 2650, 1766, 32501, 45198, + 20403, 20984, 6600, 14171, 94604, 19037, 5402, 29896, 9938, 59935, 109708, 88081, 145182, 44844, 39167, 352626, + 164173, 35374, 45982, 6122, 154, 73419, 220487, 53834, 53601, 17992, 8609, 229321, 5610, 68098, 66815, 71012, + 95069, 140968, 27396, 8957, 134489, 24656, 86659, 56598, 134852, 17316, 123838, 255436, 6613, 41610, 138033, 81452, + 32023, 32396, 123687, 63398, 8693, 29712, 30407, 19296, 121188, 3551, 36099, 20032, 111948, 56624, 16547, 27453, + 35916, 15378, 52039, 56849, 13489, 22214, 73177, 53097, 277349, 2157, 14029, 187886, 10260, 141743, 246460, 91880, + 50869, 3788, 49486, 133566, 54950, 33120, 129337, 53768, 18333, 9525, 26902, 312251, 10297, 9020, 70759, 16647, + 112432, 59260, 84609, 9818, 82766, 73569, 468, 46001, 75780, 55028, 52106, 11498, 43645, 108069, 17150, 17753, + 29417, 16705, 31799, 9606, 289, 122254, 115975, 8620, 6133, 255357, 56908, 14456, 133464, 43554, 79224, 11247, + 29630, 160, 12756, 25464, 65960, 350428, 62521, 321796, 100359, 67358, 35169, 46172, 113128, 48988, 88868, 31094, + 33266, 6847, 60887, 98188, 49659, 69117, 92977, 220228, 13947, 80181, 35103, 62170, 97351, 13475, 2440, 199768, + 19498, 36597, 46971, 25234, 67806, 62881, 84717, 73648, 181966, 10488, 94149, 21550, 26655, 63436, 48375, 14405, + 165650, 9621, 24439, 28043, 42735, 4490, 29963, 56674, 45373, 1934, 262446, 50855, 67098, 26898, 5261, 52696, + 40644, 33900, 9440, 180286, 87162, 22940, 19704, 26936, 69769, 10254, 101759, 27406, 12243, 48000, 73926, 113215, + 54935, 5726, 192787, 4312, 106216, 9366, 11550, 52949, 23457, 212271, 277152, 133895, 108374, 6191, 96477, 29980, + 218916, 58024, 54696, 40853, 91124, 65894, 91170, 65908, 252552, 6793, 29212, 15389, 44516, 122515, 52617, 35058, + 9017, 103536, 39510, 49136, 19242, 130652, 662077, 74699, 47024, 31422, 8517, 73351, 24399, 13867, 128360, 4810, + 4434, 61779, 111983, 61036, 17798, 110240, 59722, 102960, 39688, 10001, 23803, 23039, 176498, 56659, 44814, 134295, + 17188, 77577, 74466, 226175, 102472, 154333, 63900, 111747, 18062, 41171, 79669, 32773, 408933, 42562, 28931, 30907, + 107388, 43487, 2946, 240310, 23938, 24354, 319, 184983, 7927, 6488, 1422, 10790, 68809, 68209, 64775, 4361, + 202, 17123, 59634, 51200, 44391, 18188, 17843, 2619, 74278, 3230, 9540, 47187, 21702, 36274, 56894, 43907, + 16310, 34790, 16866, 6150, 5561, 13587, 107545, 108873, 126867, 86986, 28640, 33427, 19017, 5762, 80637, 17430, + 46903, 2047, 131055, 25958, 13558, 5444, 47152, 13900, 44563, 122857, 45348, 70863, 39593, 54332, 38068, 33637, + 318, 40310, 143467, 18502, 24520, 11377, 62013, 28942, 27246, 28269, 83545, 17999, 59015, 90707, 30065, 15161, + 34720, 1263, 37008, 2012, 6060, 98575, 92933, 5721, 299, 199555, 24578, 29223, 2985, 743, 115825, 109523, + 136657, 47454, 26378, 53586, 3733, 174945, 93340, 244456, 5693, 37386, 28782, 89767, 27545, 23573, 18798, 136425, + 34320, 84778, 20041, 48453, 38215, 7477, 71958, 40621, 8773, 5874, 187927, 105965, 51100, 43533, 18083, 8443, + 10180, 43597, 2003, 183999, 69689, 12216, 129696, 146188, 62389, 34044, 68410, 12765, 43273, 26949, 266807, 3345, + 34477, 79197, 5688, 47539, 213110, 21634, 22257, 50092, 32222, 42346, 39530, 63668, 98, 134978, 74022, 5152, + 59088, 174145, 37220, 9934, 9545, 118937, 5724, 87240, 19875, 15784, 40143, 23263, 87513, 181654, 285152, 37881, + 263241, 4966, 43934, 10433, 186657, 6470, 74416, 225854, 25908, 142677, 246262, 32280, 6192, 75890, 45546, 143264, + 135305, 29742, 47013, 77787, 11732, 126658, 8763, 37950, 21806, 57557, 113464, 89465, 108995, 164574, 23894, 22996, + 23169, 15369, 23117, 17642, 130607, 40503, 36239, 280990, 44666, 9981, 40427, 147487, 26869, 168452, 32886, 32991, + 46798, 240839, 15111, 70502, 65697, 88548, 44145, 28701, 48767, 31139, 206777, 35659, 181164, 166262, 14554, 171445, + 31786, 66523, 76607, 17956, 6507, 31279, 90476, 116611, 167918, 6560, 1243, 115324, 80128, 41867, 55897, 187323, + 37069, 32596, 189444, 145931, 13390, 105530, 65709, 26805, 6999, 55714, 41300, 22915, 68951, 22138, 21120, 22264, + 10058, 19945, 33635, 56123, 99085, 10032, 5818, 6016, 46649, 57476, 35264, 94413, 112522, 262288, 93686, 83038, + 14341, 23204, 28807, 66084, 77987, 6101, 126673, 7133, 38126, 5923, 122091, 170240, 97772, 46874, 215746, 43948, + 41622, 3272, 55596, 8332, 146411, 251315, 13533, 8561, 81521, 115449, 48616, 175175, 2063, 186556, 3036, 134537, + 75772, 29728, 82360, 22973, 186559, 86348, 89100, 38388, 82297, 45610, 2613, 87082, 9986, 177812, 57884, 23591, + 47485, 42543, 33582, 44713, 74439, 257444, 252451, 31825, 35631, 38540, 33066, 5147, 13973, 4343, 51830, 70378, + 22827, 26448, 95560, 36896, 241741, 48067, 203953, 298860, 61620, 20450, 3220, 67272, 6586, 107662, 100160, 108684, + 6929, 57226, 4762, 7457, 1320, 40404, 77204, 99309, 62750, 208653, 59977, 44000, 74315, 34332, 5819, 172217, + 64904, 114077, 18147, 84012, 1791, 98456, 90930, 21446, 116669, 103938, 7422, 85140, 59713, 5768, 326211, 16239, + 75411, 13229, 29398, 10758, 236107, 1539, 112472, 95979, 152154, 151294, 306, 21196, 38146, 10700, 6891, 84282, + 109646, 56492, 40539, 6589, 119491, 51354, 30685, 140209, 136906, 29622, 73617, 49553, 70525, 51671, 166869, 139616, + 74395, 37439, 49595, 45678, 11959, 33211, 86560, 52434, 9282, 62690, 112155, 130810, 5243, 108261, 99970, 265613, + 72551, 80049, 6391, 33365, 90721, 66737, 69872, 87011, 1860, 9032, 112544, 60905, 37371, 89015, 140351, 19076, + 850, 373531, 2802, 36725, 218795, 72062, 28990, 16550, 24614, 7815, 6187, 26336, 33373, 32162, 42791, 73555, + 32062, 23386, 10244, 56392, 49442, 27076, 136262, 12412, 14883, 1134, 33675, 97153, 199281, 15608, 100152, 74072, + 47942, 254301, 36451, 16026, 10687, 65067, 56708, 254030, 30290, 50490, 13864, 57941, 259331, 35588, 23485, 43486, + 24869, 21620, 92971, 22072, 88645, 1048, 182050, 13343, 32452, 14825, 19509, 3325, 216938, 45740, 99716, 189082, + 53740, 78245, 25609, 24311, 176777, 47340, 308354, 40669, 66085, 14102, 125339, 9225, 128709, 97207, 1271, 200933, + 78439, 113451, 88975, 18324, 46521, 11819, 18570, 141756, 72512, 170020, 52754, 63550, 118515, 103073, 93330, 32736, + 50499, 14722, 31600, 68452, 398867, 29316, 172786, 18417, 104924, 2606, 5670, 84818, 16288, 67106, 59580, 82929, + 607401, 291, 85829, 359, 15897, 35830, 50696, 65630, 52672, 22115, 356968, 29895, 40837, 231192, 34024, 38957, + 26722, 406, 23335, 124952, 72068, 68804, 13268, 147101, 164740, 276569, 162596, 66943, 11569, 26654, 66358, 4777, + 23229, 102127, 5848, 978, 2921, 59666, 5371, 28212, 90108, 42938, 39320, 2499, 4271, 108792, 33510, 125072, + 71653, 65239, 38250, 66357, 38577, 13964, 86251, 35708, 50755, 36010, 29448, 12209, 3844, 38222, 206337, 100876, + 67827, 137088, 14167, 252225, 84163, 195270, 1306, 5703, 54198, 779, 46802, 22028, 51124, 86759, 70560, 113164, + 35685, 162145, 45471, 34561, 422, 2611, 6464, 47486, 19223, 38246, 9191, 18331, 89942, 243642, 212364, 15893, + 17518, 22617, 6409, 30046, 126182, 59716, 36560, 104428, 18846, 26592, 19458, 50793, 147333, 30826, 1388, 27647, + 10922, 14495, 33545, 19269, 135828, 39727, 41601, 46931, 233379, 49169, 131130, 182112, 16276, 82381, 118209, 142445, + 128310, 19672, 28740, 82907, 33436, 3118, 102206, 28723, 24819, 41937, 38854, 5157, 3881, 111491, 1142, 9776, + 421673, 152241, 29309, 14961, 87854, 6054, 15424, 3796, 82656, 54996, 2108, 55367, 239450, 154525, 9643, 118103, + 106041, 64601, 68549, 48707, 30266, 25772, 18740, 9462, 229669, 91798, 112152, 191327, 14493, 72828, 8175, 66636, + 236474, 25817, 87351, 129027, 76653, 20422, 22983, 71240, 27846, 44661, 12399, 46158, 77704, 53101, 35032, 11072, + 17300, 109294, 33638, 24408, 1895, 11241, 760, 17584, 82479, 125877, 63150, 141075, 34259, 23274, 81698, 15732, + 43577, 48340, 91584, 14688, 16379, 24481, 150280, 96420, 262050, 48635, 43727, 61819, 56268, 72003, 88178, 17281, + 79912, 13218, 122519, 125295, 166396, 11811, 2171, 118930, 67746, 17636, 178278, 174656, 95661, 173039, 83845, 79689, + 17473, 98555, 127696, 203415, 54730, 22925, 232239, 9309, 12136, 175026, 20740, 180188, 10747, 39816, 314017, 266131, + 10040, 175732, 112550, 220651, 31974, 37393, 888, 23008, 86799, 4303, 64905, 148467, 75337, 251, 3284, 370102, + 50264, 9835, 5438, 23655, 4481, 29851, 329, 12855, 7162, 64931, 78141, 12804, 42372, 296771, 83547, 18624, + 34874, 86271, 3360, 48665, 77735, 88767, 11463, 63527, 28889, 22258, 29140, 194315, 113924, 25499, 6406, 31334, + 1845, 4802, 49184, 43455, 35469, 127594, 92970, 61038, 115005, 38840, 87761, 106838, 8811, 20572, 55637, 11162, + 96721, 132425, 108925, 2948, 125457, 36356, 3502, 75270, 27622, 127192, 2561, 123095, 49394, 61155, 16897, 110064, + 9699, 89448, 53356, 19628, 220310, 21622, 83036, 9885, 112214, 6087, 26713, 17901, 161912, 91492, 3440, 68594, + 9266, 92238, 8087, 6866, 150194, 72175, 80701, 13459, 31836, 43243, 239700, 95846, 44749, 50647, 21945, 230538, + 120612, 132371, 244604, 5193, 105637, 34661, 41341, 68775, 85393, 1874, 8771, 33718, 49672, 77403, 595452, 99507, + 6490, 58895, 128742, 7704, 39239, 73217, 43816, 62824, 37804, 199976, 22361, 80005, 87514, 94832, 14089, 4574, + 139975, 59142, 75523, 100268, 43906, 53442, 15152, 2547, 186002, 17011, 19513, 204282, 3343, 60568, 128318, 119250, + 4298, 51871, 41336, 71759, 21921, 45074, 98169, 145889, 99427, 11350, 1237, 5520, 28799, 7803, 53702, 21026, + 136352, 38293, 128690, 12158, 90132, 44600, 10184, 26957, 39459, 126025, 78904, 82999, 59373, 39301, 150198, 120529, + 153042, 20177, 50089, 14764, 271571, 30530, 123161, 38975, 101562, 22941, 5648, 124654, 109243, 69817, 71675, 49162, + 106884, 21241, 107795, 30258, 16572, 188262, 141456, 7688, 60718, 8271, 11044, 32440, 104608, 103419, 236109, 93156, + 43293, 128929, 42107, 67180, 25201, 115254, 185488, 130954, 72813, 167547, 20537, 39969, 38432, 22582, 184022, 1139, + 27199, 5655, 17767, 97412, 122606, 209377, 27070, 35871, 326617, 188954, 42680, 73512, 80911, 22629, 3011, 95021, + 315242, 157737, 383, 41821, 41808, 19335, 27950, 15674, 25677, 110950, 35375, 76835, 59108, 57370, 35262, 16569, + 160415, 37706, 78086, 32041, 49691, 137143, 9782, 172080, 50148, 77917, 6323, 10110, 69172, 17711, 21795, 59511, + 76184, 135114, 31046, 132319, 59105, 157578, 20549, 80778, 57649, 158421, 65143, 4575, 72235, 21899, 10797, 92745, + 34035, 106079, 80159, 4508, 78304, 25350, 75457, 46458, 32937, 25623, 47, 8531, 104751, 84953, 8138, 36508, + 187199, 66310, 115274, 13253, 32461, 38536, 1916, 42007, 187160, 35055, 26325, 84394, 35963, 94216, 45590, 97782 +] diff --git a/packages/kad-dht/src/routing-table/generated-prefix-list.ts b/packages/kad-dht/src/routing-table/generated-prefix-list.ts new file mode 100644 index 0000000000..14b0ce00ce --- /dev/null +++ b/packages/kad-dht/src/routing-table/generated-prefix-list.ts @@ -0,0 +1,4098 @@ +export default [ + 77591, 94053, 60620, 45849, 22417, 13238, 102507, 179931, 43971, 15812, 24466, 64694, 28421, 80794, 13447, 118511, + 740, 6439, 164565, 160996, 29829, 65024, 115728, 46297, 71467, 26874, 47057, 19864, 228973, 57886, 62422, 50382, + 196661, 98858, 8131, 154708, 78537, 104511, 53134, 136579, 27689, 126238, 28199, 3679, 36431, 48892, 2655, 57939, + 44415, 38209, 7970, 34780, 14362, 51843, 23108, 52670, 19456, 36805, 408716, 129012, 106025, 12683, 780, 36702, + 96308, 73261, 165714, 94326, 2882, 15786, 65607, 80947, 49509, 13763, 104712, 13107, 21149, 137011, 223495, 30903, + 87173, 75141, 2533, 121964, 131409, 110026, 108394, 16009, 75844, 196819, 1440, 7629, 23676, 111231, 127712, 61087, + 121838, 51872, 29103, 7233, 30291, 24088, 110490, 92353, 17492, 113372, 16487, 97612, 2953, 9394, 210912, 8964, + 7564, 3852, 97455, 42207, 110620, 22643, 65016, 7253, 129477, 46969, 7830, 43238, 127283, 37807, 65596, 47230, + 53113, 68778, 42174, 3025, 72417, 113389, 61485, 3233, 165166, 23272, 207684, 1480, 109690, 77717, 146330, 35614, + 21200, 125839, 9167, 183529, 102125, 27762, 21718, 34784, 24049, 54922, 44135, 54112, 71504, 58952, 18652, 36112, + 90342, 97581, 105898, 116695, 25307, 71711, 19850, 443067, 72039, 164371, 99358, 141908, 26812, 37120, 222981, 92235, + 26715, 2272, 38699, 277092, 32264, 2507, 11509, 41396, 133800, 81066, 75726, 51643, 71161, 32364, 125073, 195906, + 88956, 8820, 58708, 60150, 171987, 43866, 50300, 27077, 51779, 41724, 18910, 42608, 24425, 59574, 40645, 30367, + 16671, 106324, 56018, 73410, 30251, 125091, 17154, 23172, 186294, 741, 111661, 148919, 247761, 71695, 148683, 76545, + 14202, 32826, 57291, 56464, 2121, 52187, 36887, 19845, 8465, 15701, 42227, 10603, 35024, 129005, 20364, 271992, + 4876, 54659, 43090, 48318, 85917, 40506, 60228, 35848, 169730, 2400, 19908, 21535, 3638, 2880, 105194, 37121, + 256836, 27972, 59367, 47659, 96184, 20378, 6352, 132486, 943, 210847, 347244, 42708, 18678, 161556, 4520, 63681, + 6583, 138160, 207565, 4182, 52907, 72891, 36505, 33320, 35807, 152018, 13288, 904, 112254, 139219, 23049, 24474, + 214097, 14830, 47960, 50966, 18796, 25821, 749, 61464, 11595, 123216, 5285, 37544, 9243, 80395, 22070, 63873, + 23554, 106570, 90364, 35779, 887, 61552, 55147, 3791, 268203, 76040, 13872, 53070, 382004, 149091, 9411, 70938, + 24590, 26314, 23297, 60821, 111335, 56198, 123964, 28317, 11625, 39656, 33077, 122186, 16619, 2762, 8556, 43622, + 29039, 54719, 141778, 30583, 102425, 30319, 55618, 4660, 69006, 75066, 46293, 24767, 97976, 8387, 5680, 68535, + 92362, 327684, 180600, 43548, 32552, 905, 167743, 10812, 63717, 48600, 4157, 19832, 41433, 44366, 169717, 362623, + 128974, 242972, 74944, 25914, 137630, 138732, 9905, 65119, 59943, 13001, 10439, 346877, 10019, 72338, 47424, 90540, + 13986, 32605, 74311, 36273, 35430, 43274, 490600, 15654, 33665, 40911, 16891, 132492, 108037, 118859, 30430, 45629, + 43799, 65831, 25824, 63966, 43280, 70552, 34778, 102075, 38195, 5993, 20515, 11742, 29078, 67047, 980, 30234, + 58629, 68076, 5792, 59696, 18265, 2627, 47407, 29302, 14425, 46647, 15604, 15925, 46832, 5440, 684, 42003, + 235538, 28764, 54452, 25101, 40830, 8023, 6501, 50689, 77881, 5650, 16800, 16147, 110717, 28112, 219637, 1634, + 58937, 32412, 88801, 6927, 3463, 157022, 94779, 442571, 325358, 276, 141280, 75559, 51300, 58421, 109559, 35845, + 47623, 321870, 24845, 42379, 117252, 19971, 14000, 130543, 19007, 191657, 1705, 32933, 10170, 64831, 2632, 89911, + 20540, 14737, 53476, 30106, 91237, 23474, 41156, 76048, 294813, 109786, 153316, 31289, 4951, 134188, 5698, 58898, + 79841, 8216, 13373, 150001, 56232, 83956, 179514, 40785, 36270, 150581, 38142, 36729, 128547, 27488, 48397, 32074, + 69209, 83991, 69639, 44375, 66275, 50325, 46119, 4588, 100156, 57453, 106674, 3707, 32063, 12250, 176480, 94462, + 73531, 42286, 44132, 42292, 34439, 205098, 23362, 170867, 80937, 18578, 35224, 8003, 28892, 73415, 50905, 36012, + 44466, 3377, 68122, 77350, 88595, 16048, 139321, 45304, 216307, 26958, 49160, 2333, 32583, 197092, 51650, 27957, + 49620, 28596, 32484, 40154, 16605, 3672, 19287, 14394, 82127, 113881, 101822, 55495, 45807, 22719, 49287, 17105, + 21630, 9213, 225560, 184754, 78726, 55879, 1187, 55736, 20235, 48276, 60072, 8055, 40163, 71435, 10613, 66014, + 111007, 30011, 11754, 32797, 96926, 8244, 35114, 58420, 5567, 8879, 4349, 36989, 72083, 27721, 80502, 31714, + 21665, 68483, 67000, 32243, 58844, 22490, 151524, 85501, 39419, 31544, 46585, 60252, 179767, 135313, 38991, 99008, + 48328, 21411, 230904, 25457, 42662, 73162, 35923, 104338, 51550, 37715, 30664, 24386, 5251, 34179, 21686, 23914, + 37811, 77986, 123822, 22186, 49608, 218194, 113768, 119158, 81056, 136532, 36573, 4335, 50854, 77454, 36591, 786, + 55513, 89905, 64981, 78223, 20922, 90512, 58000, 187805, 18891, 142810, 7204, 125174, 197409, 232663, 64781, 31572, + 164656, 137833, 103498, 55315, 32593, 91963, 91694, 30505, 71449, 150025, 16975, 134836, 220474, 56258, 1789, 23900, + 58919, 39771, 52833, 15954, 85682, 182360, 82050, 60999, 67854, 36289, 50792, 14607, 13758, 73909, 111848, 63880, + 35066, 107613, 145156, 26237, 3565, 8173, 214338, 1836, 61905, 82544, 35483, 19741, 214793, 18510, 3395, 10924, + 119572, 75264, 17466, 43207, 141419, 82668, 39303, 19609, 21504, 19695, 19065, 6944, 10302, 38666, 102996, 88789, + 27354, 75138, 70106, 135106, 67003, 20045, 60619, 54525, 46131, 115306, 12445, 86777, 32668, 68413, 32737, 64388, + 15165, 34095, 171569, 11093, 64871, 119058, 92294, 117952, 34450, 66009, 203796, 6258, 17821, 52488, 314552, 125812, + 2757, 95795, 15139, 46369, 11452, 76801, 3035, 9101, 34189, 14945, 7202, 149174, 5160, 74854, 169046, 30085, + 12257, 76562, 92934, 170882, 85523, 121128, 60225, 45744, 560, 62173, 205019, 128933, 53385, 94, 81804, 5962, + 65887, 9406, 75139, 46078, 119549, 87470, 126330, 115083, 135620, 90768, 93971, 66716, 312353, 69610, 203240, 65196, + 115979, 13452, 77397, 23, 122356, 131305, 48028, 43698, 10867, 95182, 47337, 60657, 193231, 4430, 32675, 100177, + 124537, 49701, 68459, 417255, 54783, 44031, 66481, 29365, 90675, 20969, 21022, 49332, 120791, 87739, 113524, 8715, + 4715, 33049, 64432, 86239, 142253, 763, 145381, 11942, 50943, 44118, 117335, 69368, 17271, 82615, 97767, 8516, + 43358, 61812, 117693, 77645, 25331, 71884, 62816, 56740, 4917, 126017, 38232, 39911, 120566, 45088, 86073, 19308, + 34580, 62715, 98835, 12238, 12878, 32818, 80514, 190672, 33786, 124897, 32390, 13707, 160528, 8239, 24113, 94911, + 32523, 8473, 305619, 143741, 4869, 226676, 116030, 72714, 301307, 245805, 49902, 13070, 104817, 63744, 25320, 14079, + 81491, 66562, 24649, 6335, 23276, 12633, 45891, 31344, 8832, 19031, 49267, 95191, 97911, 27244, 61726, 53839, + 31265, 81626, 4566, 137532, 52065, 115327, 11846, 252068, 7998, 22402, 10126, 209408, 49622, 16068, 12953, 24383, + 9715, 82577, 95468, 95106, 43998, 60754, 21093, 14837, 34091, 72540, 179063, 7433, 84587, 192802, 47914, 4438, + 20664, 45500, 8855, 16934, 69041, 12731, 29041, 217180, 29419, 22657, 137482, 2887, 53205, 550, 70043, 123839, + 10838, 164726, 42397, 184876, 58288, 26641, 22447, 12131, 116145, 22995, 97093, 108266, 6185, 2832, 52427, 64656, + 5154, 49928, 144137, 12044, 141795, 129976, 31641, 84599, 35924, 2502, 28404, 26000, 21307, 63600, 20886, 165871, + 144738, 353334, 45550, 4235, 43730, 54853, 149395, 14340, 12085, 6025, 82291, 127186, 8279, 7961, 81927, 74078, + 10002, 50016, 8795, 38560, 119, 45637, 190798, 21574, 133779, 97318, 19903, 27528, 199668, 1330, 66035, 21635, + 72938, 31184, 60710, 108060, 31768, 145285, 89744, 113430, 39176, 71121, 10578, 19002, 67875, 39253, 95870, 17637, + 38453, 35956, 214432, 92498, 9700, 51981, 75487, 140364, 44144, 248414, 34793, 35244, 4121, 13131, 29680, 132109, + 116048, 51552, 20482, 69742, 41733, 134398, 163626, 2676, 12868, 9786, 36799, 26675, 82669, 19252, 28098, 76936, + 92308, 127797, 49202, 5337, 128, 27975, 178978, 22753, 34262, 94544, 214584, 43276, 11332, 665, 58732, 8484, + 7712, 180682, 90181, 28567, 90764, 20944, 68372, 62049, 36141, 29920, 115786, 1365, 13553, 110638, 163556, 207080, + 71312, 250718, 214174, 18727, 77470, 23807, 32279, 108909, 117314, 4887, 61022, 41180, 96549, 116044, 1081, 78818, + 49135, 8305, 20213, 10021, 23602, 148923, 39033, 76575, 54468, 41625, 121743, 61361, 28605, 110339, 97381, 108784, + 6327, 58565, 37906, 2722, 62308, 42415, 120829, 226683, 17171, 16955, 32278, 42441, 67531, 82112, 7044, 8333, + 21319, 4625, 67693, 83024, 14105, 107392, 18658, 14247, 894, 35117, 78964, 71644, 107722, 11889, 4981, 16504, + 46157, 86476, 243104, 110164, 8503, 65279, 38377, 50730, 51069, 170106, 155778, 36441, 100472, 8367, 14072, 2456, + 45138, 1449, 85419, 56978, 15246, 51849, 58602, 75312, 14577, 34388, 14985, 214746, 35609, 94173, 205371, 29378, + 191464, 60659, 83825, 4266, 1757, 79901, 4005, 96090, 13364, 26836, 20634, 9902, 161349, 52221, 57608, 45087, + 32067, 12041, 24449, 122590, 91705, 4841, 5595, 1962, 81144, 94514, 7189, 65466, 52339, 115937, 30039, 184359, + 5408, 37938, 13094, 131687, 91066, 50656, 3538, 308588, 21983, 117880, 124083, 8740, 14157, 207581, 132848, 24615, + 100545, 35998, 13259, 94379, 4372, 221513, 9160, 14015, 26630, 42025, 87194, 4685, 129112, 37014, 5514, 1659, + 1423, 35031, 86869, 42243, 29676, 77384, 91770, 8949, 213626, 219087, 14943, 2758, 4397, 146113, 19935, 39810, + 88436, 21548, 15622, 47174, 99190, 170858, 31675, 22540, 6877, 25282, 66955, 39440, 49958, 3702, 59942, 3443, + 26122, 118447, 24469, 28429, 114348, 66350, 72579, 194, 60661, 14964, 70751, 30122, 29818, 134851, 14530, 25859, + 293118, 32210, 11158, 134437, 50042, 50868, 124554, 56791, 179738, 112687, 67437, 80580, 16400, 32499, 35433, 38147, + 163423, 62209, 109887, 21489, 89627, 8619, 37255, 42560, 31040, 3283, 221255, 26057, 43973, 176482, 84209, 74565, + 36638, 128029, 50150, 53376, 45952, 23372, 136030, 19408, 5153, 189398, 9461, 12142, 1894, 150004, 6947, 43095, + 109322, 74270, 235743, 8877, 1898, 12589, 62161, 150831, 134021, 76036, 32418, 114411, 12402, 9784, 152424, 2030, + 112077, 39948, 15299, 91532, 68309, 58254, 74157, 68071, 190269, 1807, 48227, 14614, 69866, 175786, 53526, 77245, + 31938, 86410, 49785, 5548, 107383, 26754, 6925, 99713, 11522, 112823, 36879, 191627, 105232, 112178, 9544, 115058, + 11248, 121092, 115523, 216088, 14868, 164602, 6984, 12211, 39852, 3557, 11388, 124397, 71707, 42768, 81029, 87167, + 186525, 134029, 24303, 29049, 16530, 60454, 1801, 70482, 38162, 186140, 17626, 75869, 106212, 3301, 149347, 83560, + 11700, 132692, 2213, 6118, 5130, 19621, 133100, 5413, 16608, 6316, 6903, 20826, 26998, 46988, 14742, 36801, + 59586, 438, 115651, 12542, 108399, 50888, 73600, 74851, 230033, 11883, 313836, 13563, 43683, 27664, 16986, 54266, + 48135, 20496, 78612, 90668, 82179, 65157, 159306, 244506, 2073, 113828, 34210, 8905, 5015, 124130, 30133, 30478, + 196684, 40526, 10545, 25933, 189293, 20827, 73483, 91579, 16378, 24561, 168921, 100351, 23452, 105211, 31749, 3947, + 8301, 235867, 175604, 4648, 35640, 22045, 10909, 12114, 11632, 81578, 50578, 17722, 214551, 40781, 131060, 242797, + 29240, 41868, 116245, 182350, 57644, 27787, 59645, 42511, 33137, 64292, 86072, 2870, 91949, 108278, 14903, 186497, + 55157, 48398, 10332, 2801, 52384, 20759, 10283, 88468, 117313, 23727, 138084, 65635, 5090, 14195, 126767, 300, + 17717, 38157, 16186, 114320, 89668, 96676, 9742, 203368, 49363, 5035, 28964, 65388, 82238, 67525, 39995, 13922, + 241035, 69735, 11154, 193950, 66216, 72997, 12434, 16882, 29066, 91839, 31743, 96167, 184088, 75620, 1030, 139617, + 97206, 15695, 244555, 101352, 62820, 44153, 114812, 120196, 26595, 72217, 5935, 28488, 4241, 7832, 101557, 27041, + 135635, 10308, 337586, 23855, 173672, 15924, 5051, 10103, 8202, 360, 45227, 30801, 459, 13982, 27256, 9104, + 71355, 53611, 81898, 79904, 146294, 57705, 99956, 35919, 29587, 21273, 89804, 41886, 3008, 100905, 29691, 22814, + 135385, 101754, 7790, 16486, 141203, 186158, 135150, 17125, 14803, 43200, 23042, 70352, 6634, 27432, 14596, 27017, + 45094, 251700, 107172, 92556, 69362, 224587, 20275, 239867, 50925, 67860, 22054, 35132, 546, 107574, 11246, 15583, + 51884, 52526, 41469, 90704, 62011, 30436, 4192, 20677, 83296, 40746, 43027, 18829, 234584, 59250, 10989, 12045, + 44515, 87149, 5814, 22428, 56050, 1304, 54193, 102712, 89476, 74967, 28363, 182054, 87751, 63858, 4667, 36435, + 19373, 13180, 80439, 20298, 12691, 59200, 175067, 68478, 149923, 65774, 50785, 75599, 19794, 24659, 40763, 18905, + 13833, 221290, 11814, 27472, 35846, 256569, 9769, 37905, 87557, 16393, 61774, 29056, 58339, 67859, 122835, 31673, + 2884, 29565, 225212, 50663, 19145, 154284, 7940, 13382, 25647, 46917, 107024, 18714, 12224, 8197, 11896, 129114, + 11024, 5323, 163976, 216168, 77338, 91508, 61901, 29134, 64608, 87645, 71475, 46110, 122297, 22635, 34837, 26310, + 53025, 53017, 10622, 90942, 7205, 22145, 163437, 101344, 36189, 355381, 3469, 59647, 36294, 29028, 61676, 33071, + 170779, 1619, 42455, 55588, 21750, 12494, 53664, 106939, 7739, 60501, 600, 42951, 173883, 121950, 75147, 44445, + 75192, 26282, 17177, 6729, 35664, 13478, 22319, 74388, 224240, 51121, 128054, 19973, 113121, 26367, 20959, 71130, + 30181, 27274, 83822, 65840, 26267, 141848, 7294, 161141, 27036, 20489, 14220, 74392, 117827, 12263, 18511, 12425, + 92015, 38371, 93826, 46517, 106516, 24959, 428957, 108509, 55628, 41208, 28538, 6694, 203549, 200020, 130157, 14026, + 67949, 261382, 34954, 75428, 60462, 34936, 69163, 8775, 60844, 95271, 14668, 58597, 35911, 163570, 17395, 41268, + 20457, 77077, 15920, 195151, 1820, 1127, 108523, 1201, 920, 64420, 142690, 3800, 19773, 18589, 25204, 114010, + 8738, 45928, 72305, 27317, 73173, 58181, 4109, 38698, 181993, 2002, 91269, 6577, 38521, 64761, 34725, 2779, + 98254, 99182, 109347, 42999, 76257, 42992, 2481, 76329, 46008, 9716, 174991, 37659, 92796, 26911, 126742, 21977, + 5384, 89414, 18739, 22923, 26868, 2989, 52591, 14973, 151566, 3554, 169141, 41484, 22124, 26749, 78963, 86727, + 2411, 21918, 43055, 36709, 15919, 32188, 39853, 31407, 186872, 106163, 35231, 3970, 180021, 86213, 133789, 47183, + 28099, 10825, 8315, 193036, 152961, 12221, 96811, 33623, 78811, 61925, 91812, 72246, 80237, 171243, 144270, 12504, + 62352, 69843, 208025, 139707, 102653, 182703, 42668, 65058, 74259, 143770, 10084, 32242, 184890, 53802, 20214, 60407, + 16792, 41310, 4184, 1636, 123702, 13335, 68718, 46717, 224945, 64844, 113887, 41497, 29940, 10587, 27431, 128017, + 19512, 17506, 17671, 26070, 75283, 42125, 47504, 37731, 14059, 88044, 36619, 847, 112691, 14770, 55376, 575, + 92811, 347152, 96947, 9385, 233329, 3093, 22326, 45207, 20411, 273167, 31247, 6125, 138569, 8663, 357575, 28073, + 53341, 234780, 21561, 48933, 109802, 48919, 46462, 50800, 50600, 21098, 18940, 1091, 134528, 14935, 2398, 127145, + 66747, 34702, 127805, 27345, 5529, 139548, 51994, 127312, 166531, 11082, 36587, 50668, 31578, 37535, 46230, 2150, + 64732, 41722, 91822, 21109, 67189, 47573, 20129, 8421, 1596, 16448, 126415, 81846, 126357, 140669, 1937, 32338, + 967, 39499, 14778, 48543, 167999, 24888, 12192, 41633, 206598, 60067, 160162, 11609, 109752, 3487, 45910, 15601, + 119431, 19179, 93578, 31236, 207825, 71291, 47437, 21034, 78791, 32425, 31613, 91908, 91938, 6225, 26499, 49240, + 10301, 34970, 12824, 99989, 27311, 35324, 133950, 14043, 24233, 61362, 22243, 35045, 252343, 28863, 12365, 8224, + 28831, 215245, 73325, 83362, 32812, 116785, 100940, 77100, 66002, 61855, 60149, 24654, 112267, 65835, 54563, 141839, + 90895, 174574, 34653, 8453, 8786, 174076, 167014, 20249, 8095, 14050, 68580, 299481, 16824, 48793, 24856, 15716, + 22866, 165280, 33060, 49389, 21813, 47387, 179304, 131281, 60507, 145727, 21710, 16780, 174833, 11187, 19174, 11577, + 19549, 89709, 114442, 11917, 130985, 53665, 52636, 32837, 117051, 78060, 79585, 45117, 52110, 74026, 86227, 52956, + 6938, 48219, 29286, 23852, 81923, 55204, 370875, 58300, 123864, 14993, 25906, 17004, 38061, 191997, 56608, 197099, + 919, 5046, 126484, 79803, 18680, 145935, 124511, 60333, 53534, 6979, 35404, 23791, 46739, 36466, 2445, 19890, + 112893, 35958, 11939, 45333, 161529, 38751, 76585, 129315, 85429, 125900, 37046, 110236, 26761, 13725, 20554, 21155, + 11900, 10186, 81185, 44323, 81121, 127313, 181376, 68138, 91968, 77284, 14617, 15815, 15390, 1425, 15586, 9037, + 217947, 19393, 2643, 291035, 56524, 1195, 154070, 7980, 1713, 2618, 18959, 70645, 6654, 8986, 122964, 149447, + 37089, 79358, 120676, 39867, 85630, 173326, 14161, 103857, 138866, 98205, 107118, 105847, 61850, 48312, 3318, 110656, + 16491, 22884, 29985, 202016, 75577, 7108, 49432, 450007, 16884, 60351, 28287, 31574, 98296, 153369, 5508, 59238, + 73523, 2766, 134247, 6922, 6140, 15761, 20766, 33247, 44645, 98662, 62705, 5296, 6062, 16713, 27012, 204193, + 36366, 4251, 6513, 1097, 29844, 148369, 4030, 44421, 57946, 57215, 45204, 63057, 37932, 100525, 276977, 104126, + 42472, 13150, 108317, 106038, 5266, 1004, 31351, 41691, 20834, 27119, 14871, 42058, 19309, 18264, 15714, 128645, + 33753, 97813, 14991, 36632, 127182, 38788, 23800, 23029, 134259, 141169, 22689, 9008, 35810, 85196, 80190, 175150, + 41805, 96633, 36654, 189935, 45878, 63838, 3242, 5356, 312001, 228710, 66129, 4509, 14881, 203932, 11812, 70030, + 47757, 276830, 122405, 33146, 49251, 2261, 162697, 5363, 120050, 24738, 211941, 21746, 44252, 31697, 2242, 4877, + 3708, 85573, 85060, 82434, 25856, 115291, 56583, 56567, 107864, 962, 58671, 54581, 120347, 39508, 201071, 94108, + 1228, 71194, 12513, 225594, 36550, 6911, 160283, 35838, 41682, 115576, 28022, 16436, 34496, 5034, 74108, 10228, + 47025, 11047, 141530, 3837, 8393, 65028, 55696, 31079, 173365, 61729, 57479, 106029, 246526, 10526, 54647, 134609, + 12894, 3537, 244, 16862, 161607, 118386, 60183, 141700, 35670, 22051, 179401, 24135, 90785, 29822, 122577, 87924, + 126572, 2459, 80584, 28905, 2095, 87804, 54240, 102268, 124731, 60006, 15202, 109796, 157033, 21466, 164665, 37695, + 58694, 81513, 83134, 208222, 554, 5651, 7656, 87297, 12786, 33576, 15075, 146538, 9642, 40949, 163656, 9760, + 4817, 21064, 83245, 14829, 16136, 95061, 68060, 24365, 47864, 1179, 105850, 5322, 174698, 19385, 5399, 111971, + 66992, 363067, 36771, 86468, 4639, 166195, 77004, 80406, 69284, 96401, 199722, 27643, 10625, 105066, 89724, 58878, + 40710, 29791, 24556, 99909, 27763, 9231, 35125, 110086, 51738, 12458, 116193, 41661, 30404, 41774, 96495, 7041, + 264105, 37287, 172797, 19867, 137904, 45042, 61041, 151622, 109882, 58327, 51284, 132939, 52487, 238, 24806, 356262, + 42824, 71570, 114506, 221874, 57514, 290906, 425324, 6771, 2740, 77666, 51262, 18017, 10479, 14457, 11137, 19547, + 146799, 74299, 1986, 193822, 107390, 66292, 13142, 8549, 16586, 41783, 4738, 83585, 88038, 9102, 61338, 33010, + 174951, 5451, 103430, 20873, 9410, 71603, 254445, 29027, 16185, 19139, 109385, 57580, 44158, 18457, 29275, 116743, + 5568, 32928, 91629, 19307, 40658, 229962, 46426, 15411, 46108, 30487, 67181, 20224, 12763, 92267, 69682, 41491, + 97385, 46327, 89571, 20801, 26175, 104473, 82178, 53280, 108859, 90329, 60749, 15258, 664, 104876, 189856, 72942, + 230732, 51261, 34901, 19996, 67470, 20008, 38335, 18089, 46663, 52358, 14286, 59726, 14395, 26243, 124071, 9514, + 50750, 71549, 45061, 44126, 141320, 2803, 58061, 44739, 93140, 659, 64128, 26178, 15361, 168531, 40344, 8977, + 47997, 172770, 68707, 16055, 55784, 23990, 20994, 19090, 6791, 21011, 244531, 47352, 307840, 16546, 63361, 164913, + 118569, 30189, 34941, 229229, 107326, 24202, 160409, 1575, 18056, 16156, 162544, 1298, 58281, 140814, 38998, 4285, + 260415, 19797, 24212, 11490, 54691, 125241, 149765, 53575, 8790, 98579, 39432, 49317, 73332, 17200, 47059, 6532, + 45633, 17042, 110013, 4418, 7511, 26786, 2639, 40536, 45674, 39902, 37280, 138726, 143373, 16114, 16063, 95339, + 14031, 18222, 148011, 134245, 11799, 36311, 103728, 15146, 94491, 24149, 24405, 126162, 35646, 2622, 13619, 190698, + 96544, 59993, 46574, 579, 14560, 43052, 125756, 11698, 26049, 139612, 76126, 94179, 32983, 27506, 5021, 32417, + 25791, 73423, 53795, 119140, 83814, 24222, 419, 60678, 42094, 36193, 71555, 167797, 231370, 39846, 78400, 68056, + 63955, 1124, 59895, 8546, 139212, 47144, 37860, 26891, 2359, 163343, 60583, 105848, 169908, 4972, 13013, 132896, + 3108, 44849, 132211, 4330, 183486, 14009, 10090, 75230, 105867, 102476, 3031, 44769, 28197, 21633, 23419, 68902, + 32941, 109556, 36098, 52255, 124968, 209278, 40772, 6698, 26402, 57023, 171822, 87578, 88267, 23469, 27050, 64577, + 149768, 71917, 89979, 16941, 23053, 7594, 106397, 125192, 3078, 35227, 9172, 18615, 19091, 182038, 12549, 48594, + 52924, 6894, 86017, 20427, 25383, 22580, 75986, 18233, 19209, 61027, 86544, 26111, 111548, 24619, 166688, 24272, + 97361, 51184, 78541, 14792, 3959, 2430, 71174, 280134, 24880, 85091, 19069, 48720, 235061, 148747, 27783, 40579, + 9099, 95152, 259500, 59221, 24921, 73721, 170222, 102157, 161254, 66033, 357515, 82190, 151405, 105610, 28252, 213067, + 20508, 97281, 6878, 87399, 7159, 45662, 182676, 27626, 34381, 71179, 112126, 12802, 20133, 56316, 50576, 70823, + 11434, 14879, 96554, 27582, 74036, 24193, 21984, 147179, 19974, 41451, 8452, 161213, 34769, 115, 18749, 115303, + 36585, 8710, 130627, 54462, 1076, 15711, 78215, 45693, 22454, 41595, 35658, 31785, 17354, 64339, 5699, 30987, + 38727, 113863, 1046, 127166, 235160, 27501, 82135, 137484, 111547, 143478, 71619, 20477, 96454, 65400, 93505, 9234, + 117448, 71966, 130201, 9407, 156940, 10894, 113917, 102178, 91330, 3786, 25046, 137247, 37299, 14204, 156671, 48589, + 7310, 119658, 1019, 3147, 26915, 389655, 28024, 29905, 117060, 110822, 603, 9922, 51369, 186019, 151553, 53930, + 22620, 65936, 33869, 50466, 61861, 18339, 116756, 22544, 322264, 178320, 134504, 32779, 106850, 51259, 7921, 18753, + 111694, 76143, 15475, 39056, 15091, 96327, 38933, 146365, 2624, 6183, 303617, 83865, 40345, 8720, 102137, 208016, + 300446, 153481, 62817, 17230, 177064, 59995, 17444, 96781, 1707, 62069, 105642, 215627, 27389, 113620, 8641, 39778, + 54792, 22640, 92614, 72033, 327783, 56938, 97175, 28337, 132669, 24810, 100695, 42694, 183543, 96612, 26568, 321, + 59003, 67147, 64475, 124682, 17744, 254962, 92433, 55393, 20603, 153319, 316603, 192699, 151134, 16030, 30713, 5369, + 106923, 79389, 15318, 196516, 53084, 229057, 32215, 2061, 71803, 15710, 68210, 36730, 279424, 61974, 109245, 21881, + 319816, 40889, 10178, 55054, 11579, 30821, 76533, 48007, 21946, 12530, 41523, 56504, 16728, 146955, 90643, 77497, + 38274, 58777, 12829, 83673, 72711, 24324, 131406, 209463, 5085, 14864, 2408, 146954, 83391, 104916, 53219, 39654, + 88646, 106083, 13930, 24286, 40159, 28744, 20399, 11792, 25027, 26454, 82556, 24039, 34680, 36361, 145006, 21872, + 10752, 107608, 27995, 36258, 12988, 66287, 75099, 84038, 54126, 38128, 56142, 14292, 30365, 99229, 9312, 5952, + 18338, 50601, 15454, 40761, 100445, 4866, 42787, 168097, 230674, 27, 4416, 59458, 44874, 21538, 13837, 21543, + 84974, 32659, 181908, 81485, 143877, 1443, 22510, 44084, 123253, 114222, 131683, 77045, 139372, 123203, 151023, 23972, + 28082, 30654, 30914, 61473, 91477, 143646, 51334, 8042, 144002, 18818, 47219, 30784, 13096, 53692, 57020, 125132, + 219729, 72133, 94451, 32149, 46016, 5231, 19109, 89053, 50029, 67191, 30812, 104508, 42377, 43699, 106368, 9836, + 14601, 54570, 18766, 12632, 6660, 155889, 71980, 75016, 58244, 83344, 7256, 100628, 58978, 56720, 58199, 118422, + 23918, 11726, 37394, 463, 88206, 139614, 253619, 539, 113611, 38238, 154196, 29350, 64452, 9692, 12873, 4429, + 17541, 32212, 6089, 18497, 41032, 117229, 60868, 14143, 10942, 926, 24793, 66470, 12021, 18956, 23792, 155539, + 49189, 11284, 84405, 157831, 10978, 12543, 64410, 50098, 40175, 82131, 32892, 21615, 37156, 5526, 99592, 36215, + 10947, 19241, 20602, 2093, 71709, 93588, 80808, 10971, 106894, 25921, 413, 34040, 112538, 180819, 118821, 72357, + 57007, 79329, 16870, 137412, 137486, 10245, 90727, 18898, 150608, 14622, 19833, 22840, 152719, 29427, 209294, 4232, + 40615, 60643, 170375, 22011, 7746, 28136, 332881, 60551, 279716, 193813, 38074, 19946, 13101, 16840, 117701, 27751, + 19524, 59518, 5857, 368, 28708, 105821, 12973, 27739, 40578, 900, 41397, 104380, 72320, 33862, 8409, 34652, + 1096, 35868, 72140, 8303, 182051, 82682, 33389, 5630, 94527, 27756, 204584, 39519, 51275, 31654, 10240, 28759, + 22833, 178542, 47192, 48182, 45164, 83416, 42256, 42796, 81917, 217466, 53292, 37786, 77519, 106347, 83381, 18672, + 48508, 13787, 77506, 13385, 5421, 76619, 372545, 27228, 140302, 83313, 3227, 42955, 37845, 66043, 76055, 149143, + 149830, 12497, 9759, 138621, 5587, 153959, 83576, 136204, 27579, 39401, 30659, 75311, 5357, 6559, 74434, 7707, + 428725, 26547, 2082, 18025, 248187, 41435, 176983, 82585, 6326, 238794, 27806, 33103, 206760, 30220, 62067, 73068, + 39814, 3267, 31130, 1487, 32585, 16095, 47315, 334742, 89923, 102036, 75915, 77001, 44341, 23722, 4933, 28107, + 288753, 33496, 67090, 13693, 284443, 67130, 6821, 12171, 96368, 120123, 128906, 6889, 31201, 197218, 124216, 25556, + 94189, 226026, 49191, 116420, 119504, 22368, 28238, 62479, 20359, 140859, 29908, 42319, 52073, 25021, 11717, 171363, + 103216, 48554, 148106, 44322, 179, 62550, 142748, 5200, 27934, 626834, 53683, 40353, 32801, 386580, 59130, 42350, + 96035, 956, 88884, 71218, 34111, 41335, 31551, 1556, 34309, 7435, 32506, 89091, 101326, 35050, 97836, 7566, + 18198, 14509, 235440, 30012, 20704, 338945, 90305, 62331, 210266, 5359, 86970, 67633, 37643, 51918, 7476, 35122, + 27880, 2530, 23516, 55992, 141873, 9269, 20887, 235173, 106000, 53315, 71177, 78367, 19414, 8455, 3948, 72358, + 56614, 93522, 50567, 6412, 167714, 32465, 101863, 1914, 66483, 142566, 61810, 14328, 107885, 75527, 21510, 22073, + 86602, 3162, 170297, 80142, 4379, 139776, 150756, 52344, 20796, 126580, 47459, 31811, 75467, 203428, 2360, 109945, + 4987, 40280, 38609, 247457, 5017, 131195, 52873, 51358, 118857, 25612, 54684, 86642, 26003, 82237, 10347, 74817, + 34308, 134385, 105661, 2079, 114428, 3924, 56947, 20197, 29198, 93080, 30441, 23003, 6686, 189968, 44029, 59712, + 29697, 69462, 47863, 6319, 73632, 71419, 54022, 228432, 3739, 11617, 144267, 6304, 69795, 159284, 38182, 88987, + 16798, 60652, 18367, 39753, 41504, 26776, 44767, 4986, 7207, 326091, 10211, 275129, 30722, 15983, 114324, 26287, + 21436, 250022, 386, 16493, 36735, 47994, 4425, 57498, 28067, 7086, 86124, 96341, 28545, 29897, 71934, 19803, + 3239, 94102, 112964, 21957, 11221, 53105, 41589, 82164, 36031, 6367, 42771, 2307, 41889, 128904, 54967, 59098, + 100010, 163061, 65256, 39405, 19247, 129504, 97081, 10279, 317673, 79950, 84866, 47576, 29495, 35727, 17138, 23769, + 174554, 168948, 28307, 137478, 6424, 65666, 84059, 28007, 129725, 112584, 87500, 22631, 53845, 9237, 125865, 12109, + 94986, 62791, 47377, 95747, 7955, 119822, 43499, 77478, 59676, 37816, 112528, 83870, 2604, 10721, 277540, 129593, + 191497, 1803, 103962, 39100, 19735, 137806, 184562, 831, 102214, 21611, 10860, 96243, 62954, 12392, 277571, 104806, + 23844, 21269, 30123, 51663, 11872, 3731, 70610, 110093, 179525, 50391, 26607, 87825, 261436, 17108, 19172, 65210, + 34492, 179038, 18937, 8799, 428, 29645, 11956, 61342, 78404, 376484, 132083, 73837, 142035, 103650, 20615, 4466, + 16747, 74934, 38480, 234599, 17246, 46547, 32844, 24552, 27578, 22737, 103773, 39027, 37021, 1234, 22307, 95862, + 33672, 4191, 11010, 27369, 57944, 36384, 94490, 7931, 26056, 163500, 146122, 22564, 135760, 93787, 61065, 30077, + 2369, 6137, 12659, 3122, 61674, 56540, 24935, 25675, 122066, 26194, 26305, 22069, 31327, 2064, 15705, 149614, + 19374, 89531, 613, 93086, 157065, 5730, 15360, 6683, 40553, 8430, 74835, 94791, 130982, 74032, 11372, 90140, + 69619, 36036, 16092, 112362, 71290, 44790, 23930, 155440, 38855, 195955, 61949, 49611, 72100, 9710, 26268, 41136, + 92903, 169781, 27353, 78082, 95940, 112981, 249266, 45995, 51422, 17889, 6210, 74226, 165999, 87787, 28659, 84558, + 65713, 42221, 17212, 99031, 57873, 122295, 227056, 76534, 50726, 57460, 287606, 77186, 7288, 29042, 88166, 172092, + 20272, 22733, 128506, 113493, 2081, 55443, 102934, 214, 42326, 28948, 53196, 24237, 22624, 21099, 13480, 39377, + 81120, 35325, 45300, 24047, 57914, 47609, 64670, 25672, 79352, 7747, 71834, 161803, 19447, 8688, 10183, 9684, + 1684, 6277, 61421, 45761, 72302, 118558, 18353, 10661, 11774, 128325, 16327, 2665, 302559, 70280, 76546, 45579, + 161481, 169457, 36438, 37410, 96396, 127007, 10776, 56760, 13692, 115406, 41747, 83908, 414988, 69549, 169745, 58040, + 3721, 62350, 104731, 13605, 79066, 14490, 121161, 108219, 56627, 83538, 32335, 35780, 46883, 23245, 40346, 24451, + 21150, 129629, 31758, 47729, 11747, 2392, 5660, 43534, 12184, 23309, 97227, 201922, 5856, 75935, 22492, 245478, + 113458, 122567, 38892, 52163, 176117, 98436, 387939, 127565, 84416, 26809, 1689, 44206, 52079, 78841, 20795, 5683, + 27933, 162169, 34126, 12822, 3354, 45811, 72520, 20811, 59765, 13615, 3254, 29527, 141359, 123305, 19887, 90838, + 2212, 8885, 33750, 29379, 216309, 13657, 7475, 88895, 2555, 55375, 35969, 66537, 23458, 112987, 1751, 75280, + 196722, 96722, 67717, 118130, 142463, 83824, 80129, 105478, 45701, 183568, 315287, 14884, 44548, 167199, 36212, 100715, + 28798, 95743, 42919, 6271, 19418, 59193, 16434, 72701, 215, 108179, 34472, 75818, 29916, 15862, 29177, 1351, + 9396, 129616, 4305, 86650, 10574, 51218, 914, 206197, 114226, 53103, 156910, 12946, 84475, 16322, 71666, 47108, + 13520, 81329, 27088, 120745, 18694, 174187, 3645, 72390, 34056, 18867, 220604, 95316, 4524, 97988, 41515, 586619, + 90302, 23520, 19632, 127752, 62930, 258836, 36988, 204585, 13539, 57180, 13517, 6044, 19407, 65336, 268952, 132299, + 77209, 53483, 3327, 22672, 7728, 50216, 2729, 12196, 38088, 36872, 5799, 111465, 9535, 11303, 51899, 76725, + 2263, 23913, 3675, 253827, 23875, 65387, 63019, 12817, 183945, 28678, 43266, 62072, 17750, 269599, 29961, 5765, + 26274, 6555, 2446, 55197, 67172, 1910, 71875, 19799, 10585, 1419, 27911, 88939, 28042, 167002, 124915, 104112, + 22199, 47768, 14066, 16710, 7478, 99068, 196517, 131507, 51331, 27291, 42046, 63842, 66030, 117306, 144818, 41353, + 26774, 14822, 38660, 171065, 192929, 121185, 116712, 28895, 31434, 3911, 52612, 111118, 25850, 18697, 65634, 4147, + 50197, 74729, 15097, 117548, 52926, 274499, 54590, 79384, 178158, 113803, 36365, 137334, 4679, 5949, 253573, 27681, + 181256, 356354, 65776, 146248, 70184, 2871, 18045, 156661, 229600, 6542, 22726, 9001, 9959, 34743, 33915, 7460, + 105594, 269690, 12482, 86077, 72158, 12017, 58753, 24594, 73974, 3029, 1912, 30079, 2726, 109412, 146145, 35326, + 35085, 862, 90862, 85609, 78087, 43053, 160170, 33043, 23284, 4515, 162825, 69896, 35568, 601, 13016, 1407, + 51713, 90134, 750, 45520, 155676, 21397, 168585, 187237, 5401, 125230, 5635, 89220, 27254, 54715, 98930, 113085, + 11966, 3030, 1855, 149700, 17569, 56634, 16775, 51586, 223253, 10938, 121033, 70787, 71993, 76450, 39521, 26162, + 103357, 94057, 56597, 26906, 111477, 293134, 42368, 24553, 55722, 30882, 11930, 19889, 30504, 35653, 6466, 203139, + 26034, 287857, 19452, 2522, 46774, 8228, 76457, 83553, 35392, 6216, 12166, 56704, 36285, 6768, 54803, 1726, + 214814, 6895, 182419, 26778, 41143, 53690, 13669, 45646, 163465, 22665, 198804, 39125, 1051, 54093, 61411, 31560, + 16094, 26798, 90341, 277777, 81044, 169520, 129829, 46588, 6636, 71429, 29098, 27473, 76489, 47101, 118137, 125121, + 179102, 29265, 57351, 60270, 20712, 59437, 33382, 18626, 39178, 70695, 80048, 54642, 35683, 106381, 97513, 43264, + 125177, 120906, 35533, 22522, 54219, 7788, 92290, 6116, 30617, 6801, 86129, 39209, 52994, 53661, 59735, 17738, + 25324, 24278, 105977, 13689, 50123, 36059, 130088, 54180, 2543, 36656, 87050, 59769, 87529, 20220, 367, 68705, + 58995, 26101, 26380, 43246, 10688, 79793, 82063, 59968, 125199, 31463, 19802, 62223, 12388, 70063, 151361, 3296, + 60158, 33268, 27121, 110554, 125481, 31240, 69489, 60334, 131646, 25391, 20034, 24248, 7642, 55281, 33709, 57581, + 133350, 77700, 27095, 3522, 65874, 30518, 61307, 126098, 3438, 49052, 9849, 78050, 97277, 50748, 175256, 49826, + 101450, 107315, 118984, 13409, 10075, 128877, 62205, 13193, 56344, 25228, 87810, 2143, 116821, 7648, 113840, 19459, + 50778, 131885, 88512, 13697, 60547, 58403, 210177, 34494, 98016, 51781, 47807, 12099, 106135, 16443, 16925, 19635, + 13859, 8422, 14030, 4756, 14255, 48634, 3275, 4837, 16300, 230472, 6616, 53129, 77373, 22360, 111581, 9662, + 173521, 71655, 15044, 5531, 8285, 190633, 62896, 54909, 45932, 34330, 16255, 17909, 37426, 152464, 256859, 18903, + 4054, 67227, 5705, 135855, 114295, 14380, 28822, 86386, 55947, 44796, 22159, 43163, 7703, 65450, 5829, 97182, + 39114, 652, 2216, 44468, 52, 74475, 73693, 208207, 51119, 111015, 105280, 42780, 128135, 3956, 13974, 30409, + 19714, 40616, 22185, 44115, 60715, 199079, 86742, 81192, 9554, 53876, 58171, 29597, 50492, 316379, 10539, 3453, + 88180, 23111, 24529, 93240, 2823, 46332, 22213, 8752, 118271, 197846, 6618, 8946, 52993, 21325, 30302, 17074, + 122625, 9575, 29441, 295253, 97919, 3130, 132791, 140156, 23859, 8941, 106857, 22772, 37895, 107740, 9471, 34989, + 25040, 85180, 21330, 47109, 33614, 110324, 23189, 24151, 32102, 171390, 19981, 29005, 20431, 121, 38106, 170174, + 3577, 46060, 182390, 13411, 9275, 119138, 47329, 30160, 15686, 30347, 7585, 10003, 43031, 29151, 20512, 144355, + 157741, 153623, 16851, 99315, 110358, 156059, 69556, 9859, 1884, 75126, 4225, 180276, 40291, 131485, 17863, 1299, + 125391, 75039, 111409, 31614, 13736, 31156, 97629, 65733, 5008, 14589, 129738, 29549, 64881, 29351, 75196, 52675, + 87336, 57594, 21161, 14655, 77381, 35333, 37937, 262082, 70711, 100777, 11065, 52574, 43032, 79308, 11911, 5569, + 49155, 8990, 20956, 71672, 118587, 90936, 6794, 2889, 70494, 14885, 17291, 20073, 4318, 33042, 38735, 27931, + 10168, 11340, 174780, 29799, 30126, 32276, 416159, 9138, 12580, 186182, 69114, 30093, 10524, 55369, 90592, 23723, + 280104, 31769, 43457, 134915, 104001, 3107, 52049, 3483, 145413, 4347, 87847, 8340, 2862, 22905, 12749, 10655, + 84140, 32339, 14853, 21123, 6603, 75082, 30462, 29877, 106005, 84964, 69112, 129634, 13566, 31377, 1731, 2591, + 12780, 75605, 9265, 203857, 11251, 95054, 43621, 106786, 42830, 115761, 76779, 15968, 571, 316548, 48436, 23152, + 179910, 24939, 4039, 62740, 82443, 162336, 105433, 153188, 13146, 12020, 11190, 145468, 469, 151738, 6924, 16613, + 42714, 25880, 5783, 38804, 32591, 110905, 81649, 189448, 265217, 122177, 28046, 8852, 424024, 1774, 13702, 37891, + 92553, 66876, 68996, 31394, 54721, 100409, 93602, 51349, 134100, 42960, 121568, 58272, 6007, 12605, 20028, 3624, + 15242, 25008, 65373, 95897, 114681, 115646, 2589, 33333, 59030, 148878, 4427, 719, 16718, 23118, 3261, 37212, + 85465, 55213, 20762, 7510, 200214, 136975, 141829, 8623, 85982, 9053, 8985, 13680, 55174, 20625, 8519, 15392, + 165013, 16648, 8679, 27707, 23493, 74409, 23572, 32138, 56964, 21537, 197403, 32462, 82529, 23420, 28463, 4528, + 109150, 117327, 76538, 9244, 32706, 84770, 24954, 49185, 27568, 3481, 35176, 25954, 82442, 152974, 131562, 69937, + 5350, 25825, 141497, 121347, 14976, 75327, 17713, 2839, 13165, 257262, 30030, 30105, 44890, 162261, 56625, 19734, + 60021, 19579, 1465, 101402, 21343, 50719, 82005, 23880, 33978, 2744, 4244, 16973, 17264, 25584, 4273, 85481, + 4655, 19471, 172622, 36425, 22328, 212066, 128477, 64373, 27819, 33935, 83439, 54538, 75730, 73945, 182416, 338, + 16567, 164442, 82351, 56235, 55483, 38729, 47137, 36504, 14510, 39166, 16573, 4712, 17926, 119742, 48289, 74781, + 45827, 314393, 143249, 63030, 150609, 33960, 254056, 83767, 3704, 81354, 45727, 6473, 7385, 36244, 6886, 18673, + 272531, 4187, 62156, 112398, 161543, 82887, 4358, 87142, 76904, 76583, 39823, 167961, 122163, 68178, 11770, 14478, + 52405, 50115, 29516, 109139, 2039, 4206, 65909, 23385, 19165, 89405, 28262, 22275, 41623, 3099, 70734, 12924, + 14423, 41773, 25426, 95066, 228354, 10150, 40311, 18456, 3369, 167019, 217588, 126793, 176360, 66455, 4269, 8444, + 85491, 121695, 17697, 323, 7122, 20991, 35726, 50184, 35789, 94066, 146437, 243045, 303724, 21794, 8433, 198209, + 4465, 23672, 80873, 33604, 13628, 46964, 2602, 33500, 2233, 8434, 6196, 25551, 55311, 64859, 90756, 733, + 118771, 16152, 16282, 13527, 20713, 42651, 69883, 78249, 10006, 70583, 164285, 102376, 221519, 42660, 9468, 65430, + 45115, 136780, 41566, 157119, 71021, 40395, 88297, 10249, 35650, 41778, 28731, 28138, 29775, 49179, 39391, 51182, + 7337, 14843, 4441, 103029, 10864, 81753, 72912, 49213, 20665, 88374, 112909, 1667, 21142, 63823, 38287, 19613, + 1746, 41069, 30542, 41967, 15080, 138315, 9822, 40857, 1624, 120146, 62254, 46115, 32449, 11046, 21374, 514828, + 10905, 260390, 38829, 21553, 105743, 7303, 96235, 38405, 229797, 32678, 23538, 112753, 7701, 37587, 64813, 15914, + 3940, 40782, 259364, 20373, 22997, 77967, 19173, 76602, 178467, 82126, 9044, 83531, 57208, 74018, 5950, 34656, + 389057, 21826, 6662, 16035, 39683, 55167, 129407, 79420, 59403, 152449, 39047, 31506, 63344, 27006, 12334, 147213, + 63125, 155934, 26422, 197447, 54847, 124681, 52392, 3641, 69691, 15548, 83724, 62974, 18336, 43641, 194003, 56605, + 56448, 6561, 195097, 103908, 3362, 8507, 99274, 120393, 37202, 12934, 69852, 54075, 18282, 7789, 50160, 102080, + 29648, 97272, 47381, 12391, 138224, 47286, 208664, 50910, 35867, 32185, 28804, 64164, 10495, 11850, 159760, 137513, + 5911, 76063, 12977, 6056, 28814, 21821, 2163, 130, 26653, 229563, 675, 34076, 31514, 47917, 92810, 44791, + 176702, 25297, 80044, 28279, 26550, 62323, 9943, 101265, 45621, 173758, 88568, 219069, 11734, 117073, 111186, 26075, + 4525, 39923, 16003, 12712, 40543, 7197, 150583, 16316, 73944, 199805, 158502, 7166, 121080, 2343, 53537, 17725, + 27858, 14692, 138991, 22323, 155561, 72448, 37087, 173360, 14887, 2310, 89844, 54066, 44670, 35610, 30471, 49008, + 30742, 32492, 123549, 16741, 8796, 69544, 57441, 97055, 107455, 22125, 10594, 123866, 113472, 2733, 85686, 54673, + 56369, 34761, 5044, 12915, 75581, 8965, 47647, 30073, 183777, 13677, 34414, 87158, 240095, 56678, 23997, 13674, + 133699, 17662, 364, 13753, 153299, 27177, 51527, 30243, 8768, 26167, 16767, 50595, 160464, 166312, 23739, 14534, + 26058, 9664, 63302, 110621, 49078, 86820, 10195, 18754, 103971, 41541, 46431, 27835, 21875, 167947, 172353, 12902, + 71486, 20686, 45374, 12571, 44888, 12274, 1818, 10422, 17156, 10122, 31744, 9367, 9678, 87337, 19033, 70558, + 89541, 21373, 2670, 9033, 123019, 13271, 234210, 43826, 102337, 11809, 135892, 7723, 3972, 64409, 19618, 54008, + 83930, 155668, 38822, 37966, 21245, 24138, 260, 246255, 87852, 28211, 156411, 8088, 109660, 68896, 82086, 248065, + 287918, 183132, 99271, 104331, 183019, 20735, 38511, 16336, 686, 18533, 18914, 36568, 10100, 17413, 11801, 17493, + 39177, 49978, 80098, 133024, 283941, 8179, 153303, 913, 11274, 22090, 73741, 81799, 24736, 36017, 34397, 5355, + 26793, 74880, 144578, 239455, 26214, 19233, 17629, 106193, 25995, 57924, 89963, 116991, 77011, 261582, 364267, 12039, + 141580, 15178, 36187, 9064, 4070, 21836, 104740, 12532, 23742, 192159, 139401, 14516, 46285, 50127, 9705, 30183, + 46632, 6312, 66032, 10073, 30700, 26025, 26702, 43421, 26669, 6136, 155289, 120269, 19056, 202531, 43062, 10321, + 35951, 149425, 302834, 15999, 115575, 92927, 51885, 95094, 174034, 1831, 20175, 39292, 56097, 9329, 155235, 20052, + 35463, 55521, 17719, 122027, 87425, 145479, 31818, 5229, 24575, 132139, 118737, 52992, 44245, 16168, 78384, 56556, + 38701, 11367, 88487, 19022, 82317, 214446, 53146, 132874, 85922, 28449, 40982, 81866, 281616, 112901, 26578, 190706, + 100333, 155311, 101029, 171716, 147697, 12430, 68023, 26065, 61503, 69034, 60721, 126933, 7730, 7965, 21463, 59048, + 84330, 17699, 17875, 37832, 8530, 54375, 218360, 53773, 59917, 9867, 92197, 54218, 61597, 39007, 87092, 58775, + 17173, 53529, 33744, 101641, 9092, 6126, 34354, 17856, 32658, 23212, 16624, 40012, 90288, 66804, 30957, 193996, + 193136, 3361, 126541, 62118, 39023, 18809, 8034, 19719, 20381, 66386, 64493, 20206, 56654, 11892, 180795, 70430, + 31132, 148921, 124862, 23413, 7779, 38708, 40301, 16544, 1919, 80033, 29947, 93475, 1375, 135168, 156926, 69211, + 117128, 57078, 75276, 39285, 30819, 18464, 3044, 51097, 11169, 214069, 300112, 18592, 40938, 132884, 51336, 55473, + 23935, 202263, 99605, 7252, 115201, 18984, 268130, 87746, 101155, 21993, 7612, 2978, 151034, 53745, 151729, 174929, + 4835, 64678, 53387, 27068, 11231, 14136, 30257, 163776, 74550, 15754, 8669, 6350, 89388, 45349, 422995, 68021, + 59951, 87642, 86425, 54667, 91704, 28427, 56079, 64527, 107312, 2367, 6715, 32058, 167882, 83377, 9472, 24984, + 115062, 35722, 33140, 156862, 12732, 24084, 23697, 34539, 72738, 20672, 102578, 11210, 88703, 7244, 19853, 19168, + 464019, 27128, 46941, 50269, 158267, 8850, 158112, 51669, 57995, 41368, 58379, 14134, 60496, 91738, 13630, 44359, + 737, 15344, 120328, 46261, 14371, 8214, 53796, 49253, 123867, 56387, 104801, 7333, 4174, 48503, 43922, 3083, + 243339, 116418, 479757, 153147, 159946, 19349, 47019, 17868, 7568, 17831, 7985, 56769, 16025, 112323, 7079, 40969, + 134556, 11297, 18538, 58669, 110916, 153620, 73377, 72354, 38103, 205536, 68495, 102706, 191, 10869, 164292, 31753, + 80226, 87342, 114379, 12760, 88794, 19334, 85112, 20828, 29688, 22880, 32405, 3197, 27230, 29826, 77087, 46535, + 10454, 11432, 110215, 23620, 76308, 72189, 116329, 168613, 57647, 19673, 10378, 1049, 77409, 28757, 24133, 588, + 113483, 16684, 61242, 31088, 66864, 24674, 161602, 3529, 14745, 90530, 299150, 6673, 19808, 84006, 14057, 114223, + 12023, 167545, 57708, 91489, 46583, 15662, 2782, 13163, 84805, 1309, 47528, 68166, 16015, 48871, 44523, 145426, + 17102, 65184, 54856, 101626, 2231, 162868, 38087, 134570, 20611, 72893, 296437, 103821, 3547, 51502, 32402, 63371, + 95740, 8947, 63165, 25224, 250131, 70323, 10235, 39906, 34559, 51697, 134092, 90702, 108894, 201322, 13521, 98255, + 8498, 173210, 61323, 5939, 15853, 2071, 83348, 11131, 159169, 47234, 2625, 1728, 148920, 59236, 14351, 20915, + 20942, 19005, 8569, 220082, 2813, 129877, 76369, 208632, 93160, 15477, 19266, 71454, 45188, 37118, 21981, 734, + 210613, 24054, 1267, 258926, 45531, 14333, 1358, 4214, 52587, 73176, 70405, 3934, 149062, 67102, 129336, 24604, + 39782, 144525, 88004, 81838, 28194, 51093, 36216, 42928, 57849, 8118, 2715, 191067, 60965, 105811, 65180, 7052, + 84954, 70694, 46912, 219608, 89766, 22029, 26626, 102536, 84453, 50777, 25605, 105083, 100927, 20688, 87599, 26842, + 16501, 4589, 1582, 37485, 27658, 50645, 120746, 2335, 165311, 11419, 118946, 1635, 103841, 81324, 26376, 135646, + 54192, 116632, 21545, 33403, 207341, 58353, 177692, 33129, 19558, 9632, 75823, 7780, 20084, 107884, 116296, 109946, + 319622, 58315, 14925, 134360, 5672, 15528, 113198, 68474, 205467, 66116, 49681, 2705, 98462, 83417, 21258, 159469, + 61849, 81586, 62636, 15482, 36279, 20980, 9940, 193129, 13609, 130807, 18949, 73964, 147177, 131897, 86637, 146769, + 24726, 30328, 30775, 29789, 165015, 16356, 4333, 5505, 209489, 79847, 8748, 132099, 59591, 103870, 50045, 162834, + 31157, 71923, 122346, 6112, 6551, 139841, 45179, 43676, 117580, 19506, 44727, 106994, 75060, 69628, 17203, 46010, + 141146, 9659, 247052, 66602, 277310, 21659, 46258, 176126, 21072, 87, 20184, 63737, 22023, 124145, 55015, 107649, + 106474, 147290, 65612, 13076, 63041, 16396, 150430, 62688, 137443, 6987, 49604, 88814, 122965, 88723, 27058, 177180, + 68371, 34502, 30567, 11200, 5383, 48204, 26504, 19554, 42146, 47062, 6975, 51017, 98961, 25976, 71879, 161741, + 113467, 13050, 91074, 277058, 30863, 61884, 41533, 46948, 23794, 16521, 149829, 35815, 4843, 40881, 56017, 95769, + 99630, 72286, 99851, 13623, 30392, 51474, 63363, 63865, 82679, 1059, 168866, 25195, 13699, 121522, 234449, 35601, + 241612, 30212, 73616, 264919, 33601, 161573, 60734, 72643, 93146, 104874, 19083, 97309, 24319, 146272, 53100, 87181, + 18643, 3074, 12143, 84691, 32155, 10902, 38113, 83987, 95669, 22320, 37308, 44763, 40440, 203540, 152769, 7319, + 15333, 37687, 43812, 63607, 34089, 899, 246178, 71268, 67799, 16016, 114972, 58528, 142144, 3955, 144552, 72635, + 58245, 136701, 104014, 243, 38633, 62199, 14295, 9747, 114531, 27309, 21640, 159861, 117400, 124053, 13195, 210463, + 77861, 81073, 239628, 226797, 188726, 25428, 49381, 139825, 5507, 45355, 15269, 48541, 2568, 12101, 40308, 1768, + 8853, 78278, 55853, 27498, 10987, 12866, 22855, 16207, 107222, 28940, 68976, 28505, 2663, 277982, 71506, 191712, + 2421, 165066, 37699, 52827, 11530, 112085, 187070, 14784, 13345, 2370, 197969, 71689, 30075, 93786, 97183, 71992, + 41785, 19656, 26541, 5218, 118661, 37497, 14909, 185795, 104786, 64176, 31138, 67561, 17459, 21130, 111703, 11368, + 12490, 45880, 38409, 147530, 16281, 12336, 20898, 10505, 71936, 39455, 49254, 62813, 193555, 86430, 18811, 97787, + 17431, 50448, 85973, 4047, 5944, 9900, 65788, 238170, 71758, 45771, 89284, 65578, 26485, 49627, 32381, 33713, + 77317, 8559, 35413, 14870, 20803, 34468, 81897, 94234, 367167, 24080, 137854, 191387, 158, 7578, 65751, 15809, + 7362, 17010, 196493, 65502, 93430, 391382, 2879, 10420, 11735, 7147, 23542, 17615, 172445, 156086, 37413, 42670, + 46002, 31761, 57780, 41672, 11532, 25360, 90866, 49967, 54482, 3553, 67022, 173415, 930, 48911, 25321, 44848, + 62911, 34519, 229774, 187702, 2235, 26813, 21693, 1315, 23004, 97752, 23681, 170907, 179236, 168028, 11780, 33446, + 4764, 8196, 13633, 286646, 101859, 29094, 37084, 18677, 208113, 11037, 67253, 68845, 22477, 60395, 22179, 83654, + 55163, 30814, 111690, 84894, 95579, 111070, 15123, 2301, 14098, 14628, 22693, 64944, 67320, 32427, 113228, 8450, + 162556, 30175, 61058, 80543, 90709, 143529, 88741, 208523, 156949, 1923, 33966, 23151, 3826, 241299, 16138, 83350, + 57492, 27183, 107353, 138052, 4025, 107597, 35297, 67773, 34092, 30452, 43300, 6957, 87442, 94684, 16965, 217438, + 104565, 70559, 98891, 21648, 6718, 16784, 149691, 99066, 186015, 19497, 66551, 37693, 28214, 16720, 64083, 40532, + 14209, 87486, 1612, 145702, 10039, 70355, 14323, 130951, 107186, 119516, 74814, 104148, 233912, 48066, 30803, 17404, + 58877, 26118, 50223, 44594, 81637, 205665, 99360, 81833, 55265, 26920, 28438, 30781, 39828, 1038, 31826, 48903, + 6194, 56604, 14761, 59828, 145813, 74771, 74706, 51758, 50831, 37050, 3597, 24506, 105849, 6593, 4154, 16139, + 4974, 46766, 28473, 30674, 88319, 27775, 32504, 6677, 122296, 25830, 25628, 152679, 10272, 18637, 3167, 49269, + 197216, 13892, 17101, 74035, 95714, 67486, 53321, 82319, 51540, 39761, 17803, 187333, 72418, 71349, 30143, 35120, + 23324, 149892, 42804, 9890, 91555, 30670, 7507, 27360, 8743, 12725, 15462, 94244, 140452, 44821, 17416, 38926, + 250249, 54572, 82822, 54752, 51666, 63387, 47442, 57021, 34124, 37290, 40715, 29430, 7229, 111417, 75006, 22299, + 38592, 3207, 31696, 25882, 129641, 85221, 119327, 11951, 78169, 25237, 51044, 149983, 174242, 9947, 220995, 4324, + 22464, 397659, 78193, 25301, 149964, 59306, 234039, 11815, 51450, 116927, 58974, 159239, 14034, 75956, 10213, 91547, + 10026, 88574, 19060, 33083, 95376, 47430, 31034, 61653, 26190, 36085, 5131, 14374, 120062, 15192, 280008, 9263, + 14401, 19099, 200440, 66652, 8700, 156222, 62663, 66966, 265, 110, 148040, 36034, 31386, 104323, 17822, 32638, + 143573, 164335, 16580, 50402, 7203, 38721, 213812, 21515, 229889, 8504, 38602, 75516, 61567, 60579, 12745, 46326, + 4227, 18582, 60229, 59397, 140981, 39037, 55638, 17735, 2466, 3755, 51288, 30552, 72052, 186323, 70031, 82764, + 10787, 256, 117464, 143130, 10062, 6313, 63167, 28509, 30958, 1511, 26452, 130270, 6099, 62843, 2008, 134723, + 38471, 103714, 11981, 137269, 30103, 21650, 155870, 27623, 23202, 21416, 31748, 136202, 208101, 42177, 21612, 97179, + 70847, 80823, 26151, 15957, 467, 19669, 80201, 152985, 58934, 49413, 43187, 165152, 32271, 3413, 278897, 95326, + 32984, 22407, 4165, 5889, 36637, 54267, 154498, 84424, 24107, 32263, 13642, 61899, 30771, 48906, 53541, 77288, + 17109, 68812, 133945, 23919, 73353, 73829, 91032, 251994, 13650, 62276, 107145, 232161, 2098, 1645, 1664, 247395, + 157040, 42258, 5942, 117930, 67366, 16060, 9794, 122685, 66904, 16976, 197964, 13983, 106018, 68009, 103583, 28958, + 265380, 17355, 73225, 43935, 107238, 21443, 155998, 64685, 18535, 31098, 26652, 188152, 44025, 21291, 51390, 24741, + 32681, 22989, 67962, 69432, 144983, 171068, 156235, 7891, 62505, 30254, 83172, 66755, 91295, 123868, 35802, 115707, + 56120, 334807, 135497, 21871, 3082, 226529, 127778, 48841, 77508, 143672, 108714, 27565, 10322, 144014, 44830, 149778, + 63023, 9719, 13437, 27943, 36700, 13695, 163539, 196344, 81885, 30099, 44647, 4703, 224127, 11553, 28255, 159827, + 16721, 24326, 85789, 18228, 45023, 10808, 22936, 17273, 239261, 46240, 15558, 55286, 111272, 53778, 10007, 200688, + 13852, 33199, 25937, 118127, 7866, 95568, 13550, 69075, 149243, 18187, 18054, 139272, 204199, 48032, 9916, 53168, + 32309, 66646, 20390, 30523, 22084, 55674, 32559, 215681, 42029, 99514, 103068, 63726, 38316, 8856, 122667, 9308, + 126644, 295281, 11559, 40999, 104973, 114406, 69105, 9022, 14406, 80819, 104640, 60160, 43454, 8575, 34276, 11096, + 67322, 37022, 36926, 101052, 61310, 36620, 61086, 109693, 15789, 9610, 221009, 16189, 40285, 3194, 57111, 7696, + 24026, 1071, 17787, 219517, 181047, 102229, 1436, 19143, 6301, 110183, 37601, 45487, 70927, 56572, 105459, 74084, + 23319, 69989, 91217, 16551, 115823, 99155, 38977, 40934, 27248, 94397, 86590, 107504, 66693, 29641, 1379, 47255, + 115875, 1054, 8435, 39144, 278566, 3140, 317123, 121774, 63007, 54, 8414, 27632, 146844, 17916, 144167, 46464, + 56841, 9985, 60753, 54973, 59007, 15854, 105030, 302270, 87368, 102284, 52117, 2320, 180001, 24004, 45415, 28122, + 22370, 12080, 4179, 143103, 42114, 5196, 9147, 23819, 80605, 58583, 158409, 10286, 12022, 7119, 150321, 118598, + 10374, 25544, 101645, 10354, 308, 97195, 61157, 56511, 25079, 3266, 28236, 118492, 14689, 20295, 135126, 19093, + 12618, 57448, 107655, 29480, 63368, 199518, 134395, 42712, 7936, 62939, 58228, 35501, 264973, 47880, 112138, 63936, + 212291, 63680, 36241, 9561, 136713, 9208, 3926, 120889, 95999, 43551, 83774, 6921, 105801, 11525, 3247, 91697, + 18965, 18822, 61436, 115290, 32075, 47003, 24387, 26636, 48700, 190949, 19812, 48361, 52230, 62488, 108527, 105631, + 35119, 118159, 8412, 2552, 96912, 124705, 45876, 32587, 32992, 107747, 77489, 51983, 8586, 88880, 11803, 52063, + 16606, 162643, 143626, 89658, 101333, 22654, 101310, 38641, 101812, 20259, 123750, 2503, 14969, 219100, 8690, 57801, + 39930, 59910, 37399, 71781, 759, 10810, 116498, 88252, 193090, 2214, 139472, 14511, 27387, 12596, 1241, 7718, + 42914, 11603, 116092, 73428, 12937, 23266, 15835, 53439, 5058, 18649, 34255, 102275, 62646, 29092, 74301, 111969, + 64528, 103339, 89133, 263917, 38624, 31458, 186803, 51532, 25743, 71285, 12736, 12343, 37502, 180824, 143025, 172311, + 3716, 6203, 6498, 22229, 4435, 2166, 66689, 87857, 30352, 26521, 32385, 19406, 178687, 47754, 51273, 121646, + 26461, 8198, 36440, 4640, 132611, 45114, 31837, 69521, 42002, 24437, 25080, 46669, 138442, 89271, 46945, 24420, + 35833, 124503, 8025, 46899, 59582, 24849, 44172, 115277, 16345, 29941, 42848, 14801, 8048, 26136, 36090, 41362, + 60319, 2074, 33712, 41656, 49349, 63229, 13209, 66031, 309, 4824, 48391, 36461, 47800, 73514, 39421, 155688, + 49739, 46104, 1216, 56340, 90482, 5712, 163879, 113513, 26405, 9919, 71117, 80878, 34470, 7576, 186, 167527, + 63786, 17343, 68724, 45616, 32479, 50203, 8150, 47235, 85028, 41439, 143352, 4168, 39866, 18661, 19475, 52046, + 47846, 51344, 13929, 353722, 11649, 34406, 89897, 29002, 23934, 68639, 14094, 75872, 29466, 43863, 63280, 169603, + 2816, 5244, 32027, 29855, 42864, 45790, 121470, 68468, 31828, 7242, 12594, 14488, 7410, 33485, 88169, 76478, + 74885, 61809, 68536, 70978, 49632, 26100, 42262, 112129, 47629, 15034, 77852, 1153, 111801, 32807, 15276, 117727, + 90749, 35188, 38118, 105626, 19536, 124809, 8721, 101778, 18767, 7320, 62401, 5488, 105764, 8155, 101412, 36533, + 59606, 23477, 13883, 40321, 21223, 13491, 12275, 43235, 10746, 12781, 61840, 152362, 76298, 7826, 23347, 19020, + 22220, 93982, 66332, 35455, 39408, 6329, 112746, 96397, 7190, 38758, 5458, 105620, 79654, 98403, 59395, 11902, + 64856, 56883, 35273, 53643, 11602, 20326, 70616, 82969, 82156, 35788, 123268, 58910, 272765, 24592, 15867, 1454, + 17079, 21042, 67057, 18817, 70089, 24840, 111862, 91164, 245473, 26466, 103325, 34583, 51813, 59727, 75940, 43370, + 184407, 39378, 10508, 122637, 384678, 128473, 172589, 103341, 1576, 55027, 79993, 6639, 122249, 56459, 5014, 77265, + 5064, 51717, 32582, 2149, 27481, 34880, 18933, 503, 6188, 76698, 48184, 81280, 25790, 6378, 5599, 159007, + 74361, 6010, 125775, 18286, 27541, 83541, 66715, 25065, 318284, 67687, 26494, 145603, 45430, 73737, 1093, 24588, + 31488, 141097, 46614, 41796, 620, 39230, 75054, 18365, 93579, 36160, 184470, 32372, 45723, 48418, 250572, 261817, + 192118, 22725, 77160, 79580, 22670, 4248, 83282, 74287, 51913, 89394, 15782, 18868, 4162, 31369, 195445, 114671, + 70244, 80847, 32760, 73941, 35966, 33327, 48176, 61263, 26397, 21891, 35782, 51428, 16199, 17361, 60996, 162215, + 50899, 70443, 196905, 14327, 209613, 277476, 31457, 115726, 121702, 1643, 41064, 101937, 287507, 200215, 40259, 17132, + 2993, 39858, 66709, 78788, 36101, 45516, 276535, 10475, 132229, 74041, 85837, 4489, 67345, 47555, 70268, 21923, + 33062, 17585, 37566, 31019, 76295, 41197, 33727, 44308, 118628, 54158, 74493, 8091, 78705, 83923, 8776, 31089, + 52316, 104384, 21180, 13077, 34375, 98798, 124584, 38929, 107083, 5305, 11827, 45799, 107454, 122628, 99613, 39711, + 44863, 77878, 47979, 163774, 127561, 55355, 79908, 233991, 33964, 8846, 147975, 196384, 3073, 181199, 4641, 18878, + 154010, 234469, 1978, 29642, 190914, 72852, 147040, 33070, 55967, 226887, 13739, 90555, 39074, 42255, 11101, 11143, + 6272, 2958, 5785, 149827, 31047, 148068, 44726, 20098, 5550, 34454, 68139, 117608, 41123, 74247, 21830, 126493, + 26154, 125253, 9928, 34238, 98638, 40988, 315243, 29780, 47110, 42038, 38685, 1249, 19998, 18504, 2563, 17213, + 148091, 6500, 13838, 19244, 50229, 4746, 251846, 112081, 31329, 48587, 8296, 216791, 59900, 99134, 13938, 168292, + 195442, 43920, 20408, 15133, 19106, 21571, 58002, 11833, 61347, 98426, 10306, 95246, 73497, 108255, 62936, 13502, + 70015, 18245, 80358, 41111, 682, 47734, 11486, 103861, 45850, 5615, 51099, 134183, 25776, 191909, 70530, 132159, + 38022, 64318, 63079, 172030, 148951, 284196, 101745, 31146, 6288, 10262, 10014, 172794, 37411, 22511, 4387, 112723, + 232526, 23910, 161525, 17672, 109277, 67584, 32161, 96383, 27286, 345858, 68047, 143833, 32342, 125891, 44280, 13086, + 9262, 166694, 69189, 41261, 5220, 24538, 15818, 21924, 16651, 109563, 5340, 30385, 23175, 91017, 49288, 45540, + 46740, 114503, 244673, 25970, 129438, 46907, 33785, 227986, 78614, 21905, 31585, 114441, 121925, 1940, 19917, 21156, + 66914, 81575, 3244, 30495, 88710, 29655, 12313, 83379, 127952, 105486, 99459, 88635, 5563, 32187, 8229, 94749, + 21500, 48758, 166385, 14479, 34521, 359597, 72504, 153813, 10739, 78835, 39295, 138067, 14863, 122543, 48540, 34380, + 191006, 11035, 196034, 9752, 62956, 65440, 80639, 387, 17359, 20899, 93399, 207191, 16749, 28093, 88121, 92904, + 67027, 59025, 67931, 87918, 56284, 135160, 87875, 81632, 69134, 75164, 29710, 188499, 43301, 19047, 13422, 106967, + 35039, 65093, 55023, 107550, 58883, 53155, 1578, 14587, 54466, 100984, 69351, 32950, 60823, 25977, 174836, 15869, + 404451, 6689, 11576, 4477, 75743, 45266, 31052, 16005, 59856, 29472, 81237, 29067, 86979, 42164, 23945, 46676, + 7923, 90552, 46853, 182972, 34273, 42374, 17945, 13686, 83785, 52585, 13309, 23870, 32142, 64343, 98952, 28074, + 7693, 4539, 24893, 39020, 268986, 16664, 39061, 84393, 197428, 80361, 205940, 1224, 282681, 6882, 9445, 49939, + 17049, 191596, 29434, 55100, 22346, 54975, 127831, 732, 22990, 126521, 11455, 86007, 92245, 138159, 51749, 151336, + 107180, 1069, 19546, 41449, 3357, 159316, 6574, 4724, 37104, 15238, 26063, 24160, 96724, 37317, 18138, 7223, + 49153, 51769, 152694, 7631, 7683, 64472, 8352, 2685, 31197, 127743, 6860, 92869, 43267, 85011, 42057, 23724, + 82231, 8741, 18674, 7910, 164276, 4096, 12771, 7586, 23696, 47054, 195099, 1416, 20848, 13504, 357403, 2764, + 188364, 105560, 295, 178445, 22309, 57234, 22103, 107666, 24821, 35099, 28676, 58490, 158707, 25657, 23518, 61519, + 1018, 46602, 17455, 53294, 22514, 62556, 170305, 115366, 70922, 69405, 15098, 71322, 27792, 76230, 27885, 2441, + 45589, 36981, 150699, 24146, 59709, 81228, 22766, 66205, 10765, 37617, 9373, 49056, 736, 99650, 67177, 559, + 35218, 47852, 5803, 7500, 63479, 81545, 7010, 84110, 51987, 114840, 24620, 8163, 24275, 88890, 163648, 134506, + 63588, 23081, 142828, 65953, 55361, 67896, 114542, 9127, 92929, 19906, 111372, 38827, 81964, 49480, 42737, 12268, + 4658, 112744, 27101, 301, 20122, 14673, 94899, 206599, 12330, 76979, 31622, 74309, 44058, 128517, 56436, 14073, + 13065, 23339, 21315, 103178, 311456, 16278, 14920, 198146, 72224, 420550, 41727, 777, 8337, 104777, 24184, 25793, + 211229, 26740, 119387, 100011, 38979, 100498, 23747, 45421, 22590, 8336, 22845, 14459, 138478, 53166, 57049, 20497, + 52757, 82151, 2460, 50662, 32595, 50914, 9779, 140220, 133600, 20746, 24104, 216217, 8838, 122361, 11593, 28760, + 31549, 816, 28187, 5501, 94412, 60114, 28281, 153116, 43391, 8488, 90398, 47350, 90056, 27922, 39104, 94601, + 1585, 8966, 10638, 10171, 94802, 8318, 14529, 110590, 127271, 73877, 11430, 2830, 6223, 27005, 16811, 21014, + 31889, 241922, 77341, 77320, 137038, 18139, 50332, 123737, 132910, 94235, 16743, 82586, 2165, 47123, 21947, 68249, + 57616, 1395, 50542, 129396, 230152, 209588, 78454, 147757, 6080, 219127, 4180, 9021, 10748, 81158, 64973, 29190, + 36737, 228622, 98804, 17829, 74579, 16417, 183595, 101604, 134062, 17306, 3644, 19380, 50525, 72396, 159940, 117382, + 180532, 78857, 55739, 98983, 119270, 38236, 8379, 25607, 34556, 33219, 34803, 98799, 76155, 37523, 75966, 6648, + 82394, 4084, 98676, 3845, 52595, 13580, 58240, 1922, 29258, 10438, 105425, 26130, 31435, 85783, 87939, 115936, + 87820, 77028, 181067, 59464, 67996, 9819, 19251, 40273, 26943, 18184, 84410, 39092, 183878, 10146, 8789, 33548, + 38007, 71479, 208117, 24698, 2410, 113333, 13181, 6605, 13526, 49339, 7061, 64271, 180297, 17014, 2971, 168674, + 69856, 33945, 110699, 265836, 3503, 115232, 136418, 50952, 187396, 40638, 4807, 156118, 167700, 13849, 57520, 81231, + 7838, 11640, 12170, 5741, 16701, 16659, 125534, 15317, 9199, 52795, 24781, 6825, 56267, 83437, 204926, 74158, + 3661, 59223, 14235, 194403, 37407, 20530, 23146, 12357, 65994, 11931, 56380, 259451, 23767, 79929, 18293, 110440, + 5708, 110566, 1381, 116346, 62508, 48437, 65252, 42437, 221700, 23408, 20821, 78800, 67088, 5214, 80178, 40659, + 86978, 3139, 87525, 38590, 46776, 96503, 7226, 124649, 84434, 21210, 52718, 39533, 32088, 11610, 48883, 48993, + 5612, 36169, 74879, 111083, 9149, 156582, 123119, 79206, 88244, 36781, 6276, 121833, 21685, 67708, 562, 32969, + 95151, 49905, 11821, 49025, 46750, 363738, 60238, 7126, 189612, 23817, 135205, 79928, 2979, 54100, 109851, 73077, + 506311, 12222, 150050, 90908, 2594, 81368, 57202, 25388, 3628, 28737, 8460, 86804, 40074, 10968, 92876, 5499, + 105039, 2695, 47351, 172227, 78243, 121715, 27084, 78833, 28523, 73676, 464, 68232, 6651, 130040, 127800, 48799, + 38058, 37843, 5052, 96560, 71999, 133710, 27378, 191856, 30992, 147444, 29030, 53817, 12764, 121245, 60444, 26643, + 68261, 39242, 16699, 155639, 108991, 19332, 42990, 80805, 6165, 95293, 82667, 375680, 26450, 33561, 31227, 248811, + 61961, 7643, 142037, 7514, 13400, 107606, 34976, 50694, 22426, 151745, 198926, 23162, 7490, 69785, 8890, 277275, + 60890, 30537, 37432, 49609, 109623, 3559, 109101, 157822, 2070, 19341, 18250, 88785, 12958, 30738, 47073, 37163, + 50355, 61092, 55664, 18154, 67979, 11874, 16017, 16832, 257096, 63841, 46836, 35435, 7213, 39562, 77677, 20617, + 42578, 32643, 98441, 139236, 52121, 64862, 68450, 282715, 35716, 2199, 97719, 13226, 65461, 127411, 66119, 58368, + 7516, 8148, 55990, 6956, 124758, 9239, 3153, 62014, 39268, 163536, 46944, 43855, 302, 6682, 207287, 15207, + 64712, 56673, 22223, 78977, 14977, 22415, 238137, 21853, 1467, 6198, 107406, 33222, 219452, 21709, 119024, 34391, + 2840, 1157, 34974, 22756, 34229, 50276, 12565, 13069, 11121, 120511, 69104, 16271, 21602, 41109, 62931, 15756, + 19270, 52519, 17405, 24235, 63574, 6789, 324542, 136115, 8024, 15348, 17892, 47562, 1532, 70350, 35583, 71230, + 17331, 3309, 46253, 26611, 79839, 99277, 117997, 65915, 78885, 32688, 25828, 19004, 52029, 50625, 9248, 17400, + 180767, 38886, 29357, 68385, 57957, 5909, 37897, 76460, 6069, 20372, 5141, 50706, 91265, 87494, 32650, 234722, + 61380, 65571, 34714, 45634, 55767, 26279, 65231, 106901, 8927, 283, 16073, 103627, 32881, 18500, 150143, 38519, + 287603, 17485, 853, 34227, 22149, 485770, 39484, 23090, 35029, 31381, 51798, 78528, 68876, 38737, 36453, 236345, + 6428, 12075, 1812, 27252, 199567, 13210, 14175, 2341, 46926, 622, 28321, 38887, 13412, 97447, 15960, 114377, + 104132, 9242, 11929, 173622, 21434, 107890, 50877, 49000, 366616, 878, 47215, 100194, 45060, 104282, 141046, 35203, + 110046, 219551, 85771, 84943, 81924, 108674, 74715, 12699, 128910, 32654, 6935, 167969, 45886, 48348, 61573, 81800, + 52821, 34060, 4242, 56585, 130416, 152475, 207991, 171093, 29416, 186493, 59505, 34175, 77342, 15376, 12990, 99902, + 21762, 74649, 5423, 65516, 67329, 11829, 84139, 241464, 121432, 34713, 85742, 187730, 79924, 6579, 77428, 24207, + 11724, 110158, 32973, 112280, 38625, 29086, 83056, 3907, 81006, 88966, 16041, 71498, 102033, 825, 26490, 10662, + 28338, 69696, 48093, 65072, 13326, 134496, 36471, 61179, 3250, 65892, 28533, 314299, 82056, 101706, 7567, 64574, + 82526, 61878, 9810, 151779, 38212, 40297, 107886, 9224, 21112, 83917, 6731, 127019, 12382, 20817, 46524, 7526, + 111495, 45460, 29077, 14716, 3263, 2776, 32734, 117361, 7414, 4263, 57298, 257932, 86274, 32666, 76331, 77614, + 93490, 72983, 103093, 41179, 40844, 68943, 116063, 4284, 30224, 160402, 11643, 2596, 45212, 159780, 15217, 214380, + 24019, 8607, 90193, 25716, 48411, 93174, 97695, 187108, 71367, 40950, 51935, 149531, 24941, 24881, 32250, 21110, + 76729, 22520, 11901, 44780, 57776, 164255, 34822, 2491, 3769, 55143, 92422, 73099, 38114, 63649, 64110, 240212, + 202019, 107803, 52205, 22566, 197745, 21239, 67424, 3015, 31953, 41591, 28285, 76949, 237533, 40323, 293650, 232903, + 33270, 251467, 176985, 24164, 201580, 38564, 156136, 59809, 255648, 80672, 240807, 90052, 100798, 140429, 105726, 10493, + 44741, 91259, 58405, 10701, 32241, 77032, 19646, 28622, 98468, 71458, 27207, 84089, 106931, 6037, 21906, 10904, + 10085, 71638, 18970, 12327, 15090, 155131, 570, 57108, 170358, 184285, 20866, 9713, 33154, 17127, 1501, 66684, + 66787, 23409, 12207, 87238, 18819, 102498, 86382, 527, 69760, 37855, 28336, 40134, 25061, 472, 119634, 283057, + 234005, 72393, 63914, 13795, 82660, 81969, 21503, 42354, 6295, 133186, 18259, 34816, 131975, 111080, 119914, 6227, + 16874, 28237, 109468, 13462, 9076, 139909, 173435, 140650, 4094, 59998, 72608, 46830, 25005, 51675, 154533, 146622, + 17740, 201648, 55660, 9846, 40908, 71868, 61190, 22963, 19533, 38545, 29300, 44101, 220019, 36593, 119629, 19665, + 44330, 108853, 121109, 89385, 99792, 69972, 191515, 2180, 50040, 29432, 18069, 77343, 19619, 123487, 256669, 76631, + 13950, 296596, 1597, 129830, 55228, 1167, 160849, 18579, 24423, 59175, 11879, 3471, 31253, 98945, 59597, 119156, + 95308, 79988, 122939, 9124, 103177, 84168, 28969, 42697, 184795, 16008, 50199, 163322, 28590, 6494, 60509, 135058, + 82285, 113064, 23838, 104824, 5059, 80031, 14223, 11317, 3210, 366149, 3627, 19284, 75525, 82629, 76433, 17398, + 49894, 214741, 20201, 17960, 70007, 4469, 41765, 94300, 56178, 35669, 3059, 41367, 10580, 141243, 173468, 16012, + 36051, 146008, 6174, 145965, 139681, 7800, 110797, 7035, 21617, 33212, 25669, 13652, 98736, 51362, 38127, 761, + 3555, 31131, 121667, 108117, 106306, 16338, 122989, 66956, 164189, 15339, 82154, 24542, 37352, 59255, 110432, 16682, + 63915, 228093, 103923, 44235, 47824, 168857, 93914, 68839, 24883, 16577, 41048, 298253, 145530, 10841, 15100, 232215, + 61904, 5837, 125998, 35069, 28444, 58263, 14138, 85433, 11483, 143759, 34386, 73214, 19837, 19344, 20822, 8109, + 145446, 6859, 87391, 91712, 30420, 47415, 145201, 71828, 112972, 41730, 28283, 170664, 85939, 141658, 70333, 124812, + 11835, 2977, 84882, 9672, 191233, 7890, 112346, 19182, 2262, 159541, 16980, 12043, 20705, 67775, 24464, 209857, + 58630, 270281, 312308, 672, 1753, 46565, 82263, 33826, 148334, 55096, 120377, 20727, 1197, 4386, 5122, 5934, + 144714, 56754, 767, 46661, 6887, 16011, 3279, 258372, 11223, 169694, 25814, 42211, 107667, 126684, 25371, 63630, + 60879, 20178, 24287, 89912, 77914, 7710, 134186, 56763, 4151, 13041, 161212, 270864, 57417, 45691, 139371, 26391, + 81594, 36360, 47120, 2894, 96681, 102899, 35717, 25696, 169430, 114986, 52356, 18242, 1784, 96852, 53673, 123031, + 20444, 64937, 107271, 5906, 95138, 129637, 2569, 61992, 254041, 52369, 35639, 117271, 27038, 96678, 122654, 59573, + 596, 42424, 23209, 68851, 7117, 86087, 20253, 129099, 72808, 8253, 236489, 10640, 13759, 33512, 12847, 68886, + 3353, 51042, 54954, 88292, 126776, 35156, 39154, 26608, 21074, 3070, 132841, 36168, 55322, 31705, 21862, 73120, + 27081, 96769, 100873, 33028, 36942, 66613, 15763, 33080, 39547, 359328, 23281, 74973, 139830, 177478, 3930, 86190, + 179275, 148581, 122851, 1431, 4453, 146240, 239658, 55165, 713, 274, 94886, 73822, 8722, 26916, 78701, 67472, + 71399, 84867, 279082, 235, 19204, 9012, 17044, 1382, 25785, 9114, 9013, 22506, 22794, 59383, 85470, 19980, + 23923, 137385, 187894, 268567, 104114, 23511, 100004, 3566, 11291, 14071, 28270, 6390, 25458, 111325, 4382, 14700, + 102309, 41377, 7731, 3431, 88396, 37035, 150133, 15643, 75288, 106289, 2777, 70941, 230440, 48316, 25116, 63976, + 206396, 108620, 37151, 125702, 104551, 113811, 119436, 24384, 58447, 4370, 24435, 50488, 130857, 124278, 18387, 112999, + 37247, 26953, 4538, 30899, 94734, 101716, 114630, 179272, 31548, 49963, 38658, 24697, 176529, 190718, 62623, 4144, + 226077, 300866, 53306, 58044, 65159, 50710, 63541, 128908, 20104, 14650, 142818, 6874, 10096, 32173, 44239, 137621, + 66881, 7672, 38865, 45456, 94191, 63198, 21654, 91466, 237909, 17433, 116850, 23799, 27109, 61860, 54732, 29400, + 37404, 38958, 56953, 81848, 1520, 34230, 4135, 97322, 27421, 31838, 21240, 26409, 25220, 95856, 25488, 56829, + 113003, 1614, 126, 147771, 23423, 14373, 49546, 49817, 24884, 86146, 38695, 42648, 50585, 27147, 193187, 63419, + 6286, 46605, 45100, 136759, 231877, 33670, 291180, 89716, 150800, 7898, 65327, 43541, 11789, 18785, 15127, 92917, + 3226, 15816, 97588, 148034, 90004, 14309, 143531, 120478, 60642, 53426, 39390, 100241, 5053, 47683, 6092, 593, + 202400, 56336, 48570, 70208, 61442, 84297, 267745, 16889, 132531, 63667, 41905, 51392, 175329, 104653, 24808, 36173, + 57138, 33742, 25613, 30817, 30116, 31004, 44827, 110763, 103847, 17367, 29721, 39397, 9973, 205794, 68528, 30464, + 75367, 6167, 3182, 143724, 16452, 179801, 44257, 60822, 32360, 50545, 12909, 46081, 59119, 5222, 30976, 74231, + 21246, 4141, 25122, 44442, 10191, 152872, 60307, 6528, 164804, 64131, 52788, 203594, 23305, 109174, 33076, 95817, + 61051, 86156, 81508, 7369, 37348, 36961, 59494, 6598, 154530, 185385, 273203, 32275, 13214, 173245, 225200, 147861, + 5468, 57563, 4172, 27997, 50403, 22253, 19697, 3607, 66754, 52590, 44551, 213850, 130976, 17828, 3407, 9965, + 50559, 26417, 20257, 207504, 80515, 11064, 40718, 15057, 14436, 175751, 41158, 92093, 155492, 7541, 10270, 291817, + 84017, 120763, 131324, 93378, 5472, 128009, 141787, 144291, 43107, 11112, 64353, 20597, 41240, 29285, 7429, 182466, + 2890, 9936, 4645, 26881, 90431, 118441, 79842, 3776, 70188, 15995, 35014, 25366, 382, 86180, 8302, 14503, + 76234, 35504, 66433, 25753, 48040, 723, 30764, 17878, 50211, 19521, 103260, 1405, 281038, 12735, 16639, 6710, + 237007, 94746, 1277, 43465, 32115, 22848, 2422, 33178, 142178, 8284, 101691, 76065, 1536, 28121, 15450, 56901, + 22761, 37468, 57257, 336438, 96429, 11719, 1339, 3953, 1811, 118327, 157186, 30335, 31243, 47049, 38381, 35215, + 1679, 161267, 29632, 17925, 49143, 35370, 24607, 25287, 55209, 163958, 71839, 121011, 17402, 66842, 70491, 9817, + 235054, 64483, 2945, 109216, 61494, 17696, 18951, 2128, 7462, 147844, 39181, 147057, 77030, 240256, 162500, 11568, + 34925, 71572, 23258, 33113, 87609, 57032, 31715, 36819, 78002, 84868, 113775, 145786, 9499, 100577, 142045, 35652, + 9027, 79217, 24550, 93584, 73289, 21361, 23766, 32016, 201078, 16815, 17921, 88359, 101379, 56165, 78318, 16489, + 63544, 35992, 463196, 76115, 27666, 30809, 69632, 109853, 5469, 105799, 39876, 72304, 10642, 81042, 91087, 82633, + 30029, 3451, 39557, 9601, 49816, 43559, 44570, 24502, 132979, 33107, 74019, 68885, 95620, 43778, 22107, 80168, + 58086, 115607, 53717, 44189, 351930, 66820, 12176, 349081, 116300, 90000, 19710, 15777, 2110, 12072, 7937, 100473, + 2043, 23575, 189759, 185285, 30845, 204583, 141343, 98357, 6154, 24850, 10033, 166394, 11279, 9588, 63358, 66619, + 16727, 29173, 29298, 22369, 4122, 1113, 93975, 2373, 2277, 6248, 25424, 144362, 27281, 10791, 31674, 136149, + 4971, 5091, 109071, 28111, 3650, 74833, 33069, 99452, 39060, 31553, 103088, 6083, 61970, 35073, 42159, 39447, + 65951, 82331, 17467, 274725, 39674, 192758, 99239, 74038, 75686, 221820, 29305, 145449, 38151, 141438, 74464, 8701, + 11370, 40356, 35644, 219664, 130809, 33760, 32012, 65616, 177895, 96022, 44668, 36789, 32665, 181104, 107837, 21508, + 63725, 164836, 5861, 54679, 122267, 20346, 83568, 92187, 7857, 2055, 91980, 45529, 39618, 46036, 44095, 43635, + 118483, 55547, 30683, 9026, 44792, 15349, 9572, 31258, 157755, 62006, 13108, 41088, 178624, 42632, 108286, 57576, + 136994, 75081, 20067, 213455, 24260, 59651, 156381, 28506, 41308, 51673, 109778, 35539, 22471, 31926, 60313, 141628, + 12404, 177355, 186764, 8270, 21707, 53992, 20210, 175836, 12486, 35418, 68014, 148679, 30473, 15016, 74384, 2134, + 52781, 50454, 39034, 16954, 50246, 149675, 90227, 90639, 20247, 105483, 42840, 84149, 39065, 6265, 28880, 153724, + 909, 158044, 52031, 189995, 56825, 89732, 14963, 79537, 103158, 77948, 193052, 23904, 128603, 35173, 103922, 50144, + 31542, 77257, 10193, 261793, 1089, 61599, 83679, 56827, 41935, 34672, 1669, 32964, 32744, 192677, 84032, 84980, + 12428, 221609, 53227, 16700, 37963, 17089, 18238, 394, 84420, 5956, 18576, 76244, 33134, 135230, 52741, 9872, + 72921, 31874, 99863, 233313, 208449, 55160, 197159, 30521, 42622, 223154, 80731, 30948, 168151, 65889, 42412, 23756, + 127335, 110467, 63177, 112577, 147107, 45515, 164144, 8147, 46699, 185194, 12846, 5150, 38216, 15288, 59319, 209454, + 12591, 1396, 2748, 213994, 94342, 174981, 9164, 7542, 85814, 79347, 3079, 43844, 31423, 356287, 9839, 64046, + 24944, 181828, 21425, 105878, 2605, 75931, 24468, 28548, 87542, 72786, 33573, 9795, 67473, 52048, 18016, 14242, + 192551, 248913, 95190, 112505, 4496, 31534, 647, 69179, 56321, 161887, 101346, 161387, 91819, 19636, 11691, 343909, + 17630, 27347, 151697, 2034, 6300, 29522, 1714, 19625, 256183, 30736, 41363, 146757, 114569, 40479, 15465, 2041, + 202090, 13378, 121579, 195034, 33209, 67524, 29264, 68859, 35289, 9132, 124566, 11834, 34897, 23701, 17860, 41618, + 24967, 44272, 55538, 9772, 40520, 67880, 13672, 5691, 43470, 43146, 59144, 18400, 5344, 99162, 20283, 21126, + 10199, 286754, 157014, 57352, 34810, 134947, 22482, 13869, 14283, 3260, 39498, 50188, 10381, 85601, 130984, 2037, + 10017, 115073, 41784, 35604, 62923, 26892, 47516, 14669, 49924, 117650, 194265, 354551, 23233, 13596, 123144, 1265, + 64539, 13442, 26226, 983, 13051, 82353, 130403, 88007, 35686, 34010, 54566, 1384, 19698, 66960, 132131, 70625, + 11570, 29263, 50727, 110849, 135555, 4078, 19496, 118621, 120868, 32514, 188800, 161569, 44924, 24501, 105062, 111736, + 87065, 308308, 30954, 10824, 52318, 42959, 6951, 9830, 52335, 136608, 31619, 248564, 47586, 44794, 93623, 23889, + 140906, 33780, 30924, 50467, 245885, 88351, 90491, 46859, 109834, 48432, 37672, 25466, 78668, 2856, 51536, 53156, + 9065, 1466, 166162, 149156, 46990, 132982, 13320, 58757, 25258, 298, 22182, 22431, 72022, 12639, 39287, 100922, + 61243, 26416, 20111, 29015, 40838, 101281, 1681, 96725, 4545, 9838, 11567, 53063, 146387, 341555, 34114, 20033, + 10537, 143943, 157042, 19848, 11557, 42577, 57214, 27640, 17470, 179231, 12836, 195453, 36930, 46768, 313283, 35513, + 68104, 39738, 86287, 104695, 46711, 4413, 25433, 60207, 24264, 18023, 111517, 45375, 79401, 20865, 226464, 27841, + 81043, 41593, 204624, 25039, 18225, 20244, 170119, 22971, 120488, 189962, 74489, 159216, 24746, 58887, 156006, 65825, + 84338, 9196, 33923, 25183, 81652, 80939, 67675, 13888, 28266, 18067, 6244, 68109, 13776, 69394, 105951, 38639, + 21878, 12025, 34471, 14990, 46973, 71457, 38263, 85696, 1047, 50364, 18100, 216604, 230465, 75354, 183859, 29794, + 73357, 27757, 58872, 122255, 95777, 108826, 40410, 784, 24973, 20666, 10256, 47191, 210160, 225901, 92342, 20564, + 62210, 15357, 81223, 47348, 58404, 136370, 87219, 182975, 110633, 231019, 5557, 114090, 169651, 152695, 39659, 10697, + 6937, 15420, 20820, 60557, 41870, 16729, 133108, 27320, 9909, 108465, 192359, 16498, 26822, 325219, 33762, 172522, + 191062, 29716, 26412, 2097, 76553, 124900, 73484, 69292, 27519, 32870, 80707, 31445, 96256, 2314, 70692, 4058, + 239070, 10821, 41413, 95014, 2478, 35503, 100322, 236799, 205678, 14889, 48762, 33792, 67955, 41529, 176353, 46713, + 58532, 62997, 179242, 111905, 20601, 174290, 2473, 21736, 50120, 80978, 284366, 50101, 19148, 151810, 71043, 69116, + 78501, 13969, 1032, 82510, 195724, 299148, 161084, 64084, 110740, 1411, 917, 60413, 8249, 4449, 10658, 28635, + 109665, 28104, 30492, 131970, 27446, 20499, 71921, 6814, 30568, 42498, 32084, 9024, 57631, 161122, 111788, 30728, + 31425, 149345, 39864, 222740, 49752, 100795, 1957, 8606, 32820, 154188, 210448, 11604, 65504, 95671, 26463, 47243, + 50079, 54263, 5121, 8044, 3663, 137567, 25561, 3942, 102256, 169116, 15687, 13454, 219898, 132483, 29600, 88533, + 23849, 44708, 41198, 121112, 211315, 30822, 9110, 14874, 14645, 19626, 55733, 131599, 4359, 111315, 126666, 2148, + 91767, 12358, 87695, 65691, 9528, 60012, 39959, 13807, 12449, 60771, 165784, 80519, 49366, 58389, 57808, 299274, + 7941, 94765, 85206, 7523, 49763, 49374, 51040, 4812, 107848, 65929, 26938, 37068, 8930, 3191, 21092, 30208, + 27086, 3979, 56324, 7705, 50686, 214096, 86621, 19678, 9744, 23869, 7714, 49971, 10447, 184404, 140264, 142028, + 81935, 37, 202638, 112289, 39513, 2767, 321704, 16548, 46514, 195686, 36295, 202214, 1670, 15988, 55688, 23659, + 29229, 21347, 47074, 163169, 6172, 123566, 96740, 17816, 22312, 79026, 119292, 332453, 137280, 39511, 41020, 11253, + 97759, 113084, 67597, 99824, 9806, 100148, 19488, 91425, 14445, 10529, 86640, 119945, 22976, 1450, 21578, 8642, + 56458, 34421, 23850, 768, 73391, 11534, 64803, 221561, 34983, 50337, 3048, 86930, 93760, 26610, 60674, 110754, + 174219, 46834, 94439, 84023, 52573, 9508, 44750, 79062, 33149, 17148, 39204, 179378, 59747, 33608, 33811, 72388, + 2429, 27413, 53657, 1209, 136277, 15611, 10977, 18270, 75123, 18305, 73001, 65038, 165263, 120353, 2992, 111846, + 91040, 8711, 81068, 66699, 7446, 2463, 19348, 218110, 57632, 110134, 4755, 34160, 48633, 72482, 249, 46281, + 97140, 33462, 11352, 40714, 246081, 28361, 46130, 98911, 84766, 36082, 51109, 34148, 151684, 2936, 76243, 94584, + 79918, 12929, 460, 42550, 93268, 134209, 37100, 16896, 120346, 21124, 21414, 16833, 54059, 191099, 201522, 102272, + 54875, 84073, 3895, 2436, 77858, 10986, 154654, 5409, 32996, 56761, 49453, 346111, 103590, 58996, 21227, 37368, + 45276, 61068, 74997, 6502, 11968, 190483, 2851, 4516, 19600, 140163, 119135, 18019, 25849, 122333, 26208, 1253, + 17159, 181641, 62390, 34359, 132907, 44619, 54140, 33110, 42828, 34002, 172033, 159324, 16817, 22862, 123567, 246066, + 4913, 39475, 57181, 11836, 99462, 39965, 20158, 295279, 103303, 15191, 12523, 31976, 27395, 89881, 26366, 36188, + 5737, 4209, 27937, 51814, 74184, 36752, 26910, 75407, 20749, 114757, 80471, 12921, 21160, 166449, 33748, 61876, + 14377, 111451, 28376, 51624, 77062, 4759, 31489, 8667, 131403, 35903, 220511, 203998, 158735, 57711, 23070, 54147, + 10999, 74048, 6529, 32621, 27799, 92313, 30581, 51320, 77785, 63583, 107525, 10443, 9320, 51511, 19427, 121556, + 34366, 33241, 41042, 128493, 51593, 169332, 49002, 178217, 61070, 171, 83380, 12254, 33746, 11674, 26248, 60875, + 47048, 3811, 107521, 60945, 29268, 895, 49016, 21834, 36675, 58110, 5989, 35500, 30262, 101556, 212477, 10165, + 53297, 39091, 41022, 9791, 9832, 14567, 19009, 19068, 82000, 3875, 37180, 186532, 20188, 14835, 74440, 7293, + 122292, 25795, 49957, 53535, 39917, 123027, 87789, 17677, 7331, 89007, 2300, 35386, 18160, 6491, 51684, 21618, + 68301, 13263, 285552, 298645, 185935, 298900, 126950, 20478, 134830, 182343, 19829, 12011, 15031, 116530, 76262, 15413, + 4935, 40625, 164987, 49386, 10004, 44236, 39740, 43773, 165845, 43832, 56688, 2815, 185534, 81592, 56245, 1437, + 46923, 129294, 23698, 129303, 30109, 58443, 14904, 122152, 44134, 27588, 5195, 37064, 122631, 43995, 372314, 387837, + 18874, 47379, 277, 22234, 22903, 9497, 40286, 16763, 112790, 89200, 17537, 20682, 26561, 7025, 122064, 142767, + 18549, 18358, 38049, 62248, 348902, 41526, 76877, 7321, 82871, 209789, 117544, 83895, 140345, 134154, 56621, 61740, + 255565, 75916, 191295, 230290, 135390, 60673, 50087, 3175, 63556, 59497, 24739, 20520, 103747, 5481, 54327, 78229, + 145055, 27141, 91354, 101583, 179600, 37968, 51679, 147604, 145662, 21758, 1468, 32673, 296111, 37226, 10401, 244665, + 61661, 62743, 33793, 21290, 211987, 31229, 36498, 109014, 23952, 5664, 68430, 117386, 52342, 279268, 1383, 16178, + 126343, 21917, 67779, 80496, 48450, 28456, 37591, 11298, 32919, 75914, 48144, 42628, 44277, 135351, 43365, 68058, + 82185, 31919, 36044, 33488, 9591, 46231, 87880, 19683, 62139, 164744, 13946, 67759, 205363, 64547, 24950, 14744, + 376969, 125444, 36207, 34787, 394874, 9391, 29970, 31633, 108461, 29004, 95508, 16726, 18040, 1474, 161241, 87333, + 120885, 78180, 1312, 156395, 14798, 49849, 30378, 61417, 39863, 215063, 70563, 17245, 16571, 4898, 117368, 45833, + 16794, 119877, 56493, 56667, 58271, 114337, 11790, 85404, 81025, 1115, 70207, 196483, 55206, 75037, 286099, 52410, + 14640, 28529, 108282, 19807, 118656, 48399, 13926, 14142, 6361, 12773, 19250, 37526, 44092, 14182, 3300, 24788, + 85970, 100512, 5089, 93502, 6262, 1470, 30526, 6736, 153863, 47611, 5419, 5204, 108244, 23917, 15546, 201845, + 180200, 9222, 61948, 51408, 72264, 60586, 13704, 87398, 79947, 75005, 105096, 35548, 38044, 163143, 46082, 43224, + 10050, 223780, 42559, 63853, 5735, 24066, 14942, 134623, 61221, 29913, 32948, 152876, 80712, 15291, 19415, 47687, + 5471, 16468, 19049, 875, 115689, 4926, 141440, 86953, 11391, 96224, 41116, 29097, 11661, 9977, 16554, 59410, + 184257, 4916, 59752, 123609, 20010, 44968, 127762, 3094, 60116, 31503, 22578, 77738, 30320, 46196, 21138, 9271, + 19327, 143121, 101458, 26727, 134598, 33180, 123460, 124908, 45455, 1725, 24171, 1975, 27542, 3320, 81552, 83876, + 18004, 21115, 5583, 180685, 125092, 158497, 38663, 698, 452272, 14139, 42821, 65816, 1549, 15658, 88083, 33362, + 91523, 14865, 6630, 176968, 46567, 36614, 16181, 20495, 180063, 61084, 102959, 47886, 156026, 28065, 8610, 114642, + 2608, 73306, 8419, 8283, 11174, 348806, 348428, 8950, 58848, 3040, 12266, 87926, 37788, 7990, 32289, 57688, + 65907, 28786, 408131, 92280, 80194, 123266, 156847, 70303, 30490, 3057, 70321, 174337, 5786, 5649, 71496, 65938, + 40775, 32358, 26015, 20333, 119519, 11504, 86693, 47220, 106241, 179717, 45913, 80350, 11323, 10871, 42117, 44122, + 156297, 8264, 34534, 74130, 8425, 20168, 19195, 95738, 61495, 86901, 91525, 56486, 2617, 21807, 76315, 32644, + 29675, 132700, 187527, 94065, 2425, 48583, 146946, 19438, 59886, 21696, 112525, 185026, 112582, 107474, 21779, 520, + 49142, 27517, 27611, 26275, 59618, 68585, 34382, 269197, 4863, 78549, 51824, 198549, 50597, 91695, 79132, 75817, + 86710, 86822, 13732, 13511, 50650, 3411, 18621, 279970, 168632, 42104, 4038, 32572, 27693, 8881, 65349, 54005, + 85641, 7547, 8478, 41579, 83643, 30439, 9416, 5869, 18993, 49065, 16745, 12818, 25768, 25667, 58681, 44991, + 84284, 204061, 61201, 50637, 28090, 5082, 34635, 107900, 93592, 201432, 170018, 36616, 36627, 200119, 6933, 53709, + 312804, 36724, 17312, 3158, 43381, 5167, 26919, 23400, 9887, 26673, 28631, 66018, 9402, 230847, 24255, 102572, + 100931, 154504, 170013, 201115, 97165, 34404, 357, 179763, 3311, 108, 53023, 171976, 173330, 95887, 211961, 23099, + 66805, 113640, 18352, 46361, 28935, 107138, 46668, 51711, 4963, 45839, 54816, 7932, 184460, 1611, 103983, 141455, + 3201, 270150, 175156, 43537, 78102, 12443, 37971, 414715, 19126, 6340, 130684, 52220, 21607, 284399, 102147, 95375, + 37496, 136001, 219663, 16689, 24938, 7390, 85416, 109375, 22615, 13046, 57826, 15407, 16153, 137199, 62614, 211231, + 32862, 32298, 16119, 175170, 134792, 46807, 3951, 27849, 153318, 105648, 120022, 22136, 61120, 56637, 91935, 136213, + 6067, 4114, 134337, 42278, 2812, 85502, 6569, 159359, 12826, 125922, 25619, 62295, 12792, 79563, 100822, 11222, + 23825, 49867, 29919, 872, 37559, 4587, 112918, 136034, 64662, 126530, 131167, 14498, 202250, 56243, 23092, 121186, + 102694, 22868, 89387, 64332, 155488, 39744, 156087, 176575, 85881, 39781, 47236, 12249, 149193, 26406, 1951, 155992, + 46233, 184401, 41366, 8036, 65383, 139356, 189733, 213982, 15521, 49577, 159577, 2521, 106982, 58186, 55467, 51702, + 11358, 8682, 17785, 29934, 176786, 109645, 122828, 41281, 25752, 29762, 94798, 25186, 39717, 17547, 52095, 8022, + 34208, 129165, 49581, 119910, 24510, 12967, 247959, 15952, 32464, 81364, 137598, 14637, 77742, 123065, 16222, 61255, + 39371, 43724, 57019, 24082, 72028, 104622, 27929, 89643, 138229, 10055, 122728, 41443, 60688, 136821, 3274, 7812, + 71386, 67606, 3295, 100611, 102834, 3428, 102932, 35148, 132477, 142278, 25402, 33288, 2208, 132017, 146591, 21568, + 11548, 73095, 756, 36537, 63670, 246597, 20653, 141984, 271279, 12711, 23553, 27463, 28351, 23214, 77413, 75614, + 30338, 23444, 235758, 28565, 38620, 46299, 28150, 5788, 32491, 43962, 168549, 29503, 99845, 200267, 70204, 12986, + 143885, 941, 50969, 55284, 152266, 187576, 3532, 57733, 13252, 143761, 54421, 60086, 2825, 16104, 18211, 69263, + 178663, 103869, 8702, 98648, 108097, 15531, 162361, 61008, 1775, 84427, 119944, 23016, 78201, 82106, 24005, 236154, + 14897, 34582, 63819, 36233, 113573, 102759, 41120, 42814, 163346, 29508, 164161, 54185, 62292, 9036, 39296, 6640, + 171129, 26814, 9984, 294489, 22183, 40191, 69406, 90599, 96598, 18066, 85600, 87824, 38733, 247243, 7387, 161131, + 64971, 38204, 92974, 85988, 166776, 29624, 69098, 72876, 117445, 68760, 22363, 52379, 9968, 148571, 26622, 186006, + 146393, 31071, 60120, 13612, 44677, 19448, 68338, 7874, 74867, 64490, 29421, 124401, 20908, 16585, 60838, 7384, + 97328, 50410, 20319, 19156, 12761, 5816, 351477, 2786, 25656, 165861, 173732, 7123, 26785, 149182, 20793, 27143, + 9148, 25949, 18006, 7795, 112344, 192537, 14513, 5682, 26115, 40202, 70306, 48084, 99176, 7153, 108473, 44332, + 110121, 9428, 8180, 27860, 22437, 60581, 205918, 99858, 49547, 116936, 35145, 8901, 6180, 5175, 18803, 16208, + 79320, 28649, 38726, 54774, 5835, 30655, 86440, 25966, 31392, 70939, 20843, 111672, 43328, 94380, 88995, 15321, + 33377, 2984, 16233, 43332, 75870, 19044, 156499, 127031, 119860, 122514, 65060, 17517, 69497, 64192, 44374, 6455, + 80273, 40630, 92629, 46425, 7325, 37171, 79359, 140226, 155219, 32133, 40753, 22059, 43167, 16881, 38630, 50086, + 111173, 44484, 5941, 101815, 28347, 373200, 356553, 74832, 20222, 177340, 108041, 180076, 3763, 27194, 171523, 10928, + 71752, 148205, 39888, 74776, 55041, 6358, 148477, 294704, 47252, 52974, 89175, 4682, 14618, 39392, 58585, 50619, + 28088, 100276, 36110, 31596, 15012, 93646, 4953, 6629, 97805, 46485, 18765, 9249, 194698, 52307, 85352, 33772, + 54636, 209534, 25378, 210, 2036, 151416, 255499, 40976, 41349, 134520, 150432, 11157, 6173, 62536, 56094, 34646, + 96604, 85745, 27243, 6622, 61530, 18737, 8699, 115599, 51859, 41290, 143452, 40201, 43782, 14061, 74656, 29726, + 13361, 136026, 23149, 4742, 24334, 18683, 24925, 85168, 22668, 224320, 16450, 76194, 24792, 35366, 2119, 56976, + 7070, 16021, 24155, 51463, 23441, 3622, 46105, 126702, 16789, 276646, 86393, 21589, 3209, 111511, 12134, 13169, + 36211, 227939, 16170, 16148, 208475, 99712, 10351, 419718, 26242, 79670, 44112, 4569, 32880, 339579, 30648, 97335, + 122181, 70716, 13387, 85134, 182407, 26932, 273051, 12495, 21444, 28811, 92202, 62532, 31060, 48235, 131669, 47086, + 88459, 54224, 9712, 80410, 29929, 35255, 5466, 174314, 77907, 10011, 1541, 31577, 12716, 153148, 263135, 100373, + 10934, 79991, 36589, 35238, 97005, 23398, 171539, 8580, 20599, 128843, 2723, 53858, 31690, 44768, 11242, 19051, + 8403, 12581, 30429, 200097, 58445, 64447, 17535, 77337, 30303, 7870, 87634, 120825, 22700, 347, 32033, 8588, + 10336, 47933, 95540, 18314, 86731, 43480, 122415, 48270, 103115, 61655, 3386, 11664, 337709, 27487, 202769, 59646, + 72556, 173070, 8469, 81115, 46788, 18684, 133404, 96594, 112566, 86812, 64663, 85306, 47684, 65109, 21999, 787, + 67089, 42642, 9467, 104964, 53548, 77932, 89601, 50531, 36874, 103369, 55381, 55386, 56487, 110574, 42321, 104493, + 41387, 87478, 10390, 37145, 125985, 271334, 8940, 28636, 26893, 223188, 56969, 202288, 40071, 34929, 116962, 28213, + 106683, 28231, 42013, 24525, 73712, 66578, 95581, 82535, 18787, 2469, 64097, 192370, 40105, 33082, 137529, 8248, + 72992, 28876, 9399, 7582, 67246, 4830, 5079, 188992, 137276, 8793, 145644, 146116, 50802, 69878, 200577, 15640, + 36790, 214109, 63824, 55941, 70328, 47477, 2167, 52953, 138827, 24284, 37429, 16033, 22466, 117793, 20960, 43422, + 39263, 48912, 81141, 192574, 183295, 1837, 14713, 28579, 29858, 381, 62358, 27575, 50975, 47277, 158226, 20747, + 9322, 21799, 8759, 19551, 57397, 25924, 60257, 23908, 10654, 24152, 18912, 12247, 24364, 134961, 117953, 43806, + 30383, 28739, 36894, 57851, 55799, 12140, 208514, 12059, 41600, 16395, 47450, 48286, 23584, 118890, 25589, 8681, + 127295, 234074, 47071, 125810, 296610, 11331, 18254, 15170, 129078, 16080, 226323, 40895, 143558, 94050, 23705, 131198, + 244131, 60925, 25356, 21260, 86397, 300199, 46792, 88237, 36049, 206902, 15590, 21351, 1085, 93619, 11791, 83320, + 80677, 34168, 26403, 64840, 3820, 15926, 1847, 16734, 108139, 3510, 11982, 209610, 5476, 22002, 108428, 55260, + 34767, 29252, 98069, 88530, 24683, 25427, 54524, 21159, 7758, 58183, 73508, 29449, 13060, 45920, 148846, 105330, + 7239, 2883, 81088, 12697, 131671, 7549, 43047, 4805, 250593, 12157, 34279, 12914, 59556, 76223, 25084, 20506, + 103392, 43609, 100817, 171460, 29810, 37880, 81256, 174784, 4188, 149828, 64134, 59705, 252323, 25997, 44940, 97369, + 39404, 5069, 90268, 85619, 116877, 19634, 56035, 36905, 7651, 33380, 130707, 133829, 43600, 25142, 75703, 40295, + 40338, 60316, 24687, 44342, 13554, 8678, 9961, 1732, 157253, 93469, 29687, 49688, 39196, 21767, 41224, 21529, + 25978, 36956, 66355, 33481, 144387, 50146, 129773, 13311, 61211, 62169, 77703, 101581, 234, 27986, 9318, 13341, + 50104, 58984, 15733, 9924, 6129, 59308, 32036, 85687, 10449, 47782, 8891, 25720, 93777, 35277, 8953, 13811, + 9240, 22192, 6432, 17202, 356378, 52015, 66393, 12508, 274148, 255059, 67115, 30737, 4439, 11480, 231776, 166051, + 72970, 82790, 96236, 26126, 3724, 86291, 14281, 11950, 147770, 105431, 20726, 77543, 78680, 17490, 13496, 21992, + 62570, 4476, 98692, 112842, 115877, 74277, 124883, 83834, 40027, 21132, 19464, 47232, 40547, 89457, 28687, 14573, + 36817, 103723, 300665, 15319, 224392, 26400, 40495, 22877, 64609, 18201, 23154, 72374, 34795, 27583, 78778, 23667, + 165027, 32508, 73622, 56731, 67440, 2495, 103298, 105353, 2477, 41716, 11030, 8686, 37206, 79590, 125885, 13625, + 23431, 4395, 220465, 150736, 50754, 5523, 27215, 232561, 164797, 91433, 63055, 7083, 46018, 251531, 40722, 70383, + 94995, 7924, 77757, 28613, 170982, 867, 21717, 13321, 27051, 21566, 114874, 18681, 7957, 7438, 19655, 84979, + 22767, 101166, 277403, 47583, 3674, 112331, 65307, 4882, 27900, 40861, 34152, 26594, 56419, 29707, 25132, 78891, + 18930, 58166, 23382, 32025, 60701, 65952, 21108, 607, 41302, 44913, 98469, 73043, 2692, 100592, 76874, 140991, + 84749, 15560, 29248, 219368, 339721, 48121, 96609, 79943, 61996, 45630, 28536, 16244, 111094, 26428, 57889, 25111, + 80221, 69552, 27326, 124506, 50129, 75574, 64173, 83505, 1045, 142814, 170324, 19671, 8153, 208336, 12576, 12623, + 62945, 184743, 32415, 73714, 19202, 2698, 136438, 116392, 8250, 15337, 70178, 157991, 37208, 8242, 26035, 58589, + 37418, 6014, 58480, 1274, 32560, 127652, 47847, 148702, 79477, 92504, 29034, 87904, 41106, 61295, 72948, 79082, + 88569, 164147, 34772, 53574, 33963, 129292, 2501, 7461, 36693, 68888, 18880, 65806, 5892, 424331, 17516, 24390, + 30570, 2113, 9490, 25280, 1581, 110856, 24330, 24537, 66471, 15890, 104155, 126634, 49647, 82695, 115436, 114480, + 11922, 54150, 34729, 37512, 160717, 69615, 2014, 40558, 29442, 49537, 9489, 90588, 5643, 197221, 9955, 87575, + 114865, 94728, 2057, 19542, 82962, 71746, 2865, 8021, 95982, 61016, 32535, 150782, 132098, 15408, 30334, 114765, + 22633, 27477, 74001, 34329, 22838, 9812, 99985, 6414, 94726, 41615, 168290, 212638, 54556, 24532, 127124, 32488, + 28566, 631, 37608, 9436, 205039, 166709, 41813, 62681, 162340, 51007, 104187, 135517, 33216, 370029, 46677, 42823, + 16849, 22305, 32170, 4155, 35847, 216420, 4908, 9704, 221339, 1461, 36764, 69322, 94851, 163847, 168141, 1238, + 26533, 64284, 196577, 46554, 71469, 97500, 18030, 75035, 1805, 59036, 59485, 22807, 3804, 35946, 47500, 82026, + 12935, 1196, 59186, 36123, 45483, 48905, 122736, 84743, 71020, 21859, 105891, 19409, 36310, 14933, 324632, 9477, + 65381, 15301, 17544, 116221, 192960, 28345, 33914, 9479, 34240, 10843, 107872, 10760, 35165, 170015, 15849, 66429, + 59773, 117561, 48895, 53810, 1248, 297457, 78131, 22215, 46954, 15473, 66440, 19176, 155332, 67372, 63874, 27562, + 96864, 44731, 33316, 68027, 4246, 61528, 60417, 153158, 388800, 242889, 139912, 30680, 16129, 184234, 14284, 220334, + 57133, 2684, 29537, 163409, 74592, 22341, 14608, 7820, 44807, 52082, 34669, 735, 442014, 5199, 156652, 115585, + 38203, 46928, 26751, 41163, 42574, 23000, 40485, 193202, 80818, 24685, 30063, 46336, 91592, 38350, 16019, 204886, + 26377, 35729, 24114, 14839, 36424, 82137, 58468, 208317, 65760, 107517, 111976, 169534, 977, 88148, 88506, 104279, + 77387, 49741, 55001, 102462, 22628, 16787, 70803, 44152, 147610, 49926, 3305, 34988, 28018, 39850, 1762, 172940, + 30561, 35822, 23734, 154547, 98454, 6287, 35558, 37540, 6969, 16948, 182134, 68275, 119628, 26385, 69050, 27987, + 63648, 91787, 15241, 69688, 18170, 23405, 63549, 172789, 36854, 149944, 199582, 50387, 26601, 61662, 165764, 15740, + 64018, 188861, 204663, 14506, 22027, 34003, 37949, 76827, 37279, 69128, 41728, 50954, 51395, 91070, 77327, 418272, + 152934, 102026, 34299, 2147, 21153, 7074, 4236, 29765, 9430, 213559, 43803, 10595, 58760, 69911, 261653, 87745, + 194742, 224, 344942, 28518, 5330, 188455, 29445, 39380, 55115, 37739, 135330, 40527, 34158, 67980, 2019, 20921, + 28917, 61353, 29277, 143760, 174111, 25315, 233758, 5380, 13171, 38385, 49725, 63589, 122326, 12646, 695, 115120, + 1526, 21427, 111543, 128260, 43896, 37771, 92334, 46393, 66094, 257494, 18659, 47526, 25325, 7287, 24994, 1200, + 4234, 136264, 35864, 150235, 148354, 9687, 28790, 43378, 11450, 185999, 16029, 3010, 275, 54840, 18620, 5465, + 18999, 14941, 133430, 7102, 112191, 67383, 37978, 43984, 44365, 118346, 5294, 4294, 22723, 21609, 95097, 56653, + 68409, 57456, 462, 22817, 8733, 4115, 95791, 28344, 57746, 79153, 1397, 207599, 96565, 211156, 90894, 88357, + 75007, 67110, 7600, 143795, 14196, 17993, 7370, 47401, 108844, 40816, 2129, 578, 29475, 1352, 164155, 115861, + 88599, 265011, 72917, 44900, 177563, 66133, 61076, 81186, 100792, 66415, 27198, 24480, 106156, 32719, 8226, 19302, + 86323, 65704, 91571, 74710, 93726, 40774, 166720, 71206, 14248, 159482, 104866, 13711, 135341, 12882, 22933, 26886, + 194131, 14226, 57391, 10865, 40126, 114370, 59004, 62802, 47099, 37870, 61471, 61712, 14779, 23150, 60956, 17913, + 8272, 31742, 43627, 355564, 39597, 43789, 24868, 17215, 95983, 10850, 171578, 95826, 171398, 43329, 52382, 39205, + 65882, 22816, 30560, 5482, 28052, 257734, 116033, 49489, 10393, 42391, 140158, 74221, 47213, 10652, 133629, 42182, + 40689, 31081, 114221, 24833, 22120, 71238, 95884, 1589, 72212, 29981, 49555, 59882, 106829, 22147, 12985, 19337, + 34964, 98868, 7993, 32641, 109146, 79730, 82886, 12040, 753, 26623, 20550, 6160, 648, 37626, 156500, 74280, + 21660, 112069, 102650, 29846, 30047, 37920, 38707, 22416, 17527, 43165, 20567, 16777, 181025, 163230, 77041, 93275, + 5619, 104536, 14442, 35376, 145357, 711, 91293, 267964, 4085, 61569, 2548, 27538, 216883, 57568, 2256, 60727, + 9359, 183328, 33919, 77609, 186951, 2807, 102609, 77085, 24779, 43042, 7697, 75938, 53931, 15103, 12294, 16402, + 24545, 7447, 558, 128098, 36197, 23062, 46707, 17077, 223296, 66504, 27982, 13141, 62628, 55296, 54207, 95832, + 168101, 45485, 3146, 80848, 4243, 2868, 16953, 33358, 107313, 27188, 53837, 35648, 30321, 5098, 8014, 6864, + 26642, 216648, 15542, 69518, 13049, 164033, 260760, 92714, 51059, 3646, 38963, 53961, 31027, 65718, 8441, 195966, + 107912, 99428, 28516, 18239, 807, 22455, 35712, 38565, 73550, 133659, 118825, 32367, 26551, 30301, 28367, 11762, + 84369, 86004, 37814, 44184, 122422, 57026, 4072, 40865, 165872, 25809, 54060, 35341, 49754, 17581, 103700, 118548, + 74213, 178685, 2053, 49373, 234264, 17223, 35164, 99392, 33151, 130808, 2338, 24598, 52014, 213186, 156444, 16006, + 33100, 87907, 2116, 183683, 87183, 121897, 25550, 4995, 22365, 1221, 128172, 24344, 52500, 5554, 89782, 2016, + 40013, 70221, 66896, 49588, 23302, 76353, 61452, 21765, 5652, 195585, 31863, 21028, 72723, 127694, 101106, 10744, + 21404, 46840, 4864, 158406, 26107, 219205, 22949, 68216, 48434, 46124, 8871, 21467, 587, 110874, 46178, 110709, + 94049, 110687, 194252, 73380, 168493, 40871, 84591, 4279, 96418, 78366, 113568, 80733, 32871, 103415, 9257, 39835, + 70860, 74701, 43788, 318366, 31709, 7528, 5382, 22765, 25128, 2525, 52257, 20911, 443, 56027, 55517, 12403, + 71597, 15617, 196829, 263547, 166253, 113889, 151910, 27229, 15670, 56695, 27118, 39691, 70994, 5859, 9227, 98270, + 26341, 176547, 121365, 69700, 133675, 164631, 110015, 41477, 28280, 6013, 20657, 210461, 75491, 126050, 3380, 248649, + 54756, 7374, 93034, 88191, 47955, 22642, 141231, 44151, 56028, 10051, 42742, 54389, 26182, 179888, 17595, 29573, + 11952, 45860, 19676, 264, 113272, 40733, 10627, 68835, 472197, 159006, 64051, 34066, 64640, 150352, 25206, 68762, + 110753, 23633, 22112, 20042, 17919, 163270, 55949, 19557, 337, 124601, 60155, 130764, 50642, 63188, 8357, 143714, + 22576, 35676, 36362, 24812, 142, 40272, 89438, 24448, 87371, 105259, 16947, 36015, 53391, 136, 23199, 36073, + 93210, 18431, 41934, 10593, 126694, 48847, 17533, 34029, 15285, 65070, 33592, 113537, 19642, 73194, 284546, 137425, + 85667, 129350, 26968, 6511, 14148, 31670, 30205, 11076, 1506, 220046, 7034, 329567, 42092, 19955, 30867, 21935, + 52962, 31820, 69932, 14236, 33243, 12574, 10827, 39690, 11970, 174135, 28153, 146891, 20734, 126712, 21323, 40265, + 135843, 199355, 58115, 49038, 57044, 54396, 16409, 31133, 58880, 32408, 26984, 27298, 13002, 35976, 171154, 61387, + 219134, 82951, 17625, 78088, 22876, 4261, 5497, 9679, 64754, 2492, 263964, 159492, 232519, 20547, 74430, 377501, + 4257, 30374, 461, 33465, 43120, 162710, 65294, 50518, 321573, 5955, 131205, 93895, 24799, 9658, 5858, 13871, + 64526, 19652, 2901, 70075, 124728, 146097, 98082, 100551, 52579, 153823, 113631, 22528, 81472, 25792, 60475, 32767, + 70831, 74749, 72861, 41755, 276848, 7157, 25389, 110028, 17403, 27510, 251623, 58039, 74359, 18091, 50708, 89467, + 23021, 20850, 106672, 3962, 182101, 38811, 122104, 32394, 74597, 3381, 18651, 101115, 23744, 97817, 110293, 37253, + 148267, 36198, 3519, 55601, 12055, 42944, 34271, 133263, 7976, 95413, 91635, 5598, 5349, 58679, 57950, 154564, + 11772, 149366, 22890, 8626, 67540, 3799, 18687, 5012, 167347, 116945, 44732, 33287, 65318, 71035, 71417, 65941, + 18720, 30185, 48199, 202749, 127832, 1587, 3246, 105625, 108238, 117159, 28014, 104782, 22828, 55738, 81738, 11163, + 90233, 17250, 4750, 188510, 9987, 20638, 16031, 14461, 259080, 49305, 190390, 14691, 118185, 22004, 72005, 37574, + 73209, 86700, 86886, 144339, 79270, 53654, 14505, 129204, 13775, 25931, 7131, 15330, 90100, 52622, 10877, 9834, + 137742, 2258, 125407, 10878, 90799, 219424, 85936, 9073, 70569, 42871, 11487, 17205, 15699, 176689, 145652, 13840, + 19961, 3187, 108771, 19907, 9087, 61130, 38770, 130505, 67475, 29769, 18202, 59283, 57872, 91097, 48087, 6535, + 39731, 194914, 82462, 72799, 8810, 20369, 69590, 161138, 134897, 1655, 228453, 124246, 131868, 32125, 24266, 32982, + 146849, 36964, 41316, 2497, 19898, 151088, 186502, 25460, 3334, 5126, 116461, 28025, 2281, 35537, 44808, 12910, + 167061, 87358, 186367, 9640, 91073, 100691, 122545, 45892, 60356, 46145, 149118, 90693, 467742, 151743, 138009, 53160, + 74712, 7571, 311165, 28817, 188, 2377, 69313, 29598, 53179, 50217, 129275, 459470, 137679, 65308, 18477, 51126, + 92769, 135453, 6508, 23061, 29241, 3159, 63876, 36946, 9537, 41270, 4306, 50909, 132595, 108954, 42730, 19664, + 80119, 116744, 47895, 141726, 1041, 20154, 230804, 208216, 88962, 27133, 7095, 34643, 5976, 24608, 31719, 12859, + 40171, 32072, 88409, 11313, 44911, 91010, 24472, 25842, 102859, 76732, 14848, 25181, 139059, 102051, 109383, 112500, + 104558, 41471, 92391, 7314, 98987, 4288, 8166, 373942, 47761, 86881, 41222, 3712, 19272, 32290, 21185, 46273, + 71472, 64765, 57109, 135608, 113864, 48751, 60216, 15724, 175377, 27567, 112796, 18273, 73338, 51592, 245687, 100900, + 10857, 55010, 31720, 50806, 23402, 71732, 1067, 129268, 23758, 39673, 37515, 71954, 1591, 31733, 27822, 35424, + 139864, 46227, 8017, 50399, 5644, 74021, 28076, 85584, 4076, 127136, 37464, 102260, 118760, 24879, 11109, 28407, + 16427, 136669, 88300, 8782, 134198, 49502, 5217, 243528, 18853, 31871, 85719, 9313, 20291, 111347, 8859, 232793, + 100849, 152830, 105869, 31365, 37423, 46174, 5288, 386390, 22038, 11906, 5770, 10139, 36677, 146746, 11499, 81033, + 19071, 79322, 71912, 26542, 195521, 10379, 25001, 31339, 57445, 2750, 6263, 58554, 11069, 15459, 87289, 89651, + 31869, 529, 435067, 57142, 55718, 10724, 15530, 50660, 66882, 102319, 10462, 143371, 148490, 217411, 10252, 44760, + 44, 24999, 38673, 7037, 41296, 7080, 103758, 32647, 75242, 101191, 76455, 28603, 49704, 54470, 60157, 16716, + 166810, 63760, 52521, 49375, 9906, 135040, 4540, 38220, 20943, 103257, 50219, 154213, 122258, 73230, 121482, 29653, + 49112, 43518, 71058, 63793, 105667, 44288, 91681, 25034, 15969, 72202, 23056, 81246, 10344, 93662, 911, 44819, + 6408, 52562, 5953, 16497, 187694, 1131, 63578, 47977, 21399, 61396, 149533, 57971, 72742, 5611, 13207, 221621, + 58970, 79728, 27059, 171306, 14867, 7768, 72956, 52885, 14376, 30921, 9280, 8158, 81889, 33376, 39155, 71120, + 41856, 131166, 105010, 30693, 23225, 58453, 31791, 3690, 15042, 11816, 43900, 117272, 56993, 64225, 105202, 54205, + 16074, 71491, 22908, 25691, 131389, 59527, 11646, 42043, 74276, 34270, 28537, 26163, 72407, 41791, 219412, 115141, + 53875, 38258, 76312, 84827, 383108, 40765, 11202, 132777, 53597, 167648, 158387, 30192, 37363, 40773, 1146, 72250, + 68993, 32806, 59487, 128806, 44854, 6158, 96774, 39870, 122548, 5997, 50579, 45575, 430927, 3282, 94975, 100509, + 198279, 200501, 32872, 18750, 38430, 107646, 104178, 8936, 80409, 100084, 192864, 160858, 12245, 67627, 83578, 90755, + 2981, 54132, 8983, 55539, 628, 33889, 292355, 74386, 2818, 172034, 214107, 19271, 17760, 834, 34352, 169686, + 37437, 145284, 73189, 54617, 238229, 4948, 71657, 94135, 7968, 30905, 23213, 304209, 46892, 61572, 104947, 117623, + 2200, 107472, 204184, 88505, 3730, 10311, 20501, 150712, 34190, 70936, 38672, 37360, 65983, 2571, 19225, 134448, + 37959, 151534, 4637, 20807, 112291, 80673, 5980, 35705, 87850, 61874, 47965, 31875, 70827, 182878, 35126, 164376, + 6522, 43040, 97902, 71998, 20750, 38267, 227637, 76251, 73913, 116387, 6656, 46715, 111621, 13498, 6684, 248541, + 41652, 32337, 11354, 14685, 19587, 16826, 46225, 7338, 2780, 13317, 78374, 44055, 58668, 1088, 55437, 86278, + 25916, 40461, 78119, 116159, 85259, 146157, 14297, 2255, 18200, 39030, 6734, 42798, 168962, 119973, 38038, 22310, + 95781, 125342, 77701, 24562, 42445, 51140, 84077, 79797, 102050, 55904, 96004, 87841, 7776, 193841, 23065, 73430, + 57662, 41418, 52176, 99003, 103313, 45282, 85005, 26027, 47742, 65487, 4547, 20906, 96358, 53110, 36530, 18902, + 41964, 66288, 159127, 54902, 66174, 22317, 184026, 50221, 100396, 167624, 175279, 22979, 29069, 146485, 480, 42086, + 204735, 209840, 27750, 486, 19679, 2874, 101071, 35317, 27978, 40196, 245641, 90054, 7479, 4028, 52060, 61770, + 40264, 884, 39262, 47984, 22534, 2991, 111271, 48232, 61183, 27973, 80376, 56009, 36081, 165143, 131566, 47390, + 107436, 17924, 139245, 18948, 58223, 25723, 60245, 119466, 14680, 77779, 96719, 17119, 23002, 182396, 70393, 3161, + 101311, 78419, 54640, 23621, 24716, 19555, 111633, 409, 124108, 27386, 2245, 258216, 12908, 23526, 10212, 69858, + 5646, 124783, 685, 22255, 31750, 16915, 43000, 28583, 40380, 54912, 117699, 74670, 14215, 28794, 179610, 6571, + 232799, 21298, 4374, 40448, 102772, 82421, 69208, 18356, 14122, 42989, 68207, 134104, 96775, 342439, 22630, 77974, + 61398, 44393, 92954, 93632, 50917, 174764, 71894, 24307, 12096, 8836, 209934, 117370, 149880, 80277, 3249, 24377, + 67833, 1203, 220478, 45163, 598749, 19930, 49756, 19462, 124194, 104517, 87257, 36828, 155871, 228667, 44446, 48887, + 49216, 262088, 45585, 33699, 790, 142940, 1214, 51939, 14677, 49283, 82736, 30811, 65319, 13215, 29155, 8388, + 56917, 46741, 80552, 21318, 7440, 61972, 13877, 113912, 145744, 1135, 64582, 45451, 95701, 94255, 93908, 15993, + 12206, 4906, 63468, 345616, 49405, 22021, 38717, 72500, 129269, 41952, 6995, 118167, 76199, 30271, 54602, 17741, + 45732, 43176, 108447, 54078, 9767, 2976, 31141, 98669, 11058, 3418, 36920, 28316, 9047, 12901, 81784, 44333, + 210885, 31029, 19486, 48518, 11051, 76288, 43203, 81787, 7392, 48679, 35182, 66212, 26307, 52539, 2090, 49456, + 2130, 146238, 10565, 420461, 8132, 25081, 22634, 263972, 147526, 30634, 102378, 79556, 20802, 111069, 161896, 208679, + 232698, 76981, 47548, 35236, 115660, 54019, 196404, 365534, 50060, 190162, 23999, 64445, 59789, 24002, 10716, 4605, + 57344, 32663, 9089, 10394, 107623, 22597, 164592, 49576, 80343, 5393, 4211, 9135, 112676, 3507, 56842, 26209, + 23291, 13451, 13815, 72724, 9866, 28546, 4585, 11238, 160971, 144491, 17045, 4629, 34032, 57499, 26471, 40064, + 118291, 51528, 233736, 42900, 15719, 86514, 25147, 31587, 59730, 27331, 34546, 93487, 164911, 55981, 1017, 52398, + 28975, 111773, 14576, 7163, 2659, 23748, 94127, 7591, 58046, 118031, 293274, 2302, 78480, 108856, 26363, 33313, + 21854, 78915, 9334, 18530, 66209, 195667, 8136, 17789, 53863, 87424, 64346, 25683, 109085, 21080, 41596, 10885, + 116045, 55411, 15616, 32700, 29021, 102829, 16981, 4307, 46481, 113835, 166601, 106369, 107552, 19257, 124248, 5139, + 22130, 56921, 41150, 3696, 18764, 5694, 250839, 81058, 70254, 74576, 7048, 33695, 31272, 9587, 51251, 12329, + 11300, 219240, 77500, 229429, 52460, 62159, 41229, 11359, 43933, 8689, 31439, 145750, 84738, 78427, 136685, 118384, + 20721, 79633, 31973, 11098, 53869, 52988, 3196, 72237, 190840, 185112, 79139, 28639, 79673, 97047, 9398, 15414, + 105300, 137313, 85877, 40993, 7561, 3929, 57151, 108868, 321817, 76740, 180609, 35166, 66924, 90464, 82436, 5838, + 13940, 45290, 67026, 19641, 33281, 26857, 36996, 98291, 101046, 54893, 50303, 114759, 183181, 305580, 14099, 33514, + 32176, 17094, 121586, 67669, 71878, 42975, 11177, 90273, 5678, 12886, 12867, 27433, 62924, 67753, 13445, 21183, + 79535, 24720, 155338, 122583, 56646, 51283, 120295, 43762, 40303, 86530, 14604, 190243, 19559, 88421, 25961, 150460, + 27703, 51292, 26294, 109748, 93042, 39631, 12236, 40405, 73368, 57016, 74750, 36429, 42187, 74845, 176283, 98118, + 3670, 19744, 24913, 39170, 37376, 11919, 150215, 7637, 46440, 61291, 85017, 262212, 7023, 214701, 15499, 13794, + 36816, 64790, 2778, 127954, 109628, 14013, 60615, 35098, 20680, 32322, 6631, 27129, 5940, 3801, 64451, 3605, + 276440, 16166, 42526, 46620, 275233, 3268, 98778, 42589, 170848, 13985, 52719, 40320, 112093, 58444, 41626, 122502, + 136996, 26123, 73872, 45093, 14984, 31647, 91921, 180982, 20226, 196060, 19058, 85542, 111441, 139969, 139431, 39188, + 77693, 106755, 134828, 38509, 112960, 28035, 81045, 32920, 48577, 41211, 1169, 51509, 39370, 57152, 7562, 65585, + 55707, 16633, 123632, 42194, 50314, 21232, 7589, 10730, 123404, 177035, 1102, 100263, 26570, 53756, 25965, 84962, + 54281, 98616, 1761, 302765, 61372, 4231, 11004, 6580, 123391, 152361, 53908, 15894, 4857, 138528, 185271, 79781, + 35928, 183068, 199461, 71306, 246740, 25571, 212575, 29887, 132507, 135536, 39243, 171966, 106646, 38340, 64208, 46548, + 44241, 12489, 59841, 52248, 7196, 49745, 18871, 214620, 92258, 92083, 128377, 10678, 9825, 273, 124186, 8170, + 37688, 47121, 81217, 46232, 51197, 22560, 24038, 76445, 303141, 269387, 168244, 7423, 5590, 148765, 8018, 44136, + 15476, 24093, 81595, 151484, 132986, 88790, 24759, 249112, 10955, 113624, 1490, 61893, 85782, 267543, 81516, 155220, + 34486, 85773, 20198, 5827, 26696, 182958, 47877, 133459, 7991, 47474, 35921, 5055, 28813, 58299, 36457, 2282, + 18858, 199851, 25971, 38032, 39546, 9892, 80469, 94210, 11703, 7530, 262048, 119116, 11365, 73348, 20636, 76662, + 38185, 147830, 33090, 12842, 5716, 136557, 29208, 2738, 93555, 29435, 37866, 29232, 11925, 18290, 200392, 9150, + 40121, 28353, 66557, 42934, 60002, 7949, 165121, 44607, 6985, 46265, 88952, 117927, 10976, 127994, 45713, 196202, + 171384, 16237, 17072, 82678, 3887, 53943, 11497, 25289, 43394, 194558, 17520, 13713, 13337, 10319, 13519, 27977, + 56346, 1512, 69563, 16655, 6381, 207119, 32722, 52407, 252336, 7634, 295451, 137340, 39573, 12588, 51167, 91362, + 75042, 12378, 44599, 44996, 53711, 62621, 59425, 154573, 1028, 6863, 70608, 89277, 31781, 37585, 33115, 45438, + 44295, 19441, 13395, 85115, 95925, 51396, 98398, 31173, 131713, 76310, 2356, 18575, 7214, 48396, 22367, 234325, + 68125, 225602, 25129, 98668, 43571, 24981, 27341, 97198, 70954, 116350, 138790, 6397, 213234, 6496, 199647, 33078, + 1628, 16810, 38131, 5432, 8760, 45379, 187797, 54575, 13391, 66830, 185263, 43959, 65485, 1291, 64470, 193537, + 17320, 32341, 19589, 46922, 56038, 16790, 33933, 93128, 1710, 22162, 60010, 166501, 25248, 110735, 133168, 19601, + 60803, 25674, 33996, 3691, 57399, 83385, 754, 371286, 19839, 26834, 28124, 75623, 3870, 66122, 97406, 47346, + 326, 7492, 166, 56406, 281556, 73496, 63104, 70066, 50945, 17847, 47599, 57275, 72400, 26598, 25290, 133091, + 21460, 6170, 26197, 90337, 316244, 23380, 15810, 44542, 75619, 115807, 61180, 160228, 56246, 187764, 113793, 119939, + 98775, 14502, 129669, 102901, 481, 18772, 33749, 181939, 13513, 80163, 57426, 95677, 55765, 123256, 156375, 131050, + 50427, 85036, 176699, 53355, 7388, 116459, 9415, 65345, 123519, 14467, 53003, 35892, 32929, 29108, 141336, 7280, + 57908, 121975, 16321, 66321, 27124, 17980, 13278, 64623, 61316, 27319, 33770, 106870, 101097, 65509, 32584, 7662, + 57467, 42561, 35131, 72066, 30228, 57341, 330807, 12061, 48792, 30042, 47357, 5873, 10788, 40943, 43598, 71460, + 20402, 3772, 196771, 8512, 37318, 98225, 52470, 20068, 50526, 32163, 6724, 21, 155730, 109241, 153690, 41562, + 34456, 4040, 322, 2322, 158065, 82495, 155777, 84593, 145305, 52472, 4167, 12757, 17832, 20800, 6523, 97949, + 43733, 5158, 13272, 52760, 64052, 64053, 5758, 88849, 4506, 28625, 252038, 68417, 35072, 144810, 45051, 20202, + 205355, 88775, 28776, 58617, 177028, 20250, 77055, 25756, 184004, 13595, 134521, 29706, 187081, 41138, 39350, 69874, + 68616, 45852, 200508, 35308, 35938, 187989, 19158, 97, 83703, 30783, 33740, 8875, 10367, 31971, 9154, 23524, + 36892, 35352, 34135, 16609, 93186, 10325, 61501, 123818, 260137, 38611, 132695, 10376, 51934, 40069, 47671, 13891, + 89970, 27882, 128001, 251194, 4985, 3138, 130144, 157048, 23445, 50690, 120275, 98986, 26755, 15060, 110524, 20070, + 21558, 21044, 241916, 81443, 7948, 110867, 98839, 117178, 78741, 134771, 8767, 9438, 23376, 10179, 78727, 73418, + 124405, 70709, 73897, 174550, 85594, 44683, 44048, 151892, 68596, 110549, 32110, 143310, 57536, 6531, 94425, 3086, + 49351, 16758, 85882, 1621, 12619, 28857, 47203, 30663, 56593, 71986, 13128, 49100, 132668, 101411, 14597, 17845, + 99924, 49653, 254641, 68928, 109728, 11860, 46362, 38227, 71844, 106860, 76428, 22535, 71935, 47939, 100970, 214387, + 196018, 26420, 64000, 108949, 65464, 85292, 57337, 107755, 17617, 127677, 45676, 32224, 14987, 15885, 23365, 73603, + 89701, 45904, 16644, 214947, 143773, 45929, 4638, 50137, 33997, 6717, 227758, 15712, 8687, 13499, 81744, 240749, + 22701, 43383, 31416, 14599, 33258, 22158, 46890, 13056, 2914, 36843, 11727, 56468, 4436, 2709, 77185, 66068, + 72108, 79794, 13174, 6017, 85610, 25092, 117935, 33783, 9671, 20404, 190908, 59000, 49067, 32677, 203049, 32652, + 2327, 64872, 122696, 32308, 82988, 180624, 3995, 109656, 1361, 89288, 58639, 34932, 1672, 23129, 114033, 56989, + 44033, 18540, 122789, 23587, 35777, 124932, 162078, 37243, 30269, 38337, 87579, 108472, 24057, 7318, 56171, 14522, + 10605, 36398, 2675, 68455, 82236, 229893, 28011, 180822, 616, 32685, 24540, 51512, 15793, 167325, 12485, 108625, + 13919, 125674, 46942, 46777, 47249, 192801, 19663, 7112, 112086, 41046, 136949, 15147, 116698, 102811, 103661, 220539, + 9484, 36775, 262931, 43580, 80207, 119020, 20707, 35285, 90574, 74644, 25380, 59691, 33304, 112273, 100827, 37498, + 68624, 28392, 22953, 10192, 93127, 6246, 78709, 81865, 56101, 3539, 141381, 87190, 42210, 15605, 33843, 301219, + 160929, 15074, 16495, 322258, 4827, 113897, 61108, 7004, 38995, 49556, 40164, 186048, 38095, 7638, 85669, 24310, + 4701, 41162, 181937, 6723, 125119, 103514, 13105, 42284, 5027, 699, 66990, 9452, 33680, 16754, 14243, 12215, + 9236, 9693, 21846, 11245, 231236, 26285, 119411, 107569, 14135, 13418, 13188, 12438, 87837, 154259, 13184, 45250, + 23318, 104289, 32255, 140998, 70261, 10596, 72378, 28503, 78893, 204776, 26664, 107431, 30151, 53770, 139833, 72090, + 81482, 31259, 9478, 50013, 14332, 4473, 8040, 96916, 1084, 3459, 15193, 216567, 74256, 68159, 8514, 64488, + 27532, 4063, 51181, 118473, 46644, 135301, 17479, 19499, 79185, 32968, 60750, 136481, 3148, 433, 1609, 42629, + 62615, 19220, 27930, 149236, 6981, 35309, 3835, 52812, 55672, 37988, 64541, 83189, 31668, 21678, 96359, 51037, + 36825, 16611, 55113, 22398, 1849, 16045, 71184, 149329, 14536, 231005, 89158, 27040, 37446, 89933, 98572, 9183, + 14738, 180870, 66669, 119194, 23779, 41333, 133198, 165958, 43058, 12753, 4695, 25960, 162749, 13009, 104735, 168462, + 72199, 56087, 30780, 17289, 1168, 63376, 32319, 148939, 21346, 183499, 57766, 111111, 5592, 163921, 51902, 6385, + 85932, 38348, 98269, 48430, 85302, 6828, 144609, 62182, 9668, 31728, 14877, 74838, 18351, 19716, 44331, 8938, + 57135, 111212, 35796, 196509, 150360, 14184, 81850, 64400, 2080, 43786, 89260, 297313, 228015, 4259, 2273, 106355, + 77953, 79956, 258556, 39080, 34670, 38042, 9041, 17580, 119302, 94867, 12312, 65168, 151751, 14199, 84612, 41781, + 31009, 41498, 2644, 8135, 106725, 11873, 65756, 50677, 84265, 100278, 19103, 25944, 45214, 71109, 29333, 199959, + 59289, 28006, 82735, 73888, 74178, 9357, 31611, 55567, 113071, 91025, 22225, 16280, 263206, 143472, 82038, 1016, + 111009, 3454, 111282, 6624, 4021, 148481, 82606, 3337, 44449, 87085, 117440, 117450, 188119, 96922, 66328, 69088, + 192629, 7164, 177681, 13842, 123592, 13678, 3709, 1404, 392506, 88363, 34011, 3111, 292847, 35632, 135945, 165656, + 114487, 28103, 36070, 105813, 12831, 13767, 283706, 20001, 205858, 222376, 26231, 74767, 9852, 14707, 55251, 21778, + 20780, 81817, 61071, 27574, 79648, 73651, 59592, 26903, 75767, 1269, 47843, 22121, 357014, 1880, 118720, 33888, + 97721, 25074, 162232, 165250, 18166, 101436, 126490, 14600, 21005, 87059, 98216, 138975, 67950, 11357, 183453, 60893, + 33226, 31203, 30167, 34258, 204009, 158681, 64517, 2772, 16536, 8063, 37505, 72518, 2987, 13469, 1060, 70404, + 11335, 58467, 606, 80478, 66717, 28334, 72244, 111835, 144910, 7759, 9382, 16929, 47950, 24768, 18442, 3812, + 17262, 78772, 57860, 139250, 55060, 192875, 157943, 1432, 15063, 5317, 91497, 24602, 2934, 71481, 112610, 7454, + 51038, 67723, 15783, 27576, 26775, 96465, 16801, 41801, 178497, 1952, 4258, 35229, 66008, 12359, 80274, 39191, + 3427, 79381, 23113, 37373, 49433, 369, 197495, 44949, 128592, 41764, 85267, 26089, 20036, 84751, 45080, 183648, + 157553, 6777, 354790, 8756, 63861, 32306, 73479, 133947, 3089, 129130, 28938, 68599, 23015, 83388, 4921, 46304, + 51210, 118961, 162098, 13808, 28696, 56249, 178796, 65420, 35933, 130109, 17053, 154383, 49942, 34368, 45185, 215004, + 71135, 72056, 58502, 92030, 231518, 3061, 31419, 84551, 99620, 59670, 13439, 24874, 17248, 49981, 120, 101486, + 21835, 11954, 21387, 80202, 176536, 69099, 25576, 83929, 20676, 54972, 21235, 121229, 16944, 207747, 17151, 62192, + 38700, 14914, 67294, 97250, 165831, 65091, 88309, 65140, 233253, 78072, 119737, 2388, 295625, 75403, 48047, 5295, + 36723, 97958, 31105, 11399, 13023, 94081, 105941, 87375, 52745, 78093, 142695, 58118, 10907, 36936, 47744, 98453, + 19423, 63195, 135090, 155076, 67972, 116671, 181141, 76674, 125868, 7497, 7282, 18173, 95473, 37926, 40931, 83084, + 82875, 9577, 10786, 82123, 1183, 13043, 25845, 7930, 108455, 108021, 144839, 298080, 52685, 25893, 74303, 83892, + 33417, 167899, 3363, 124169, 64095, 3491, 11574, 30740, 21433, 19985, 31353, 13355, 52438, 74347, 30435, 24346, + 33191, 43579, 92911, 14343, 127809, 55919, 202307, 87794, 44505, 85592, 45, 37946, 211823, 75477, 10315, 11020, + 7810, 8418, 14824, 32246, 2752, 43445, 78058, 51648, 95548, 28128, 139532, 78431, 162031, 54035, 118501, 105304, + 7185, 33563, 841, 39346, 91196, 67423, 12694, 225158, 47563, 63637, 48178, 53247, 61721, 57205, 32651, 77002, + 33359, 256934, 6389, 100897, 17897, 60470, 2058, 38569, 23682, 58623, 3141, 225129, 42806, 36558, 74186, 65297, + 178101, 65934, 727, 11385, 22874, 28253, 252846, 104582, 49707, 66565, 52743, 129552, 199897, 4526, 6020, 48400, + 75419, 48628, 13529, 146713, 82456, 3396, 19131, 77933, 8618, 26055, 136494, 270517, 11171, 161242, 27374, 10081, + 79712, 4483, 132351, 34334, 116847, 98238, 115123, 62251, 18783, 13247, 236033, 17204, 44190, 86807, 31546, 3315, + 46564, 61125, 188041, 5405, 5346, 199954, 2749, 50374, 59046, 179633, 124580, 35456, 95032, 68263, 16037, 104183, + 7893, 68877, 23745, 4536, 14916, 44942, 88134, 154142, 3214, 4074, 126680, 444, 26800, 13417, 122265, 141794, + 24172, 9594, 4737, 198047, 121453, 10533, 56408, 70714, 34362, 74346, 24830, 15909, 10250, 226352, 86319, 65102, + 17408, 113698, 65723, 192701, 18888, 29804, 92420, 119089, 4840, 52158, 6809, 47211, 68696, 127333, 9079, 122944, + 22831, 10761, 65707, 48241, 13162, 83275, 41056, 127568, 36005, 52744, 10097, 2969, 32512, 44770, 40452, 180033, + 14800, 160155, 1451, 16499, 62357, 50678, 47022, 118784, 41723, 26067, 60563, 169681, 45046, 58619, 46085, 32423, + 27247, 64149, 46183, 1743, 37486, 3802, 17194, 24647, 5372, 19756, 118649, 4609, 2564, 83610, 112562, 46541, + 34261, 17420, 81688, 66264, 298500, 194042, 239563, 21230, 66509, 23479, 154376, 33179, 133920, 158984, 47967, 146524, + 89138, 72960, 127604, 25358, 31305, 13512, 95336, 27420, 117697, 9777, 159295, 2091, 19097, 4938, 15518, 81702, + 108304, 448058, 126466, 7371, 81386, 239535, 1607, 211558, 84106, 53806, 374, 125065, 23802, 113479, 52151, 46803, + 46411, 21764, 51063, 22432, 63304, 31295, 6642, 26992, 946, 58917, 74562, 10672, 51417, 1090, 157283, 6175, + 41777, 30873, 49434, 31205, 41041, 70979, 28851, 11292, 19501, 20377, 16444, 125033, 115864, 49848, 35298, 117499, + 60743, 35553, 7657, 117188, 294354, 1111, 19913, 107692, 37955, 25543, 107874, 180298, 94165, 181537, 43123, 3058, + 18116, 7886, 11895, 8962, 1156, 81655, 22350, 1372, 17937, 241608, 114745, 32122, 20645, 31944, 32077, 3682, + 57114, 6050, 45126, 4535, 90804, 42423, 18205, 88298, 58042, 4218, 130816, 58944, 48643, 192539, 3516, 129850, + 92288, 67482, 5645, 6008, 9861, 19038, 25523, 48393, 2557, 163977, 7711, 69978, 88546, 98350, 106098, 5102, + 61333, 26201, 45643, 21353, 101008, 50380, 9018, 90864, 12853, 151908, 45670, 48604, 5148, 32715, 39819, 1925, + 87856, 45697, 2007, 66226, 4152, 9542, 6588, 36053, 144503, 58516, 41489, 32328, 73841, 5629, 33075, 228550, + 18718, 1278, 118540, 138339, 9789, 1171, 99245, 73118, 147565, 168255, 51267, 5701, 10846, 18000, 6368, 25481, + 42085, 30996, 30750, 21874, 12789, 33285, 67958, 162991, 30223, 14020, 26867, 21295, 8993, 66175, 79515, 148442, + 56352, 397258, 104422, 69338, 67203, 81, 103746, 192218, 2448, 103541, 4268, 26498, 28215, 26795, 119536, 166999, + 6052, 116957, 18923, 77982, 23540, 54054, 31372, 18430, 126319, 39890, 1756, 5893, 75933, 40146, 271195, 76657, + 36689, 21496, 89710, 61547, 80235, 15197, 81995, 39003, 23231, 6321, 26632, 19783, 23561, 14948, 63802, 56505, + 21383, 38408, 16252, 72824, 38800, 196129, 74267, 8127, 77548, 48102, 87833, 36171, 102798, 47900, 15495, 173684, + 21234, 30001, 52403, 46069, 31468, 16788, 57124, 45489, 158608, 82654, 59957, 10410, 46188, 171714, 31155, 59538, + 63960, 149773, 98372, 22924, 191679, 82803, 49057, 16752, 8051, 157358, 24903, 23142, 67014, 23023, 30666, 6199, + 11185, 61761, 108153, 49193, 170078, 64795, 286604, 157605, 42186, 22327, 189946, 54197, 28827, 199449, 44686, 59637, + 34777, 152898, 16645, 83206, 41930, 102775, 24648, 36524, 212079, 95709, 1548, 100504, 12421, 68448, 31268, 474576, + 34750, 218355, 92781, 134981, 24111, 67882, 54747, 174240, 110344, 94961, 114973, 72370, 73918, 15923, 15159, 7647, + 45171, 65568, 43530, 34464, 70826, 34474, 60365, 197139, 141949, 210472, 46891, 121996, 40063, 94408, 12981, 134679, + 23979, 51874, 24714, 19995, 24254, 36223, 157770, 7326, 37309, 94133, 22106, 78205, 26724, 2537, 25382, 106656, + 27179, 66311, 110415, 22901, 24718, 57112, 55550, 53766, 83648, 13063, 9413, 49639, 54938, 59353, 51453, 13425, + 14591, 124743, 17157, 12381, 17425, 61233, 6845, 73147, 29525, 129664, 27744, 5277, 102675, 54090, 98234, 13352, + 48975, 17754, 16594, 102750, 48654, 39678, 70262, 34873, 12316, 8508, 46395, 134713, 8929, 37841, 2872, 44089, + 60640, 45964, 41710, 35853, 41709, 32561, 10847, 30533, 50168, 131914, 35502, 7179, 63264, 101004, 13098, 13461, + 89812, 195792, 68289, 50907, 50716, 618, 42226, 56670, 48632, 3817, 23883, 6968, 38755, 21629, 7154, 3872, + 138583, 91142, 62213, 50389, 160123, 68183, 5493, 38, 55579, 58839, 15930, 157836, 71829, 40468, 28030, 16196, + 24230, 33693, 43412, 1608, 233277, 6049, 16708, 17549, 46322, 16760, 153973, 62078, 39650, 117125, 26112, 36797, + 166388, 166224, 101342, 69645, 34718, 6450, 41096, 88450, 24108, 54215, 7626, 56703, 98252, 7498, 5996, 104382, + 7031, 18513, 1464, 20355, 106695, 12121, 15785, 39445, 62498, 113, 143048, 42438, 18258, 69031, 20970, 28430, + 35062, 185809, 306428, 189608, 217827, 3480, 29084, 79447, 78731, 32985, 65014, 127203, 34824, 4550, 257781, 321144, + 33354, 3217, 218324, 88938, 19520, 11596, 43514, 141333, 60852, 65248, 6297, 11095, 2432, 14119, 13748, 7469, + 60224, 12819, 57422, 64540, 8587, 52981, 73978, 23601, 2836, 30776, 142181, 19639, 62955, 10337, 595, 211745, + 702, 140990, 56342, 19255, 20227, 135292, 29499, 348456, 42285, 59021, 23153, 398, 40560, 12943, 20085, 118311, + 95592, 31441, 30421, 18709, 62486, 96516, 37157, 85251, 11094, 115980, 17285, 128523, 53035, 4890, 10141, 87821, + 143291, 78302, 59857, 33639, 18842, 69975, 11769, 10009, 46177, 505118, 54757, 192067, 77994, 12960, 50591, 24130, + 1770, 3662, 133316, 209915, 9657, 48891, 39791, 15630, 107422, 16844, 81827, 8655, 172915, 60826, 36740, 246774, + 32655, 23684, 174097, 93563, 128716, 9168, 16463, 138739, 25886, 3373, 1869, 35842, 25164, 41241, 1126, 17486, + 156740, 84370, 68201, 98020, 119928, 18998, 6457, 120651, 165875, 3291, 58021, 5480, 85817, 5424, 7918, 43877, + 11007, 125299, 24181, 67060, 89110, 233405, 35096, 471, 33956, 19798, 152016, 168120, 12652, 156405, 133111, 116254, + 65156, 42113, 9958, 120153, 180266, 111540, 269252, 123200, 8494, 34196, 8578, 82607, 36889, 12572, 69053, 33057, + 19958, 67796, 22249, 3640, 20955, 15759, 9894, 33572, 96, 8969, 80871, 33260, 1264, 67165, 9108, 87451, + 118288, 65977, 59131, 17708, 135769, 66193, 98679, 61993, 44754, 23134, 122439, 76614, 86671, 113511, 7544, 55848, + 5632, 21119, 183800, 40580, 19026, 82001, 99485, 55491, 168220, 108940, 236173, 16220, 289120, 163513, 75470, 32902, + 33569, 88755, 3113, 96883, 93821, 299927, 114933, 5574, 66144, 80618, 43070, 125428, 70635, 119887, 3673, 55958, + 7687, 101495, 5831, 97794, 5642, 23968, 7105, 10753, 2714, 39381, 104590, 82035, 55445, 30394, 30985, 36078, + 56636, 42720, 49190, 134117, 71545, 2558, 567, 53812, 184182, 144, 41897, 149050, 93133, 9426, 48576, 19715, + 7332, 68965, 74846, 31701, 37389, 27344, 9014, 157686, 12643, 17067, 29166, 15457, 52315, 133218, 97956, 26560, + 22729, 19455, 100687, 66306, 11014, 31241, 157229, 1036, 158742, 1911, 18856, 8404, 17050, 49637, 105257, 26263, + 152889, 33300, 24194, 15449, 50178, 57835, 89431, 93344, 34601, 52068, 6811, 39076, 41945, 41909, 38286, 221235, + 52136, 190373, 62204, 7398, 9948, 20577, 17514, 97470, 26914, 13538, 27727, 115631, 63548, 60637, 26694, 36204, + 95721, 237466, 17635, 8311, 115951, 155407, 372775, 119613, 40759, 7149, 153893, 29766, 8960, 16679, 2573, 75649, + 158258, 17939, 56612, 138333, 38938, 4371, 39651, 46863, 49232, 4029, 76050, 62734, 48325, 4870, 8916, 70189, + 42234, 482, 51761, 85517, 81523, 299332, 91771, 91710, 253019, 32687, 95521, 66713, 66128, 29849, 16707, 26650, + 40978, 43719, 4139, 42193, 20048, 230595, 55007, 13024, 238048, 4557, 73590, 107873, 38760, 29575, 21439, 27927, + 62928, 129083, 22364, 3858, 122560, 50006, 32925, 40998, 118532, 6970, 5412, 10487, 43687, 53543, 72899, 67285, + 137472, 16857, 46509, 32903, 163689, 209351, 336887, 206325, 26680, 147850, 10276, 166671, 9878, 111570, 4130, 56293, + 17448, 129038, 49791, 3323, 51035, 77462, 50111, 20907, 16211, 5982, 24162, 56278, 60834, 28521, 209493, 95578, + 36749, 39356, 52739, 208999, 29178, 58645, 19127, 12057, 14241, 39982, 78123, 80813, 59868, 6719, 15571, 19762, + 150086, 20349, 278029, 2666, 2305, 133772, 121203, 3570, 26477, 55785, 32824, 6151, 42422, 310941, 94923, 18190, + 34342, 18288, 70657, 25029, 165341, 34909, 320097, 33308, 83279, 75404, 45112, 17301, 33894, 4577, 18669, 9219, + 14257, 107253, 512, 36710, 29928, 16653, 7279, 43478, 12743, 3862, 3899, 37090, 13957, 212990, 2895, 53197, + 125571, 40119, 7450, 95395, 89134, 18938, 54751, 39220, 66712, 44978, 98747, 35334, 10952, 9840, 217378, 35750, + 16507, 209521, 52915, 36820, 147839, 56525, 13285, 1981, 30146, 51236, 7364, 104143, 7249, 222095, 6399, 165008, + 16565, 13029, 27969, 105952, 45399, 41770, 169129, 53714, 39874, 7853, 68938, 61382, 114565, 59737, 77275, 29135, + 215780, 6342, 153711, 36717, 31990, 50742, 2834, 52961, 230881, 31804, 87897, 73954, 171477, 9339, 91416, 34110, + 102, 54930, 1613, 11876, 196546, 591, 20460, 50406, 44538, 17056, 34267, 74596, 10880, 9193, 96244, 29689, + 84948, 20424, 26253, 45632, 281705, 119042, 185961, 44497, 86651, 192207, 11795, 114742, 10617, 265712, 62568, 92499, + 31395, 217897, 187475, 50383, 2342, 23312, 42405, 26096, 453658, 416, 133446, 93547, 43569, 10623, 121978, 21757, + 60561, 74826, 42853, 104403, 132901, 72040, 67234, 59013, 21845, 102518, 153642, 11146, 17727, 102632, 86313, 72775, + 58556, 407048, 21027, 6844, 258242, 89512, 3527, 107940, 22262, 23956, 4850, 30270, 58728, 16998, 117875, 20689, + 4008, 14646, 93000, 1170, 77997, 167859, 54531, 246835, 11806, 26637, 167713, 64014, 37431, 88103, 62362, 304585, + 30599, 44120, 31653, 42563, 81375, 43712, 1534, 49941, 109137, 145866, 103885, 146125, 185787, 22202, 33673, 3681, + 114085, 22277, 2072, 10477, 217292, 47874, 25630, 61064, 97453, 94351, 180344, 34224, 169085, 68184, 1881, 53451, + 30593, 100128, 39311, 3509, 60212, 90102, 7297, 23473, 11544, 261844, 8370, 27945, 102056, 100221, 39918, 115112, + 65580, 27264, 10582, 66982, 2384, 29292, 51255, 8837, 91655, 60384, 127046, 34788, 4855, 82574, 50338, 58959, + 95725, 135206, 28047, 308666, 7295, 41193, 95306, 61581, 157994, 185542, 36976, 28033, 16228, 38830, 10448, 15213, + 20669, 49883, 2493, 9951, 53276, 25223, 13971, 17396, 141590, 48264, 32377, 28668, 105246, 22993, 20557, 139621, + 17334, 37996, 19658, 20744, 25440, 34122, 116482, 22910, 76067, 36796, 67819, 11102, 17967, 9536, 55606, 117501, + 39321, 17563, 65478, 3571, 38911, 73504, 54583, 10444, 11362, 66927, 89439, 14702, 28559, 7682, 1513, 70102, + 63807, 20057, 46270, 21001, 21627, 256959, 184942, 165920, 26468, 36183, 112227, 4448, 85816, 65617, 77464, 8502, + 40120, 56527, 77638, 3102, 1025, 32749, 21811, 153766, 15234, 14725, 328941, 53852, 58319, 90203, 39231, 68729, + 69516, 3450, 70578, 52687, 66512, 8474, 90859, 4090, 124548, 66064, 39981, 28642, 75845, 40634, 22064, 107475, + 78873, 112238, 18036, 60258, 22137, 89278, 35142, 92257, 46681, 60200, 168934, 5658, 51242, 3324, 71459, 64772, + 85683, 32, 475, 18068, 32909, 134334, 1380, 31213, 76747, 9749, 4201, 23438, 35555, 83518, 23878, 30619, + 43396, 224662, 33147, 28171, 101465, 60063, 34256, 71664, 1765, 160809, 93427, 22239, 73094, 35152, 17172, 38091, + 1077, 83764, 93545, 57605, 2962, 9185, 13119, 81080, 39028, 85967, 49835, 9330, 66777, 27605, 184827, 77443, + 57831, 49230, 57654, 174766, 42048, 200323, 166050, 27249, 15828, 76305, 4194, 44524, 13962, 51363, 67697, 14701, + 36041, 69963, 72263, 4470, 63657, 47301, 159567, 22495, 52412, 69094, 411, 3653, 5242, 4607, 116518, 99099, + 58846, 58552, 89470, 53229, 2141, 273007, 27018, 137017, 5506, 4365, 159178, 120422, 219012, 127162, 69641, 26407, + 134451, 42927, 9321, 12622, 3936, 145843, 24141, 64837, 182230, 170529, 104842, 135255, 17558, 17348, 53327, 12872, + 17153, 10391, 30, 74060, 152237, 14185, 205380, 31566, 22621, 30796, 108736, 26638, 49377, 673, 90154, 51194, + 170216, 63571, 45050, 32131, 35257, 17360, 55199, 20393, 68233, 118377, 63209, 4669, 65374, 65781, 84411, 92579, + 6510, 108160, 56952, 192831, 11126, 55557, 110245, 73583, 212151, 16429, 369879, 20590, 7184, 24497, 41359, 86934, + 2480, 131718, 116501, 37833, 22517, 9731, 10757, 78661, 3437, 19565, 6471, 13896, 33073, 74906, 286, 53375, + 30156, 29608, 44034, 13989, 16557, 36336, 11786, 9016, 3768, 30883, 52210, 33318, 55067, 37827, 143891, 319150, + 86829, 93201, 25144, 48395, 91000, 56891, 94800, 60129, 12350, 105311, 185, 78672, 148650, 2793, 13582, 54814, + 66017, 22898, 190292, 69218, 79424, 1785, 10144, 90001, 70885, 6226, 65791, 123504, 49066, 19757, 151381, 633, + 28250, 21135, 4879, 91112, 21369, 79333, 54846, 138302, 51213, 7684, 96590, 49597, 34533, 30172, 13591, 193897, + 11510, 1702, 73010, 42908, 3258, 13889, 151689, 47726, 18176, 159526, 179101, 31058, 18465, 26752, 188829, 15744, + 84413, 58916, 38458, 15452, 6315, 50155, 89783, 5933, 36411, 8754, 11471, 15409, 163765, 12966, 2808, 36697, + 4346, 161209, 70415, 56116, 356, 211098, 14356, 36846, 107618, 53672, 48457, 30017, 598, 176513, 81011, 18982, + 13727, 86974, 48130, 58055, 285026, 119331, 69388, 22734, 162695, 127129, 6676, 10209, 8749, 19120, 28354, 35996, + 14583, 2294, 21774, 7098, 7132, 30506, 68020, 26105, 63521, 287047, 31652, 29144, 184253, 45019, 59739, 54276, + 32378, 4681, 28618, 155387, 25991, 60268, 28085, 115514, 5604, 89273, 73475, 54698, 30961, 26093, 14648, 29343, + 53675, 45157, 6691, 46637, 4874, 13617, 45597, 95292, 84693, 71181, 195063, 4205, 5086, 45424, 49483, 57401, + 34811, 81433, 10920, 60312, 26978, 122016, 164823, 61302, 56564, 35695, 36714, 67995, 7904, 42503, 11148, 636, + 33519, 45175, 78078, 5252, 51221, 95264, 51178, 64304, 113942, 105499, 51841, 54146, 69253, 1646, 75338, 105535, + 6664, 10892, 158642, 62272, 125563, 215912, 33549, 19391, 22055, 105382, 20979, 20764, 220680, 38079, 4062, 13641, + 102008, 4203, 20143, 28206, 742, 47639, 13458, 8201, 51930, 16197, 22187, 25787, 19494, 75134, 114304, 94871, + 176108, 45555, 116, 160484, 44424, 12397, 146985, 10330, 35123, 239546, 28027, 7835, 13025, 82191, 412132, 7092, + 75685, 170210, 25010, 106234, 11759, 43065, 8341, 715, 74335, 50832, 12326, 62728, 22250, 132367, 15166, 1485, + 181453, 100489, 159053, 49558, 131147, 218225, 72843, 179732, 16984, 57645, 5036, 111986, 132115, 50930, 114625, 1202, + 154311, 67481, 16722, 47767, 11991, 40423, 208598, 57602, 76452, 34425, 3714, 48128, 52609, 81295, 9952, 36845, + 85351, 16065, 20654, 19823, 196, 24873, 26870, 1297, 30969, 9296, 54228, 102469, 9198, 72519, 70039, 108396, + 74919, 85172, 66125, 101377, 2529, 117805, 96810, 24778, 56838, 145070, 22575, 3889, 71779, 55500, 69200, 56256, + 29187, 1545, 88869, 2923, 116304, 4719, 101420, 2691, 3504, 4334, 13196, 24283, 62330, 11167, 46433, 98976, + 41190, 4304, 38379, 134621, 86153, 55398, 18685, 16560, 28393, 104952, 236521, 190161, 254926, 97238, 29381, 35076, + 104228, 132048, 21165, 43831, 105189, 81338, 60610, 75143, 13264, 30942, 42614, 10992, 84359, 171469, 36755, 2955, + 3574, 50858, 7720, 113363, 12415, 1082, 8309, 16640, 8534, 89729, 1994, 39280, 57147, 259158, 159887, 14424, + 10175, 59116, 66944, 434056, 188174, 112776, 24021, 39757, 59504, 17275, 18210, 6474, 60932, 53833, 48968, 4909, + 66318, 8851, 66549, 14450, 16407, 3384, 60036, 132244, 107921, 15561, 173708, 120543, 17638, 31452, 27253, 351, + 99103, 3968, 104995, 54896, 49278, 134414, 51028, 59123, 28403, 11528, 46089, 883, 39786, 58165, 83022, 52349, + 145865, 50737, 25618, 47214, 8462, 6871, 83132, 9635, 3558, 132475, 8680, 97166, 43406, 3898, 139071, 89536, + 142271, 8320, 29089, 83232, 29139, 37374, 11887, 13573, 21989, 22330, 18987, 8277, 36552, 94008, 13402, 19357, + 93955, 192242, 23973, 19276, 72365, 83440, 43343, 20913, 7176, 15598, 107084, 9241, 13556, 46936, 46456, 48864, + 106185, 54904, 62326, 15469, 37957, 3365, 221144, 71002, 321774, 79203, 35192, 114690, 17782, 21700, 10335, 97643, + 129017, 125386, 115842, 123788, 51154, 15859, 74495, 30765, 27938, 35855, 55933, 25082, 24952, 27639, 71742, 23512, + 1935, 32522, 98612, 72854, 39366, 29090, 7051, 75719, 2791, 156786, 23966, 41744, 33489, 127417, 36727, 13017, + 41582, 128016, 125842, 141281, 56078, 108220, 31920, 77429, 24558, 59290, 9558, 18760, 9311, 51229, 19776, 17252, + 5449, 45702, 18396, 21552, 218786, 72761, 87355, 23965, 27808, 90969, 38872, 26050, 190429, 178071, 13825, 15009, + 68013, 27591, 38972, 14110, 36020, 50172, 200538, 281255, 86003, 53465, 47708, 215042, 29735, 28534, 1368, 69656, + 3404, 24318, 47650, 37653, 87348, 40656, 8697, 25748, 119357, 39951, 51347, 22849, 115714, 1560, 13392, 73201, + 2324, 35889, 27677, 41987, 86796, 378138, 63587, 34445, 81973, 5437, 94808, 11707, 40992, 27780, 19572, 62130, + 43376, 35564, 204011, 21987, 93621, 4561, 146551, 233073, 28784, 56146, 1533, 99739, 16808, 78879, 204037, 27916, + 36367, 31905, 55776, 19527, 2517, 49088, 49061, 67853, 2909, 67751, 72529, 12945, 191926, 57369, 47731, 345715, + 24978, 37675, 6961, 26371, 55303, 17783, 45945, 57303, 53308, 102816, 50376, 152609, 205724, 29088, 100615, 14981, + 60068, 20755, 67912, 22984, 3098, 119389, 76407, 31834, 21375, 86387, 71132, 54368, 64784, 154591, 7007, 38915, + 23949, 15688, 86044, 32410, 26579, 72957, 140702, 2534, 63121, 109577, 36571, 7087, 12319, 38027, 139635, 69422, + 80145, 58104, 77565, 134884, 39967, 128121, 122239, 98149, 97861, 77841, 122108, 53941, 6757, 105412, 5433, 45309, + 70143, 41083, 108846, 68195, 67642, 48548, 22786, 15898, 37082, 16924, 108012, 18926, 34698, 130920, 7208, 84736, + 69140, 115981, 3166, 30758, 122883, 16988, 18278, 9920, 46151, 31634, 162250, 22353, 62187, 128316, 15612, 158856, + 80934, 678, 6963, 76542, 429, 111036, 69613, 18534, 19437, 41943, 186274, 23104, 135071, 64054, 152408, 69055, + 137885, 55427, 70719, 41478, 222647, 50820, 94953, 48990, 13331, 245898, 18180, 15149, 154065, 124925, 217145, 82559, + 327, 139288, 191206, 14470, 61778, 6536, 73668, 129358, 74257, 25611, 26141, 105172, 40116, 65034, 30807, 96573, + 37493, 33559, 28049, 4551, 14855, 7513, 48252, 20443, 85079, 69697, 8617, 95869, 237641, 48366, 36858, 128756, + 42342, 3347, 3587, 16403, 102164, 201481, 17664, 68064, 199965, 62177, 224396, 15650, 71204, 12089, 125579, 76206, + 4662, 192158, 115192, 18064, 29368, 28388, 88784, 93189, 5042, 144274, 26733, 112088, 113914, 8714, 3328, 85297, + 122214, 129118, 14482, 6034, 8955, 1500, 17059, 143211, 13149, 46390, 180700, 11460, 102503, 31711, 124947, 75478, + 43173, 42126, 146701, 30953, 5659, 29827, 78342, 38880, 163787, 38389, 57177, 12232, 69003, 35027, 31713, 32846, + 307084, 9190, 1177, 53022, 63392, 36783, 49720, 204857, 171080, 2218, 24668, 46908, 21390, 44902, 5316, 16460, + 81918, 10429, 22493, 40140, 86666, 28478, 19197, 39367, 36622, 197757, 35536, 74, 24126, 27886, 49478, 60977, + 28887, 159663, 25478, 34942, 5736, 100422, 11653, 280716, 28054, 76213, 269086, 56854, 207170, 12900, 26987, 34564, + 163428, 35718, 204153, 14178, 79891, 154374, 93383, 82977, 346467, 2260, 194527, 234413, 95363, 223815, 84524, 24639, + 38980, 45400, 11669, 39361, 111806, 107478, 23180, 284178, 80828, 13760, 60535, 160002, 9200, 111334, 9377, 23027, + 19288, 10467, 219338, 9403, 294896, 38891, 13604, 6964, 114468, 33089, 246026, 89623, 87405, 73198, 53282, 51034, + 111715, 3542, 21564, 14671, 141705, 108308, 240720, 1039, 7015, 84375, 20894, 62644, 72754, 61580, 52142, 32302, + 68463, 27046, 8380, 114459, 48738, 174003, 101876, 70865, 243147, 15573, 1008, 73339, 33397, 75662, 295288, 97483, + 101210, 83260, 287998, 315229, 37051, 33599, 14151, 14676, 98801, 6611, 61888, 45855, 82847, 52987, 104357, 56918, + 20397, 29312, 1971, 1398, 4940, 37732, 8028, 358536, 185559, 183958, 188192, 3892, 18716, 73119, 6906, 4369, + 54718, 83934, 109096, 203506, 83491, 133450, 4849, 4027, 11725, 13003, 142499, 87752, 40803, 56197, 37560, 36968, + 1128, 46164, 119031, 23432, 12128, 100279, 84518, 28176, 23060, 41295, 20799, 1558, 5174, 23677, 97773, 39548, + 7745, 10446, 105114, 10769, 67007, 87718, 236680, 161719, 46701, 32017, 4931, 19125, 1571, 28423, 43674, 64085, + 27807, 77944, 8529, 13223, 180186, 17974, 13548, 140128, 256996, 51253, 43890, 57544, 18975, 7744, 44658, 28313, + 16837, 105, 127130, 126370, 7877, 20389, 133921, 174075, 212758, 64619, 11797, 47857, 250379, 30209, 1917, 86594, + 15440, 21744, 135164, 34325, 87954, 11946, 12540, 311, 57755, 56815, 231078, 40529, 24719, 141657, 31438, 81877, + 124057, 251942, 13765, 33986, 83461, 63216, 7231, 198398, 258, 90044, 88181, 173923, 50864, 61645, 10174, 54553, + 8874, 28481, 1537, 50513, 29038, 39872, 84988, 57246, 71289, 12015, 94709, 4871, 31627, 279590, 81311, 43545, + 15429, 59109, 71443, 10416, 9005, 6896, 26184, 104749, 4061, 38580, 23344, 2875, 113851, 13804, 63848, 29618, + 107716, 136811, 54885, 33689, 82819, 50624, 149592, 114467, 13651, 11306, 73580, 85777, 79656, 166366, 222515, 39565, + 117851, 47704, 43581, 137265, 17539, 25867, 43261, 1034, 111446, 117360, 32008, 3664, 12938, 250542, 16992, 14203, + 39724, 83746, 187150, 20582, 190787, 110150, 10914, 42740, 4352, 122213, 31831, 49606, 15402, 8583, 34572, 18519, + 21070, 72128, 605, 14069, 62708, 68579, 26360, 45158, 8539, 33700, 23921, 174967, 23777, 21086, 47035, 21403, + 73853, 29070, 14402, 55968, 13552, 25123, 132516, 99114, 38810, 83727, 93287, 108584, 86117, 4037, 69026, 148748, + 16285, 151286, 2526, 67336, 56400, 114949, 38156, 64061, 1718, 14168, 68963, 19987, 75342, 72606, 92460, 25819, + 142863, 865, 10759, 102417, 29033, 128297, 176851, 239106, 378, 3476, 126764, 880, 110113, 35294, 142380, 60787, + 180321, 12139, 33148, 3243, 32586, 75807, 8895, 69743, 23606, 76161, 0, 32234, 26393, 15253, 101159, 37444, + 160984, 100458, 106798, 118153, 207987, 94453, 31068, 199369, 23783, 55406, 72063, 196753, 8406, 65985, 6649, 169369, + 16904, 2912, 14378, 60219, 24596, 44552, 22129, 79241, 47274, 46556, 155072, 25, 11693, 93339, 175196, 82189, + 46539, 25983, 55777, 39522, 60524, 133678, 18470, 77553, 78595, 83411, 33676, 121561, 48423, 93395, 74963, 17181, + 31718, 50927, 45519, 1283, 20170, 4973, 55435, 106949, 9009, 50903, 7663, 1685, 146268, 116269, 1759, 101910, + 15183, 77271, 25151, 101108, 191060, 44211, 30237, 101323, 172765, 81369, 7540, 8670, 1349, 2983, 72423, 57110, + 138436, 10840, 116808, 148570, 37365, 13235, 144011, 326438, 10970, 137859, 245169, 166948, 40509, 8825, 6884, 140489, + 225817, 339638, 308668, 14440, 20021, 172262, 20842, 35348, 70394, 77862, 44480, 114795, 152138, 205967, 80249, 66813, + 21541, 21337, 74233, 246096, 66559, 255232, 7873, 74405, 66544, 78592, 41670, 16122, 89352, 272783, 66583, 16290, + 2725, 97449, 74073, 152635, 17258, 14662, 164479, 7400, 91345, 77718, 116341, 23495, 7313, 145703, 66781, 95536, + 3815, 59962, 62075, 48667, 115868, 88403, 11136, 31573, 8660, 48458, 10303, 30913, 40362, 76502, 54728, 210919, + 4071, 10395, 14132, 19281, 103524, 46479, 2475, 9346, 39388, 11097, 7103, 61594, 118275, 49094, 17865, 16935, + 21950, 49474, 38996, 44270, 6549, 83808, 6663, 54784, 38226, 40402, 13514, 51690, 32754, 33017, 146868, 3834, + 209574, 31460, 2290, 5991, 29201, 189025, 95100, 116204, 43495, 37630, 621, 42790, 18028, 147636, 74713, 72101, + 20296, 39620, 52377, 149079, 40597, 71087, 48840, 41861, 18370, 78232, 9624, 43707, 47520, 67447, 30945, 84196, + 202450, 110891, 61689, 177365, 24134, 22168, 57538, 47780, 2219, 96079, 30892, 4049, 8195, 17695, 2313, 196966, + 69545, 7785, 159038, 116984, 38041, 85191, 95576, 35677, 136934, 87868, 8522, 47264, 46374, 90011, 2454, 121060, + 19041, 15646, 218596, 20647, 159811, 5273, 78222, 5790, 84865, 690, 36264, 169096, 58620, 49769, 19870, 56574, + 846, 44944, 30096, 21418, 98749, 62549, 49755, 90559, 13569, 65527, 23045, 35183, 30714, 1120, 129237, 133919, + 97246, 73893, 1242, 5886, 32186, 73097, 9292, 172076, 4479, 48341, 159618, 10702, 27355, 9327, 3896, 145089, + 92973, 127562, 80631, 140609, 35214, 40087, 6621, 60953, 151491, 93233, 133991, 5681, 75963, 13834, 189749, 48238, + 37631, 42943, 70301, 22025, 1561, 101790, 52288, 23519, 27200, 15594, 330, 128333, 238083, 77116, 27436, 14656, + 23182, 176130, 43018, 93766, 60756, 68727, 36117, 110670, 12291, 90265, 17003, 86775, 25766, 50378, 97783, 72630, + 39355, 51597, 76915, 9532, 102333, 333464, 26234, 91954, 87362, 18461, 14063, 123415, 65741, 87119, 13821, 69651, + 59906, 29250, 5334, 111676, 19538, 14466, 71314, 49631, 201575, 100994, 81169, 99402, 48772, 1958, 86381, 84313, + 102938, 3927, 14847, 115137, 24438, 1719, 106468, 14344, 292580, 113808, 25709, 4422, 39964, 14448, 9781, 167464, + 66366, 38612, 21306, 135701, 9004, 3967, 14882, 60987, 61379, 70755, 79621, 230123, 50548, 251137, 45013, 68059, + 37622, 8601, 52714, 131016, 38732, 161564, 5054, 17043, 28379, 100315, 11542, 22026, 68180, 40581, 195387, 5840, + 76622, 286510, 15220, 21299, 17488, 10435, 34592, 77414, 69849, 292262, 92100, 61536, 5963, 40657, 72834, 85029, + 7219, 137580, 32702, 24398, 48143, 18315, 7679, 10221, 43413, 55964, 56264, 22174, 55358, 421, 32944, 33586, + 540, 28638, 297324, 19055, 58691, 13123, 1457, 13545, 29506, 12292, 90917, 28767, 19245, 95156, 15265, 9677, + 52193, 329199, 120474, 31550, 48621, 32454, 173844, 6940, 5518, 104662, 27465, 11002, 13048, 23406, 127455, 219189, + 118625, 966, 55994, 71978, 44755, 44425, 58484, 81947, 191081, 71933, 4128, 149448, 42061, 45036, 34993, 81007, + 89197, 17973, 12644, 20286, 2259, 12086, 128406, 38420, 60665, 7439, 63696, 63513, 66994, 154461, 3204, 269842, + 71210, 72373, 47920, 147744, 51232, 11668, 11964, 5455, 3585, 62466, 87325, 135079, 142096, 6038, 3351, 59851, + 55024, 90797, 96831, 151965, 7892, 59192, 18848, 84506, 8345, 62542, 50046, 26538, 58653, 123823, 70584, 33087, + 463307, 52217, 94498, 162262, 65658, 37813, 1798, 133237, 64319, 20468, 342279, 2483, 137941, 174532, 134882, 13053, + 136323, 101319, 14858, 315661, 53499, 38349, 1883, 36549, 12746, 4783, 17315, 26629, 43492, 1433, 30848, 103477, + 6978, 53491, 254027, 59138, 95163, 23047, 130383, 71358, 29925, 104048, 147110, 20605, 60175, 141493, 1502, 26372, + 5128, 8043, 160154, 205752, 7352, 24589, 41005, 8787, 41463, 180084, 35056, 128767, 184756, 116446, 216131, 52109, + 121146, 31824, 120507, 78611, 20473, 80909, 52814, 53045, 18426, 47765, 19434, 251918, 4598, 37752, 2736, 30186, + 5309, 104505, 61873, 39702, 54580, 60995, 6795, 78461, 14277, 102209, 56595, 76365, 121151, 84271, 40571, 22053, + 10691, 60167, 8833, 12920, 56711, 2593, 16106, 101890, 43880, 47882, 105443, 26296, 63409, 2286, 236404, 3535, + 76682, 31275, 91877, 94825, 11830, 60322, 77261, 14017, 172218, 120226, 23764, 296, 264898, 128743, 243138, 11474, + 32632, 32455, 30389, 37844, 66536, 221177, 32878, 17990, 81062, 25157, 197499, 6108, 31649, 68174, 4883, 39604, + 25788, 24670, 22233, 11063, 92774, 167596, 40860, 103202, 60222, 67201, 150525, 69109, 11100, 21724, 207627, 23699, + 63159, 50235, 17317, 132722, 9432, 137156, 14594, 85114, 224657, 60669, 32727, 134466, 25240, 42617, 6453, 86119, + 53613, 52551, 39633, 95001, 152, 22181, 33930, 6888, 138620, 139630, 14275, 67967, 163829, 44096, 23693, 35554, + 2397, 2230, 20289, 26508, 85345, 44002, 373, 23558, 12501, 6393, 128738, 2065, 37507, 108535, 43648, 37304, + 64932, 28919, 39223, 9316, 38575, 60236, 72946, 52874, 43522, 3500, 42635, 18532, 65789, 350700, 113725, 232391, + 80198, 62151, 18623, 5216, 78796, 80102, 187580, 46871, 35226, 102412, 1673, 53825, 3851, 294484, 119721, 213745, + 108891, 1551, 16270, 77, 73311, 86689, 90501, 11580, 3060, 174403, 54046, 2060, 28391, 42872, 40600, 5734, + 93671, 48215, 33532, 19766, 39663, 848, 33334, 33841, 46142, 2841, 116488, 9276, 30982, 41267, 161026, 52345, + 66041, 22012, 75546, 14211, 37281, 137475, 48692, 71432, 68157, 27935, 25905, 26558, 26553, 91667, 162874, 41931, + 71872, 49342, 134603, 3964, 81142, 58684, 69664, 61624, 211527, 194930, 43281, 38136, 39747, 141202, 109912, 103720, + 118119, 105830, 244717, 53752, 22695, 3660, 15950, 115237, 2859, 29995, 32157, 26681, 11066, 63677, 2677, 27475, + 20232, 20055, 83960, 187268, 168911, 71409, 21339, 67656, 7933, 3860, 3943, 4936, 197005, 39134, 94952, 21684, + 17066, 56970, 61053, 6076, 111071, 9161, 30747, 75947, 44434, 9778, 40744, 3960, 133994, 16681, 158292, 120209, + 120798, 124190, 20560, 28017, 12766, 9520, 51106, 98867, 227798, 634, 8951, 16361, 45756, 34211, 71984, 20818, + 132852, 2771, 8039, 31695, 29917, 46819, 43140, 4911, 36076, 31350, 146547, 12832, 55352, 87682, 102259, 20181, + 65281, 125529, 76789, 74087, 129800, 20270, 210263, 10610, 41958, 199960, 136842, 106466, 18944, 91106, 109596, 45385, + 84678, 1826, 26239, 13942, 18580, 74780, 96474, 109106, 168093, 58817, 77576, 139329, 132621, 264328, 131918, 3004, + 39997, 277046, 10657, 188062, 54092, 9389, 208482, 138342, 27740, 112130, 141428, 159919, 32354, 78912, 10848, 248488, + 3770, 58675, 143326, 309222, 114118, 121341, 21213, 104326, 103242, 247904, 10251, 17363, 43918, 50107, 99756, 9914, + 15899, 1529, 57591, 10958, 18574, 191645, 27047, 33819, 145944, 50973, 10931, 515, 3190, 56895, 26750, 118357, + 123469, 6582, 232402, 185264, 219903, 14816, 104060, 2257, 24169, 2672, 34078, 85037, 100571, 54688, 18794, 11296, + 62403, 48268, 85546, 58594, 16776, 5744, 7336, 60494, 92779, 55136, 31725, 41292, 14535, 43085, 170495, 62016, + 17168, 20160, 13301, 31775, 16475, 23866, 68028, 7708, 14304, 27024, 412982, 50360, 37231, 31172, 8313, 18133, + 1712, 149874, 165108, 53176, 28218, 60812, 11433, 110651, 242754, 239758, 103026, 54413, 61688, 60159, 42554, 63279, + 28980, 26817, 24147, 239906, 1318, 94868, 66949, 9143, 51359, 25429, 40100, 150526, 222657, 6742, 15209, 35877, + 99200, 23325, 5454, 116860, 67989, 20127, 66101, 152710, 31772, 35795, 93237, 22499, 23932, 217156, 63347, 85225, + 35351, 44967, 43097, 63722, 201251, 232588, 42922, 76661, 49041, 51431, 91264, 26250, 27306, 16146, 171707, 8346, + 19128, 15883, 46818, 14147, 40135, 98017, 121209, 5750, 3986, 4803, 194061, 109091, 77333, 135407, 15726, 139517, + 19649, 240604, 296254, 18468, 120683, 261045, 15054, 49607, 151927, 90876, 33988, 94815, 21081, 4338, 3114, 25278, + 7076, 168237, 197598, 5263, 78375, 28858, 96610, 1656, 77501, 139877, 164638, 29453, 101599, 77549, 15067, 106520, + 8011, 191624, 67048, 47927, 89585, 15567, 16358, 40740, 96715, 19262, 3368, 257227, 58179, 82342, 164701, 77237, + 5378, 183488, 891, 51832, 102138, 1421, 126828, 72223, 106793, 152999, 9464, 38181, 26051, 369537, 47723, 127195, + 217276, 19701, 87052, 2899, 4197, 91519, 1222, 71232, 16297, 58674, 236388, 7640, 27014, 38262, 53246, 56437, + 46721, 45960, 154554, 93517, 13322, 57858, 23476, 62092, 22806, 29494, 60980, 59522, 5278, 152497, 10089, 42687, + 29629, 152505, 1415, 65715, 70632, 27834, 5144, 69061, 9647, 106305, 17509, 83636, 71519, 6066, 32864, 49020, + 58818, 62070, 83328, 51524, 40603, 27209, 14130, 13006, 128530, 34289, 191758, 175447, 8903, 6644, 258111, 1630, + 36770, 19694, 13099, 93412, 56900, 8695, 92739, 9361, 31483, 22727, 24402, 12783, 26935, 64112, 50162, 73354, + 43845, 97824, 38035, 43333, 34265, 4521, 118246, 36777, 34920, 27802, 62580, 75613, 87658, 36480, 37311, 41140, + 6114, 54859, 37921, 25174, 84767, 123137, 100067, 76401, 64250, 31608, 5782, 50390, 47318, 8602, 146787, 57571, + 50720, 184949, 26374, 49411, 19264, 59669, 72340, 61, 162514, 37868, 20961, 23322, 33357, 104140, 12370, 75813, + 13117, 173944, 10984, 8143, 6705, 185922, 4223, 25479, 46696, 78515, 10296, 173037, 75032, 3611, 146130, 87547, + 71054, 20284, 76208, 179291, 87004, 88099, 25915, 10612, 42035, 58634, 8935, 45162, 69138, 29960, 4954, 9827, + 11903, 75332, 189060, 145940, 99854, 68852, 2428, 35935, 102328, 55387, 95597, 31446, 19611, 66526, 25614, 75226, + 34525, 91178, 34821, 57507, 69312, 5965, 54613, 73567, 6431, 38968, 305485, 72661, 49842, 46229, 13066, 35961, + 101600, 6117, 6625, 94731, 133178, 5938, 14272, 71096, 108751, 55431, 8310, 74568, 41829, 15997, 69572, 115173, + 89939, 33817, 27169, 164256, 225664, 113979, 208705, 24886, 48916, 4745, 105265, 2285, 99556, 76020, 16691, 17685, + 9195, 67243, 29412, 15704, 130387, 12347, 87154, 6073, 5960, 22667, 45766, 126328, 36857, 50552, 563, 58566, + 116724, 31042, 63335, 55856, 53518, 24055, 44320, 132215, 94002, 7583, 31931, 161536, 39077, 14213, 37853, 130359, + 53996, 47829, 71470, 59662, 6945, 70919, 15101, 29373, 22261, 65721, 32624, 111143, 64291, 96324, 23614, 39737, + 8314, 9557, 132103, 65689, 152785, 54960, 122712, 73272, 57588, 3782, 8427, 101263, 16522, 3063, 1332, 9763, + 9091, 103598, 131811, 79941, 5048, 53350, 22985, 72314, 87671, 137942, 20832, 32726, 35441, 97544, 14838, 32661, + 39509, 25235, 12815, 229677, 1945, 18478, 104804, 53001, 12423, 99415, 90412, 47427, 158923, 19643, 66679, 33927, + 178413, 13851, 2378, 163553, 37549, 4735, 113147, 13151, 14095, 34868, 84260, 49719, 1475, 55719, 70677, 22297, + 73188, 7304, 101811, 35472, 62878, 110472, 31193, 7011, 4819, 44319, 37242, 22235, 24012, 34311, 34630, 8117, + 68534, 47855, 55542, 88606, 42606, 10807, 64311, 44304, 4010, 54889, 48956, 37274, 120809, 12117, 36576, 15154, + 57497, 55581, 76707, 88824, 59564, 29146, 75878, 346295, 101758, 137754, 84121, 153651, 103718, 85489, 61427, 59883, + 32701, 4692, 17006, 92343, 80116, 63122, 3829, 201429, 12345, 10158, 29504, 164968, 95834, 119755, 125824, 142638, + 46918, 23943, 90123, 20494, 21468, 29167, 157195, 24079, 53213, 49163, 19311, 138007, 15665, 149198, 44724, 55347, + 31200, 1573, 21255, 26337, 3867, 100570, 205427, 109262, 5140, 8979, 83224, 17644, 96013, 1279, 32509, 16380, + 250744, 103649, 111338, 4321, 21016, 68917, 10756, 39197, 10069, 10563, 184865, 35905, 13968, 1109, 7847, 19871, + 35449, 21656, 7996, 38626, 180829, 25293, 37599, 10356, 27683, 46005, 32258, 8111, 39704, 15702, 161889, 13627, + 59956, 21006, 29672, 64295, 22893, 319443, 755, 33239, 3115, 11630, 35242, 316161, 26293, 180051, 34293, 25262, + 32785, 45248, 4291, 1345, 75934, 380808, 185068, 5400, 62445, 95085, 113696, 37657, 141162, 2763, 1716, 31145, + 62720, 101394, 197152, 222158, 2018, 103950, 53054, 8291, 83638, 37618, 74005, 127265, 19949, 171632, 21168, 31182, + 114012, 109942, 16057, 103239, 95006, 48470, 141582, 50740, 3330, 57743, 91063, 68640, 99829, 25131, 192726, 1408, + 130935, 113922, 160076, 66999, 309272, 153746, 62089, 54683, 9565, 23036, 233538, 39614, 55874, 51238, 28998, 51475, + 121727, 56411, 33932, 53786, 37017, 49406, 91778, 26837, 23586, 252174, 2540, 47569, 319858, 177485, 2308, 5581, + 40970, 118880, 34878, 1602, 27602, 64100, 36001, 13488, 8625, 28038, 116561, 45356, 112329, 100, 159062, 48033, + 61060, 53312, 14278, 30173, 100088, 27580, 20456, 230013, 118525, 51822, 34883, 100756, 25922, 52426, 18317, 13881, + 16232, 8187, 317935, 69863, 1907, 53514, 75569, 36902, 60671, 5105, 60024, 76920, 51583, 106419, 20458, 110614, + 44553, 36111, 187025, 173919, 80993, 52249, 116521, 11851, 5262, 26289, 48960, 29999, 94679, 33367, 125032, 72126, + 8676, 211498, 44721, 235091, 940, 97176, 26565, 5948, 20736, 10278, 50485, 12407, 11823, 41971, 135546, 103878, + 3020, 21249, 253851, 97728, 16476, 7536, 49750, 6746, 12340, 94756, 71789, 16549, 152600, 40488, 30681, 120494, + 97416, 90981, 75736, 36235, 3703, 36522, 4051, 90148, 25744, 143233, 74114, 50674, 66826, 186399, 55544, 63905, + 16245, 17811, 43885, 57562, 16876, 12660, 11009, 92234, 46446, 722, 137637, 44043, 84798, 27042, 5314, 21312, + 74227, 10917, 92213, 211986, 176020, 52818, 44610, 174541, 45192, 85977, 66236, 78509, 61955, 14788, 256627, 12032, + 75496, 83996, 101534, 82235, 23946, 104883, 3183, 12850, 23626, 57697, 3225, 16042, 40372, 9541, 108356, 126495, + 26036, 101893, 24582, 39880, 6149, 95087, 46546, 59092, 11822, 168583, 82335, 18729, 30582, 146256, 6074, 39807, + 16541, 11986, 80148, 61456, 41914, 80721, 142480, 21004, 82385, 83655, 47670, 4769, 232823, 41862, 437909, 4116, + 40921, 59664, 133104, 38104, 80773, 101843, 38426, 90874, 14930, 55522, 12793, 23708, 3631, 8582, 3112, 415975, + 7517, 106586, 112390, 31555, 39619, 56075, 6299, 30930, 4348, 38188, 23437, 11888, 36180, 29057, 78844, 52556, + 126106, 24776, 65214, 92664, 138939, 39642, 153427, 17494, 62611, 31501, 49371, 27056, 1477, 16503, 156270, 23995, + 113512, 205238, 84709, 77316, 47321, 67623, 42436, 36548, 25052, 10369, 19122, 181758, 14546, 6743, 124696, 124095, + 118881, 58058, 29158, 24211, 29060, 38102, 144694, 42736, 23589, 64142, 131963, 43763, 128322, 42128, 13330, 35824, + 36795, 108915, 43897, 15657, 18401, 58900, 14806, 118110, 137921, 30097, 47026, 4142, 104699, 4185, 87711, 85997, + 267929, 153261, 228359, 20660, 36194, 54339, 43073, 5692, 172791, 8213, 26146, 5686, 18113, 28694, 17786, 77352, + 4766, 1852, 168140, 76409, 188215, 2179, 198971, 53244, 30083, 37124, 32195, 48123, 332586, 62934, 88005, 15880, + 94089, 68377, 87929, 133891, 5805, 11217, 130365, 104237, 77909, 44881, 6260, 14391, 22194, 42271, 49170, 261884, + 68234, 79361, 14141, 107542, 154976, 30425, 14602, 73251, 43220, 47978, 4990, 19457, 40660, 21608, 104477, 3582, + 70001, 38324, 113052, 201756, 184893, 168071, 1921, 43965, 138095, 106316, 100248, 3825, 11128, 115487, 18833, 101956, + 103010, 11802, 30806, 12615, 22663, 5074, 17663, 19426, 5108, 141373, 42930, 183720, 212615, 34077, 17051, 59686, + 8485, 22524, 8691, 78244, 5565, 124298, 80099, 217111, 49222, 20498, 66793, 529503, 54614, 4186, 75927, 40419, + 26530, 57883, 5327, 44876, 42639, 19706, 60003, 74433, 16319, 1599, 26748, 132682, 55062, 6955, 63969, 38461, + 152662, 43166, 12979, 233, 105595, 18768, 75949, 56729, 21114, 135004, 23948, 42101, 22216, 320051, 21698, 23811, + 10294, 826, 29297, 123324, 68158, 26264, 158791, 95573, 10436, 191222, 9993, 12298, 86950, 123108, 20858, 320830, + 7206, 182464, 11757, 81755, 62115, 101508, 64002, 77412, 3977, 47597, 13370, 37935, 3657, 20526, 27967, 53371, + 59874, 220318, 6962, 46594, 456, 88484, 47078, 50207, 118617, 53233, 59503, 10872, 18156, 12054, 11152, 77825, + 106663, 95945, 18093, 1644, 112229, 13310, 264515, 9070, 80992, 55835, 162270, 15035, 17442, 111426, 61158, 71574, + 8217, 10224, 82739, 45979, 55551, 15768, 183976, 64417, 5133, 165269, 38884, 710, 34344, 57175, 27784, 31790, + 251927, 69733, 22676, 125462, 51153, 73708, 53147, 16750, 39364, 77866, 28089, 2461, 201321, 1010, 4858, 82482, + 7816, 123565, 3065, 124019, 66803, 1441, 2105, 147722, 23057, 2238, 2465, 109286, 156724, 37091, 60022, 193667, + 145664, 52352, 15876, 275211, 14276, 101280, 2609, 1583, 95705, 18345, 47036, 168953, 979, 1876, 17511, 99269, + 2796, 7756, 45223, 209272, 6875, 13501, 19972, 3039, 13429, 88164, 40587, 48347, 212525, 23645, 107978, 39326, + 50602, 71127, 74343, 5725, 26276, 95436, 69705, 115587, 28284, 114395, 42685, 63375, 3424, 66319, 70412, 12400, + 19465, 68740, 40524, 83167, 52397, 11466, 284475, 42974, 46963, 1472, 38647, 63800, 31420, 5787, 41395, 21919, + 51399, 116728, 34891, 76404, 206476, 116758, 145291, 39684, 92317, 182072, 3771, 102030, 48851, 66664, 589915, 408336, + 637, 73775, 252578, 42675, 100820, 82225, 43433, 158753, 83349, 100070, 42160, 13599, 10317, 42154, 91152, 132379, + 60227, 237752, 99758, 270587, 21972, 37249, 93833, 30614, 6908, 73035, 2132, 42490, 282439, 134241, 47775, 14940, + 32857, 14647, 284209, 112665, 224767, 50372, 44452, 85605, 95629, 194385, 241460, 28172, 83882, 169721, 7505, 79753, + 42106, 57611, 175719, 33441, 87338, 1110, 81138, 125252, 69757, 29461, 19692, 61492, 29840, 67754, 2077, 4100, + 68709, 14021, 73781, 501, 37665, 25322, 62392, 106943, 45244, 60281, 4687, 224380, 114577, 31964, 38842, 6435, + 49188, 3526, 19117, 97765, 175943, 38531, 30032, 27448, 54009, 25775, 5789, 30779, 186746, 7022, 85999, 42272, + 106158, 428476, 5637, 191524, 70168, 172347, 5932, 69204, 3358, 12019, 9877, 55515, 234002, 64516, 10898, 36793, + 50555, 139948, 27732, 39934, 9221, 14752, 203772, 22375, 129338, 7278, 3245, 117137, 9562, 29787, 9323, 102610, + 20118, 12371, 39558, 8236, 32923, 70519, 19395, 17047, 78479, 97791, 16130, 89463, 118280, 22050, 37024, 89641, + 65752, 52765, 71810, 108442, 4977, 44545, 69952, 19070, 10474, 23484, 18908, 21788, 102174, 82995, 4717, 97497, + 60947, 27112, 3005, 36385, 129006, 15642, 16432, 25512, 10570, 43825, 108125, 73183, 83451, 75116, 95227, 44522, + 8598, 15990, 22094, 1787, 8078, 14629, 33317, 78398, 159367, 125840, 12669, 67807, 123785, 64349, 37447, 22738, + 80438, 10768, 15104, 98321, 16742, 22746, 11237, 41146, 5905, 13127, 47595, 35425, 5281, 12465, 54216, 37342, + 181513, 107706, 142857, 10267, 42402, 87816, 7247, 28472, 6977, 228601, 32966, 2076, 163136, 1044, 31531, 56439, + 93179, 59512, 313709, 36615, 42191, 80611, 121959, 23989, 14968, 38253, 2001, 194381, 50421, 88797, 45161, 11328, + 112401, 85555, 7543, 130762, 105440, 130853, 18911, 12575, 33456, 139214, 29318, 62849, 57347, 85786, 171698, 27142, + 121611, 1138, 80752, 54791, 4221, 77844, 47524, 140582, 94954, 18043, 313972, 120855, 36517, 63651, 18688, 30456, + 24046, 74888, 58460, 70311, 27796, 50699, 84011, 20285, 6255, 200303, 74910, 57724, 33394, 16958, 104867, 22244, + 72990, 7001, 57825, 20114, 135408, 43742, 54632, 30820, 116627, 34162, 194876, 27214, 1233, 2542, 89114, 33428, + 57874, 24432, 73244, 13342, 25654, 36812, 42775, 137065, 95419, 3034, 109640, 69998, 68156, 15470, 40930, 43371, + 401399, 12617, 9352, 26808, 313338, 55155, 36975, 17890, 55208, 52186, 226992, 16324, 45573, 22248, 14465, 65809, + 93124, 76501, 130378, 8533, 119251, 28580, 43089, 52170, 47200, 44870, 76202, 54398, 38196, 30768, 33917, 36165, + 11909, 25697, 353546, 54562, 130667, 372979, 149630, 9125, 45391, 144756, 57973, 19599, 73904, 2645, 258411, 25268, + 64964, 61348, 128044, 132867, 167846, 141414, 52489, 108908, 4137, 41282, 39617, 33797, 115606, 493331, 11554, 30049, + 52036, 71190, 77943, 31602, 62214, 2239, 12602, 19146, 7969, 18428, 131922, 852, 160925, 6259, 23383, 165390, + 7187, 12808, 189017, 142381, 1132, 41841, 18701, 29043, 134835, 99, 15791, 9068, 40309, 98444, 127461, 27332, + 73195, 112064, 5097, 39453, 64494, 72450, 24011, 7695, 80472, 162172, 41084, 177912, 444841, 55640, 51858, 96982, + 61111, 3710, 41637, 100576, 26500, 116386, 75146, 6890, 45323, 42246, 54944, 26323, 40743, 9228, 36712, 134278, + 53625, 8230, 40754, 28377, 52797, 241804, 26552, 261558, 22659, 53109, 11974, 49532, 15631, 300307, 18096, 92, + 29739, 107565, 55632, 22306, 36706, 120177, 123369, 52145, 28841, 81811, 2967, 6374, 39147, 48191, 58003, 205210, + 102836, 34591, 52967, 14473, 26794, 120917, 61270, 53041, 10536, 41117, 108058, 363097, 14845, 27166, 219697, 10564, + 87305, 3185, 20437, 15052, 45874, 8849, 48484, 35021, 12241, 1310, 87883, 136400, 127587, 39626, 11562, 40667, + 83833, 19748, 78478, 3152, 57183, 48701, 22123, 86751, 79722, 116436, 5100, 237306, 30844, 5999, 191075, 65796, + 41304, 91788, 24098, 149740, 84655, 17286, 186934, 44080, 20825, 86063, 13887, 29758, 92500, 168321, 114890, 64965, + 3722, 47787, 198506, 43466, 25655, 5407, 13240, 101325, 27811, 1251, 36859, 37980, 10157, 135970, 13500, 104703, + 81634, 106088, 4011, 23578, 31362, 89054, 38594, 186633, 34088, 336505, 15479, 9229, 92487, 49521, 16527, 53016, + 70123, 18389, 75248, 16479, 22190, 140271, 80094, 10382, 185100, 9051, 71918, 40905, 72658, 25068, 29185, 15832, + 139035, 62499, 80063, 154077, 192523, 94489, 96542, 73906, 88241, 73266, 64690, 151133, 2078, 76322, 24201, 11290, + 230490, 211312, 36153, 103, 44528, 9539, 36954, 4068, 85638, 788, 127939, 83761, 100198, 46098, 285530, 41414, + 22088, 50928, 15371, 56983, 29982, 53638, 112051, 38452, 291233, 156894, 16748, 48674, 241062, 61341, 921, 24898, + 13865, 8071, 235453, 118716, 4445, 177183, 19708, 21189, 137791, 14558, 145674, 60895, 37835, 46685, 36314, 3038, + 107218, 25232, 61381, 204828, 31726, 43310, 143414, 32798, 19718, 11254, 127092, 88407, 38234, 37863, 143492, 19751, + 72528, 6573, 60076, 8937, 23046, 86273, 12926, 43848, 19177, 134938, 18033, 102451, 66695, 155909, 57463, 34776, + 5109, 25338, 58091, 2353, 17251, 3298, 36065, 271, 28077, 50946, 7379, 25203, 5617, 135531, 5335, 33853, + 21554, 1514, 322875, 85207, 47839, 81931, 162589, 73080, 72425, 41666, 2205, 107217, 133825, 13839, 41951, 21025, + 1486, 3869, 68800, 11775, 73065, 13658, 35920, 41391, 181275, 74450, 84216, 8800, 141508, 82895, 22465, 8542, + 21768, 13000, 8240, 5971, 62971, 23798, 245635, 47169, 63082, 25334, 10884, 141547, 2512, 44744, 8650, 42190, + 34200, 155357, 119815, 26059, 9904, 121764, 22752, 11476, 120309, 20694, 128696, 2930, 6392, 69057, 11264, 13880, + 91243, 58841, 35178, 12534, 68416, 70963, 20428, 809, 268253, 4190, 31257, 66625, 41199, 52789, 24931, 112688, + 116757, 6355, 182513, 90872, 138551, 2904, 50394, 19487, 185526, 10604, 30792, 10910, 41246, 165026, 42224, 113107, + 28986, 50164, 19196, 60791, 4093, 348976, 132032, 123165, 19057, 48820, 65522, 51163, 17295, 95175, 4484, 1308, + 4148, 239196, 12160, 143978, 245766, 14112, 90855, 4531, 122360, 139905, 44798, 12441, 35356, 12686, 74598, 10815, + 112075, 44675, 49095, 131228, 20301, 124634, 94533, 9419, 75441, 56741, 243625, 2690, 10998, 35598, 155583, 1231, + 7977, 46730, 242597, 113005, 19769, 157864, 288814, 15932, 62922, 81227, 227889, 46275, 937, 11294, 103072, 3033, + 63547, 8861, 201131, 23991, 100196, 49148, 38402, 10013, 26427, 1584, 47267, 32310, 157820, 36321, 1694, 31226, + 20983, 78184, 30061, 128167, 236696, 33413, 66741, 24836, 22935, 47910, 35635, 2678, 8140, 12435, 14911, 265634, + 90315, 43108, 109685, 72546, 156004, 8866, 37002, 25727, 47204, 51301, 8652, 23492, 140973, 93561, 41906, 95949, + 7726, 23391, 15506, 42212, 45097, 63870, 38047, 35887, 52725, 285299, 46733, 43623, 22636, 21753, 30231, 2194, + 23436, 125735, 107590, 55499, 257282, 66930, 21546, 157729, 105247, 1991, 14483, 140680, 522, 45784, 342095, 166370, + 88389, 122072, 21358, 24129, 216031, 54089, 2437, 107595, 202204, 163607, 26303, 41895, 46812, 40542, 78707, 2821, + 211666, 62835, 14082, 13734, 19693, 1603, 38978, 130765, 68828, 24577, 173255, 173341, 81691, 26679, 29930, 3426, + 45925, 32463, 13450, 24567, 11256, 19340, 65455, 7397, 30292, 9441, 111484, 57305, 372, 668, 19439, 90961, + 5236, 16883, 27753, 170633, 167826, 153144, 144689, 113366, 88328, 50765, 15108, 221902, 232776, 9372, 46450, 174146, + 151611, 40736, 105766, 1040, 5360, 65342, 48072, 55757, 82104, 206605, 5190, 78870, 18841, 118613, 78897, 18174, + 80393, 27669, 110040, 158621, 25465, 12309, 35524, 25100, 18285, 9967, 2817, 4292, 20320, 37594, 50630, 17986, + 72377, 87322, 63433, 19730, 31730, 51337, 57653, 20420, 33160, 147371, 43603, 49870, 45803, 188912, 85055, 11824, + 38715, 173010, 66391, 15353, 27705, 31430, 7938, 120390, 37379, 123726, 25452, 345, 24163, 78121, 86371, 105015, + 18360, 52480, 28116, 10182, 103586, 12625, 43431, 22706, 4015, 110996, 23635, 211386, 32305, 9669, 5607, 79602, + 269494, 361, 52000, 75409, 91252, 16133, 217163, 25649, 20080, 40640, 108653, 52740, 36567, 24900, 6769, 93887, + 54650, 108271, 21897, 49233, 7797, 97128, 54633, 281648, 57073, 36934, 16265, 167589, 12650, 209086, 62181, 5542, + 31164, 122349, 81815, 67651, 42209, 38682, 31383, 122219, 6375, 11705, 72211, 22777, 261663, 21772, 69350, 85211, + 105528, 83301, 31254, 16229, 81661, 5138, 1814, 117287, 106002, 97442, 104335, 3000, 2800, 69084, 40361, 68511, + 5375, 11262, 76099, 146464, 17247, 106291, 8526, 92034, 43151, 22579, 624, 893, 4442, 9159, 34683, 63801, + 15727, 54296, 36030, 7741, 194619, 26966, 57386, 521, 100855, 29101, 4249, 27495, 144898, 122045, 157745, 29287, + 62320, 13800, 162790, 170925, 78465, 1720, 58206, 24580, 39929, 46731, 7089, 342751, 16454, 254942, 25844, 55945, + 1967, 64379, 6774, 82424, 28311, 43620, 50135, 58126, 61363, 144665, 15325, 125247, 17219, 113505, 10215, 205466, + 9395, 52783, 670, 169135, 8745, 37048, 58689, 91650, 121445, 22644, 27467, 23132, 76939, 233859, 30141, 24917, + 80385, 41981, 8292, 35986, 162380, 57998, 43320, 212379, 22009, 3420, 17253, 49172, 54191, 105827, 25802, 3604, + 44248, 112875, 6127, 145960, 16299, 26569, 47356, 14611, 122830, 30067, 21003, 192364, 48151, 121965, 327180, 5291, + 74429, 11440, 101228, 65667, 78291, 16492, 12720, 3052, 64755, 138800, 114706, 157348, 14238, 155607, 17452, 62457, + 44966, 41313, 98226, 78628, 2511, 99734, 74253, 133560, 17712, 1897, 74082, 2056, 67954, 146843, 392522, 79571, + 93583, 59314, 13047, 43084, 829, 117157, 13262, 38703, 105899, 54574, 104172, 22161, 49935, 96314, 98235, 13239, + 84750, 53580, 34154, 114889, 11591, 72967, 66707, 104862, 33185, 16902, 22355, 42766, 85447, 36865, 165090, 84531, + 42717, 343264, 93849, 81105, 27409, 214283, 113392, 12987, 208542, 45776, 2947, 83062, 28965, 25376, 24971, 20612, + 62052, 26121, 83563, 52492, 52525, 3766, 104113, 40486, 5597, 125, 87057, 6525, 25694, 6096, 199883, 183361, + 65594, 40734, 5880, 182733, 16343, 16696, 86236, 796, 63224, 7501, 54031, 26465, 276188, 66868, 2954, 84925, + 12475, 2514, 223628, 75284, 9331, 78770, 22694, 89261, 127507, 56323, 111184, 138073, 38522, 10128, 31041, 55066, + 57287, 41322, 157136, 126272, 24128, 75696, 37221, 12395, 133161, 60386, 24760, 6499, 79723, 24499, 76440, 34317, + 105548, 60338, 146011, 210310, 133695, 189639, 390311, 10417, 48917, 77547, 25558, 125695, 27558, 50249, 50758, 54053, + 43278, 12651, 41946, 33000, 46520, 10764, 46950, 144684, 13778, 1185, 25117, 155135, 141954, 96252, 124109, 34349, + 110785, 24351, 73685, 9493, 83366, 25352, 100253, 91513, 17715, 18369, 120888, 58948, 46317, 59607, 74045, 40939, + 105763, 18522, 3886, 43064, 66298, 18918, 33778, 108305, 147013, 68203, 169050, 212793, 41086, 17383, 9620, 80428, + 94180, 46324, 90898, 384293, 16478, 2162, 57856, 4565, 220447, 72458, 27818, 12844, 44611, 11585, 8997, 5614, + 730, 93897, 76702, 124308, 19722, 10289, 81775, 44327, 78975, 156207, 11289, 31827, 117889, 212382, 163177, 125496, + 125643, 46366, 29130, 40665, 26254, 99947, 10510, 84091, 16574, 120313, 104829, 138305, 18480, 16829, 8176, 17121, + 65006, 22646, 16279, 64878, 15806, 31922, 5544, 11868, 38549, 32569, 219914, 15814, 246418, 24003, 289255, 313945, + 46052, 157270, 10005, 11234, 36056, 153288, 35135, 63031, 8440, 1275, 43404, 298391, 34984, 44385, 45317, 14880, + 30170, 5875, 12546, 45344, 3163, 4914, 1779, 66305, 59800, 72227, 6487, 36265, 4458, 73948, 78254, 47797, + 115442, 39130, 71796, 11449, 4283, 30975, 181777, 25982, 41970, 19821, 110322, 41466, 33507, 69754, 31950, 489525, + 104078, 4158, 69239, 85214, 1653, 56217, 34223, 9944, 22, 35473, 131427, 25058, 121158, 166086, 254762, 9745, + 276486, 4120, 33379, 30250, 3655, 10419, 70458, 24518, 6338, 80002, 3419, 4757, 24048, 2139, 938, 210466, + 133421, 8823, 35575, 1574, 23641, 94423, 17694, 113822, 2161, 40833, 49449, 5625, 24422, 131600, 516, 97149, + 36006, 24910, 111249, 104788, 8086, 63142, 17790, 49461, 10675, 30015, 25712, 12492, 181474, 43374, 19331, 39246, + 12307, 170327, 251360, 85956, 29514, 4826, 115208, 41680, 59143, 108007, 189711, 48650, 14729, 28727, 61638, 233522, + 52509, 9807, 83278, 43091, 87128, 205247, 225046, 135671, 122470, 46278, 118683, 70557, 19446, 17394, 5920, 32166, + 80852, 8067, 86203, 410557, 33314, 48904, 140515, 16642, 24573, 28653, 11481, 199433, 119864, 5958, 55298, 63357, + 14237, 29162, 73299, 12246, 9652, 101149, 98618, 9611, 57779, 9592, 43988, 1014, 6612, 44197, 23629, 95201, + 51851, 69205, 94416, 985, 15284, 36462, 143673, 5555, 98871, 71794, 26730, 9761, 90581, 198325, 133604, 57541, + 124466, 1970, 23017, 51130, 156831, 80242, 24852, 47760, 21190, 76629, 13862, 175502, 22015, 38086, 18855, 52006, + 71380, 1872, 6332, 88866, 161906, 10811, 92768, 111004, 87247, 58129, 1244, 4815, 69201, 134735, 125070, 78458, + 18392, 146150, 4886, 66816, 17908, 31646, 6778, 69889, 108470, 70270, 87563, 7366, 72962, 13622, 24210, 28614, + 40719, 40963, 61900, 8988, 14338, 191234, 63447, 35774, 17911, 12734, 257831, 101714, 95260, 78005, 17582, 88301, + 43339, 8921, 3918, 82471, 20610, 84864, 253574, 21526, 78916, 58506, 1435, 433170, 20710, 147863, 28259, 4548, + 72451, 2742, 1878, 156795, 11315, 75234, 32104, 46510, 31448, 15513, 60382, 37929, 17263, 142357, 16994, 34753, + 58853, 23100, 69236, 5182, 178878, 2545, 32116, 152276, 48111, 71595, 81171, 93964, 116002, 57412, 4245, 19877, + 45497, 156962, 37019, 27777, 80506, 87318, 64770, 13895, 82605, 511, 8366, 87800, 85880, 42605, 80454, 41603, + 36300, 27404, 35498, 82743, 121755, 128166, 1871, 81847, 25215, 5995, 60337, 177660, 36118, 54831, 138197, 126290, + 301929, 182977, 7852, 68390, 88728, 111588, 58683, 115820, 405223, 74730, 36671, 37592, 276136, 25471, 6538, 61648, + 553, 89532, 101905, 27590, 34704, 25370, 42539, 28362, 212438, 98439, 26905, 4804, 49970, 60393, 7138, 33248, + 78329, 11555, 51340, 1796, 922, 17391, 17952, 3534, 20711, 34703, 17977, 21148, 25036, 13075, 6201, 46397, + 257130, 73731, 213188, 107033, 38295, 56744, 112539, 122324, 145369, 82762, 25343, 6279, 18128, 140444, 35989, 4474, + 15385, 14399, 63732, 21041, 30829, 98387, 31090, 23985, 55656, 40284, 132675, 4230, 48345, 64072, 15739, 73537, + 8012, 23754, 107906, 9060, 3561, 183232, 19339, 47011, 28004, 85175, 46305, 37773, 122041, 123221, 100918, 79448, + 192900, 143005, 63829, 123166, 58338, 37779, 33298, 109415, 112508, 115553, 68473, 184384, 41085, 82184, 61692, 70292, + 29976, 115765, 134590, 89398, 87040, 39038, 8330, 21348, 47117, 350, 39878, 167810, 23905, 28058, 42971, 15124, + 4336, 12612, 11230, 18966, 92061, 59408, 12185, 128370, 138880, 27612, 27335, 49677, 97407, 61794, 36678, 60848, + 42083, 7518, 89496, 57735, 172121, 74451, 85960, 144252, 6256, 110791, 28888, 21551, 25192, 5388, 80308, 1657, + 172671, 42290, 48, 195157, 5, 36476, 3307, 89171, 93568, 157177, 7894, 66766, 1420, 14058, 167637, 30649, + 12677, 121794, 69021, 13231, 31605, 230408, 111760, 67720, 56743, 31038, 52660, 61746, 40620, 81659, 12864, 14180, + 6015, 215868, 90084, 141993, 78415, 116175, 52441, 7100, 231077, 112914, 39586, 51204, 31298, 52736, 64704, 3584, + 80026, 74802, 13972, 215480, 13902, 93110, 20076, 42335, 19048, 41860, 36983, 169990, 24924, 34272, 70434, 20851, + 170586, 12670, 115383, 39041, 32955, 71225, 30362, 30667, 176119, 19860, 10392, 48818, 87859, 7042, 111918, 36751, + 36731, 38144, 156426, 19519, 6773, 997, 29515, 53787, 27711, 72672, 159500, 51584, 24658, 40226, 1920, 70951, + 26475, 36713, 58252, 424939, 115216, 30508, 35252, 34310, 133207, 17198, 8490, 5530, 93250, 121485, 122775, 66308, + 95820, 59227, 108938, 53397, 88522, 6280, 46904, 14869, 8317, 25024, 14413, 98196, 5714, 54882, 8596, 43570, + 124047, 5657, 302052, 35, 55219, 105769, 57203, 241055, 86860, 6086, 121752, 2426, 19677, 79643, 9137, 36087, + 23961, 35741, 73879, 95404, 22928, 151374, 193172, 7078, 162209, 225418, 143148, 102355, 8904, 2279, 71928, 20953, + 225992, 15275, 49711, 9805, 359835, 337011, 5747, 97478, 56084, 49629, 13712, 164652, 96201, 13261, 69730, 256157, + 29392, 154079, 57395, 17417, 96558, 76159, 89103, 154068, 86071, 1481, 55845, 81677, 93643, 5966, 26830, 128209, + 55114, 215629, 46997, 96220, 13347, 6748, 42089, 26362, 8183, 87721, 60802, 344, 95129, 74756, 31533, 58592, + 82012, 86256, 21385, 21021, 2017, 92952, 1400, 17103, 123336, 48961, 24557, 31513, 34219, 94330, 102993, 33899, + 115554, 104210, 45821, 24373, 157159, 41476, 17357, 64836, 47747, 20243, 42746, 100702, 101684, 123185, 46219, 68593, + 41008, 135956, 132526, 17386, 18735, 62966, 27255, 23471, 193781, 89150, 2616, 57853, 104151, 97883, 5292, 181287, + 226906, 114560, 56557, 82841, 7552, 61282, 26131, 1854, 179874, 74127, 60152, 17376, 124113, 21668, 38821, 14260, + 31159, 29704, 82096, 204953, 21162, 64569, 11540, 37899, 44010, 79498, 50023, 39667, 14771, 235800, 15254, 382134, + 51268, 23240, 7289, 52385, 166128, 26710, 362, 239, 31382, 28658, 33227, 60344, 73124, 109004, 15966, 14103, + 77438, 65958, 13394, 9762, 92830, 27896, 85690, 1773, 205709, 14225, 11061, 49451, 12113, 15690, 22771, 34399, + 1292, 19355, 16900, 29634, 38937, 103120, 45588, 32502, 13114, 67382, 21201, 255605, 1334, 78137, 14683, 11960, + 2118, 39992, 111625, 158751, 15597, 47752, 3544, 123201, 69581, 8045, 25346, 232034, 14449, 70422, 61526, 29243, + 21934, 152079, 39735, 29007, 76618, 18195, 153, 27890, 48728, 123899, 68311, 48831, 67038, 51326, 21747, 8692, + 14967, 4922, 69709, 15204, 51495, 12889, 28173, 172579, 24243, 12645, 53481, 33706, 87736, 191077, 102314, 17799, + 147249, 101629, 161049, 4798, 26720, 45298, 89381, 76472, 11119, 5522, 59583, 86712, 46063, 85661, 57137, 135132, + 43749, 6257, 4316, 44510, 5843, 232177, 211804, 97913, 44147, 11778, 5876, 129172, 152629, 43931, 5404, 124003, + 133428, 97102, 297704, 57882, 65703, 24717, 67411, 23646, 14269, 13007, 172728, 4591, 45604, 57195, 12344, 102301, + 57982, 61997, 95626, 254590, 28672, 129544, 121196, 14934, 55616, 30754, 102467, 25644, 45957, 41766, 105011, 59359, + 8438, 80296, 37663, 282854, 95433, 8167, 10263, 65749, 37698, 56099, 237283, 25218, 220862, 42641, 90021, 45007, + 132034, 144203, 26155, 333670, 39456, 63723, 27049, 39050, 61870, 153573, 33337, 28349, 4161, 99154, 19089, 5689, + 26501, 109719, 53252, 38261, 73560, 103696, 77848, 61508, 56418, 8146, 14836, 30856, 9845, 7946, 131232, 127176, + 4654, 201895, 63780, 158964, 20916, 91453, 54086, 43072, 10456, 54618, 169463, 46325, 88920, 26447, 165915, 26064, + 119358, 76658, 11753, 55625, 9015, 143112, 11520, 269278, 65931, 121236, 179251, 9829, 96507, 5826, 140198, 247937, + 48029, 31061, 53953, 19018, 38534, 95073, 117331, 37745, 21676, 22041, 2439, 21017, 109081, 12120, 58943, 64119, + 43078, 113509, 30640, 37723, 34943, 350151, 79862, 143849, 25089, 16375, 77573, 16298, 6131, 168435, 40662, 50028, + 28766, 133195, 55258, 45544, 23665, 5135, 54199, 18072, 5477, 69501, 106176, 37245, 10255, 33562, 9039, 7013, + 16695, 29965, 30540, 106269, 67, 100273, 20386, 1936, 45778, 12653, 4617, 22300, 42443, 22525, 113152, 24837, + 42770, 66959, 15725, 52734, 29534, 17022, 9388, 334890, 23733, 206600, 71568, 50746, 100513, 18013, 100834, 84319, + 62617, 6595, 63101, 185162, 42630, 76641, 44106, 8165, 48746, 35407, 72152, 6974, 14191, 19580, 30414, 39009, + 43753, 63797, 66647, 169610, 50295, 55790, 16670, 533, 26007, 9475, 35597, 110160, 8792, 8536, 3652, 9896, + 57243, 120506, 37648, 15821, 43119, 131495, 35035, 35955, 54725, 31031, 169396, 210089, 164253, 337858, 11412, 77625, + 58250, 28866, 7134, 37720, 112304, 27676, 81211, 65246, 131796, 9378, 94506, 130705, 25165, 11403, 69938, 129091, + 4651, 244698, 53305, 17335, 3188, 92793, 26039, 26725, 24831, 59363, 5282, 15758, 47748, 53877, 45181, 5916, + 3705, 107447, 7016, 107200, 19540, 12998, 120359, 9387, 13211, 7466, 190758, 9392, 102095, 49540, 128418, 188973, + 5593, 50901, 14566, 196318, 18699, 54825, 76278, 15395, 23666, 36000, 88985, 17589, 32005, 23087, 139968, 578970, + 117571, 145460, 7486, 3620, 33541, 117754, 26398, 27210, 60584, 18889, 20981, 1633, 74573, 38616, 14563, 6638, + 86311, 86421, 25557, 42564, 99443, 36130, 97953, 1966, 25172, 83868, 86024, 136991, 27222, 67124, 48935, 48573, + 168938, 36866, 83414, 114109, 7143, 95340, 16828, 116402, 11853, 106392, 138070, 12698, 53560, 1736, 35550, 51146, + 18834, 22574, 37375, 59652, 19960, 32431, 133520, 38384, 86522, 43137, 73914, 52482, 28217, 63002, 68351, 25347, + 53266, 74606, 37238, 64254, 117700, 2458, 69113, 78847, 72989, 4146, 51993, 68944, 34323, 36106, 26991, 208463, + 18721, 68637, 46037, 28177, 66450, 253789, 306984, 98512, 34346, 8717, 62376, 80238, 74056, 18246, 43446, 108106, + 47217, 76264, 2682, 95684, 202002, 20293, 22279, 7215, 46269, 5998, 50165, 12049, 9429, 33348, 73125, 27380, + 68582, 110884, 139796, 3350, 75458, 44810, 10607, 55791, 37823, 11401, 91589, 124148, 82843, 54499, 20716, 21192, + 96652, 231365, 33208, 41522, 32549, 31981, 17453, 40828, 145144, 39135, 25049, 9457, 27958, 103347, 32810, 43385, + 19820, 53369, 126180, 23951, 158086, 100187, 144947, 109980, 31955, 45450, 139714, 69089, 201406, 73593, 53985, 61675, + 135379, 20004, 20095, 20957, 31207, 58416, 82105, 5163, 192545, 15375, 4004, 14708, 12950, 3007, 33958, 201104, + 51704, 2284, 38239, 215421, 9094, 170678, 68181, 18112, 248263, 264771, 5839, 65767, 76147, 15661, 13266, 123439, + 64028, 57174, 54342, 51589, 110009, 117919, 35421, 20125, 79407, 23467, 26562, 86760, 89345, 56253, 2635, 17366, + 99284, 45433, 65377, 5392, 223492, 45675, 94215, 41399, 47966, 42373, 88171, 10577, 26848, 109297, 198937, 27318, + 15359, 54014, 189688, 14526, 201137, 11181, 412, 4944, 2861, 25417, 41460, 9506, 110507, 60409, 42693, 128247, + 71231, 35793, 106673, 70206, 72297, 6817, 45319, 20585, 31851, 34828, 8363, 13868, 118777, 52262, 102674, 38674, + 71039, 6463, 77753, 62309, 151051, 2673, 41364, 138637, 240855, 165918, 128697, 37821, 16333, 64502, 32568, 39824, + 50766, 20504, 14159, 65654, 14727, 29996, 6383, 42332, 7939, 14308, 8137, 87581, 4149, 88311, 165279, 7060, + 80908, 49103, 28053, 140076, 418780, 65632, 172843, 34187, 88378, 29997, 545, 51172, 59276, 68688, 2146, 123301, + 1327, 93829, 16449, 152910, 7284, 87749, 23351, 3595, 38576, 26223, 23401, 96146, 79814, 32323, 172636, 114120, + 65820, 86539, 208708, 76711, 42199, 188268, 14011, 15148, 84860, 5832, 11441, 25143, 49574, 6953, 65399, 115759, + 62596, 134341, 14187, 18034, 12396, 16855, 108734, 25950, 70598, 14918, 2364, 136070, 40117, 153617, 71320, 115693, + 8648, 35391, 90995, 4541, 7994, 124212, 8041, 66856, 16836, 11191, 127474, 68712, 7630, 145631, 110314, 22066, + 14047, 1763, 167187, 174630, 359699, 151667, 4177, 41323, 106878, 1793, 50733, 69877, 525, 43923, 4828, 82891, + 29037, 171929, 324595, 65899, 28064, 91271, 65021, 37989, 13380, 31251, 2943, 407325, 11675, 18588, 38513, 8392, + 50669, 90077, 18495, 9450, 74216, 88387, 45547, 81293, 103539, 10795, 31036, 15610, 180314, 21332, 14365, 65897, + 27449, 55756, 149067, 98485, 56299, 40269, 32366, 5250, 172344, 23410, 92231, 18561, 19274, 44347, 122063, 8423, + 7301, 50878, 28326, 38202, 246099, 51058, 26017, 51056, 32043, 22795, 132375, 6943, 19422, 300889, 9098, 15040, + 36506, 86225, 57465, 49440, 129317, 12410, 39803, 48164, 6806, 32451, 82234, 132656, 30140, 15444, 60069, 3760, + 4614, 25897, 159, 28083, 46639, 99936, 101909, 30904, 66926, 173983, 34220, 17421, 932, 21450, 21043, 113018, + 86600, 42052, 132088, 128256, 6322, 29, 124274, 99295, 27847, 33188, 30130, 20462, 233103, 11629, 82802, 23282, + 10541, 47224, 44501, 68401, 39025, 9673, 4555, 49397, 34887, 30110, 111225, 33589, 3517, 74773, 153162, 143852, + 12972, 3530, 24385, 31076, 26220, 32573, 109457, 43144, 2031, 300519, 129953, 9238, 66561, 196911, 99752, 28368, + 115015, 174339, 10996, 8970, 48658, 26284, 10285, 694, 47596, 445399, 13275, 71937, 12714, 3528, 3654, 55811, + 33845, 269037, 118340, 11397, 3893, 231593, 42203, 2159, 16165, 12556, 48781, 72601, 35237, 68887, 18601, 17941, + 89983, 98696, 97413, 15006, 14769, 128317, 13607, 7426, 11962, 244737, 72660, 28493, 147224, 62322, 6982, 534, + 47018, 29199, 6388, 175620, 29977, 6018, 54235, 144119, 27979, 74453, 23072, 144966, 5552, 22680, 20439, 8839, + 82338, 13010, 108618, 484, 86023, 58829, 612076, 306318, 131368, 51558, 106387, 22146, 1218, 11904, 270917, 87286, + 24853, 275508, 76852, 5463, 237840, 82183, 83602, 44537, 132193, 104954, 15511, 29147, 15455, 20088, 13624, 153696, + 40873, 90309, 158996, 18756, 3668, 32089, 12462, 41486, 65351, 2207, 126590, 223126, 53388, 4077, 30070, 25994, + 15229, 66181, 63714, 79318, 59889, 160619, 15153, 12456, 272245, 42542, 24758, 47453, 47934, 65558, 28145, 13413, + 11858, 21191, 27021, 92095, 34347, 32570, 60556, 197324, 18038, 189522, 6812, 28247, 90853, 8634, 180039, 7329, + 86981, 148519, 9289, 4888, 300602, 74523, 9708, 49271, 19343, 63426, 74637, 174896, 114181, 28008, 26788, 8747, + 29362, 99695, 52578, 19976, 84921, 101587, 24035, 38033, 6095, 92547, 36135, 72547, 106059, 4207, 181943, 47905, + 79472, 24101, 58847, 20037, 38015, 46536, 22348, 109208, 1206, 45622, 37915, 26165, 48741, 7232, 1005, 4065, + 6208, 18528, 151721, 62784, 80000, 18897, 5854, 32564, 21916, 4912, 34596, 201975, 17423, 134488, 54506, 154143, + 6002, 72868, 7520, 36987, 108083, 112937, 75656, 32044, 24479, 15432, 40091, 58495, 34931, 9602, 16762, 2442, + 56661, 153348, 22812, 98811, 9511, 56184, 1409, 9718, 26995, 197, 49218, 167694, 100694, 44775, 53809, 6725, + 163853, 45609, 73, 98613, 35997, 111306, 20357, 36132, 81254, 14735, 26511, 23223, 58321, 51715, 70878, 210120, + 18919, 64042, 68936, 3280, 171890, 120028, 29382, 125765, 86877, 48327, 105142, 9969, 91341, 1198, 32748, 184452, + 74503, 188311, 34295, 291694, 70477, 184873, 41972, 33654, 53412, 208288, 120160, 47130, 7027, 83694, 30556, 99012, + 59281, 43841, 119720, 74947, 39892, 41874, 34808, 25853, 131302, 33590, 193671, 96415, 5864, 20797, 100412, 35136, + 15947, 13143, 28210, 28717, 61301, 5772, 22132, 12722, 67466, 83494, 128102, 176810, 162369, 37658, 11958, 19685, + 47956, 184482, 37093, 126586, 27874, 17054, 70824, 29354, 35624, 31284, 22117, 80846, 282324, 89289, 5812, 73290, + 21270, 10973, 15681, 37548, 111847, 5570, 82, 33861, 102548, 16344, 48551, 72401, 41482, 3442, 126293, 65750, + 30955, 205735, 76092, 105960, 116737, 54740, 25948, 8274, 28264, 111716, 110866, 252362, 8592, 112975, 10016, 89336, + 55458, 589, 60295, 9209, 22301, 101479, 32825, 64171, 75090, 106896, 21619, 26306, 29821, 30637, 132744, 10929, + 30697, 32956, 20307, 87608, 51709, 317395, 23678, 60949, 3041, 29345, 83581, 57276, 19208, 37802, 184528, 147377, + 8038, 29282, 6679, 77158, 24634, 288107, 38217, 32048, 30467, 20308, 11907, 18988, 87509, 32192, 89058, 11561, + 126428, 56272, 20368, 65338, 19389, 104074, 29008, 8049, 18814, 30607, 339, 30562, 152686, 25435, 16446, 78067, + 20701, 122568, 3475, 39655, 83474, 29081, 12004, 116638, 45832, 69977, 107214, 46129, 80891, 11087, 68939, 167121, + 105808, 42070, 117480, 33702, 11378, 25602, 124846, 110381, 153223, 6672, 47709, 80371, 120770, 144580, 11225, 22744, + 98186, 104427, 63765, 10261, 150633, 71276, 51714, 50281, 49838, 190522, 8987, 235791, 9141, 340331, 86198, 17567, + 12755, 210134, 6552, 101752, 30962, 11267, 2982, 8706, 5260, 70113, 110165, 127201, 74490, 93235, 145772, 35288, + 21256, 25014, 2694, 10842, 31678, 81199, 6436, 125138, 65062, 15308, 22318, 40788, 33326, 9604, 145295, 6946, + 289838, 90308, 57283, 326774, 187831, 2998, 288064, 53373, 20595, 188251, 90664, 58927, 89768, 31693, 379534, 13903, + 2805, 2413, 50298, 47087, 58535, 56981, 59799, 135588, 10844, 74455, 88188, 208971, 70085, 92510, 18541, 33920, + 12090, 24555, 11134, 23200, 2451, 6275, 135010, 5521, 138068, 6562, 161302, 44283, 98544, 214408, 65576, 50058, + 24461, 33687, 92862, 93762, 4511, 119896, 3685, 41519, 6754, 9529, 39394, 26959, 41684, 51727, 24271, 311732, + 28203, 45748, 149989, 111355, 3383, 25313, 20209, 43668, 65355, 32723, 38554, 67434, 82833, 42676, 66416, 25724, + 30161, 223, 65165, 45436, 83924, 12283, 5232, 29830, 234361, 377903, 56, 40022, 128424, 12472, 60521, 234629, + 28921, 22232, 168965, 36999, 222594, 73040, 24600, 9682, 33975, 51080, 29219, 59847, 125491, 30202, 38650, 136512, + 34069, 20533, 23257, 105963, 11508, 99917, 34237, 4124, 67464, 43359, 26791, 158522, 144226, 38792, 18892, 13958, + 41850, 71554, 19647, 61226, 98703, 124759, 9034, 30539, 34371, 467683, 65227, 93480, 7901, 29703, 82581, 17619, + 21254, 2734, 102235, 57361, 38398, 17196, 96566, 364971, 65651, 28491, 137653, 151200, 23549, 142281, 78664, 84647, + 53883, 34900, 46611, 40124, 213340, 36091, 30841, 52023, 123269, 197827, 78380, 73808, 12028, 41191, 45370, 7903, + 71764, 90561, 215513, 34771, 177701, 60585, 32517, 25415, 28758, 92364, 41752, 82386, 2623, 76984, 37173, 50064, + 68395, 4403, 43681, 98106, 11549, 37791, 53245, 104084, 15232, 60303, 28511, 10160, 68603, 161243, 108330, 25902, + 9660, 75510, 24031, 320844, 63116, 2614, 187946, 158900, 36079, 32819, 38425, 121867, 57093, 51652, 80423, 5684, + 31198, 161752, 36511, 146667, 20475, 4507, 19201, 30548, 48467, 78275, 5077, 22549, 89984, 50714, 17018, 66706, + 35619, 3531, 25020, 48835, 186847, 9463, 64248, 111387, 107469, 188636, 105137, 8703, 31389, 34009, 25409, 14207, + 43631, 12180, 14360, 89167, 73867, 33692, 74998, 119507, 41949, 87046, 115959, 6770, 68841, 6405, 81460, 142743, + 114250, 5132, 148038, 8544, 1605, 15540, 177509, 152453, 30564, 1453, 29815, 32099, 63403, 6534, 252, 11418, + 17588, 16946, 116226, 138792, 27680, 101291, 14339, 60744, 99533, 194699, 39891, 16377, 12641, 146345, 141037, 140957, + 70325, 42826, 21176, 19061, 50428, 51375, 12005, 144174, 73426, 76160, 46461, 129205, 78379, 40145, 25474, 133179, + 11855, 127230, 80781, 27661, 91651, 396, 88286, 34420, 72081, 94521, 46013, 15403, 91720, 88126, 549, 79964, + 60198, 187793, 125452, 14392, 15743, 83700, 1715, 206475, 12065, 22805, 39201, 63593, 83398, 13801, 99478, 25699, + 140046, 83985, 258911, 49800, 6761, 9435, 74133, 1389, 46598, 11934, 27897, 5386, 45900, 50232, 260248, 96335, + 5068, 33530, 49692, 65315, 886, 4019, 40151, 91916, 62448, 10628, 13597, 141884, 148968, 14083, 261394, 163080, + 37347, 1276, 8705, 22521, 19405, 57813, 55957, 200637, 9680, 15938, 42198, 23570, 15819, 166772, 12555, 37690, + 43496, 26735, 63396, 52654, 63370, 37031, 83006, 34067, 75667, 18077, 2332, 127143, 163700, 1741, 5704, 42945, + 37639, 34898, 146321, 65846, 3633, 51526, 134217, 9232, 22774, 1772, 66282, 48233, 34341, 21424, 13242, 67351, + 183131, 97579, 28584, 80530, 134335, 38834, 239665, 40278, 37200, 32081, 53185, 40538, 23915, 99449, 228969, 44407, + 7054, 17139, 112714, 101287, 14194, 810, 50801, 33269, 12970, 136397, 73876, 55691, 26438, 37462, 53441, 99617, + 13350, 42910, 71168, 42168, 285521, 78217, 93695, 78702, 25594, 30022, 14876, 15315, 8219, 34298, 97268, 51265, + 104410, 54835, 26232, 30943, 91039, 14205, 64948, 29544, 168804, 48486, 75782, 69559, 138480, 99517, 39422, 37873, + 149734, 38422, 8276, 41956, 15907, 166205, 43978, 154555, 33818, 28840, 72911, 38423, 61132, 39592, 4460, 37088, + 60082, 68918, 21965, 92108, 4622, 33094, 35917, 111849, 110187, 72295, 8713, 89755, 56736, 89742, 45364, 6790, + 13551, 41957, 48065, 8188, 73571, 62739, 53050, 144415, 3945, 24426, 98109, 2450, 73463, 37147, 78116, 9166, + 65498, 53281, 120479, 179965, 17758, 17052, 32430, 155198, 263266, 84633, 148996, 48061, 17593, 38254, 12219, 51478, + 2710, 95095, 20869, 29664, 27585, 13750, 47237, 181, 54469, 7569, 60566, 139036, 38200, 24063, 28235, 13774, + 45367, 230307, 83783, 80750, 63754, 95816, 43190, 122385, 28881, 144847, 15520, 94088, 3473, 3890, 113331, 76118, + 12791, 80924, 118713, 104097, 98287, 136037, 67781, 10689, 31895, 90630, 43079, 93377, 65787, 61758, 1438, 103534, + 4463, 1696, 4512, 120430, 94536, 3019, 34145, 75690, 24951, 53596, 58640, 11685, 36332, 6632, 11422, 26659, + 59901, 43634, 130651, 33557, 28803, 3908, 133680, 9899, 52130, 43287, 6912, 145132, 86403, 177137, 54381, 65649, + 7668, 54154, 97545, 91326, 181822, 89508, 11188, 2346, 74831, 83183, 79610, 47140, 18977, 54325, 82292, 130974, + 9850, 25539, 71074, 133045, 177206, 71768, 81956, 28358, 145485, 83131, 29163, 37533, 109798, 18435, 75184, 52995, + 7292, 6596, 2251, 2040, 31421, 19770, 177982, 2607, 26280, 108782, 69065, 324604, 77211, 91802, 37849, 30896, + 58511, 49307, 70, 135829, 12507, 6486, 51031, 9444, 127004, 180981, 46202, 95370, 11113, 65980, 7248, 1864, + 147, 52898, 183217, 22572, 8729, 6517, 4166, 36847, 56208, 124700, 29553, 6200, 43066, 1797, 193216, 150871, + 79926, 125256, 38154, 42479, 129937, 102831, 33444, 18931, 31345, 35792, 147516, 19534, 83947, 136739, 76241, 217709, + 39915, 177, 29612, 176661, 46146, 21703, 66186, 15852, 98763, 32284, 15029, 20993, 42566, 64132, 96065, 164496, + 1337, 178401, 214897, 22155, 13192, 119711, 143143, 16098, 18323, 17920, 25851, 94549, 105163, 90198, 141550, 124816, + 80570, 619, 144085, 9847, 117753, 16770, 4661, 55732, 16555, 15568, 31762, 116903, 72883, 28446, 93397, 75397, + 11077, 69803, 1471, 136227, 159438, 102246, 162403, 73442, 40764, 27766, 18455, 53335, 70933, 12129, 19140, 50321, + 83329, 51649, 28681, 50106, 26066, 38874, 69436, 77741, 12276, 9285, 17504, 16474, 72059, 83880, 8401, 45101, + 21655, 38252, 34797, 40053, 173836, 50540, 136368, 21585, 126713, 108126, 142751, 130247, 69454, 55138, 130179, 11656, + 153482, 162984, 158538, 204679, 91585, 26846, 8149, 101037, 70644, 67384, 153679, 898, 102558, 18887, 79015, 11681, + 110483, 73677, 144865, 17407, 6764, 28552, 369746, 32468, 127864, 203511, 3905, 45256, 190133, 1948, 85436, 54536, + 3961, 2931, 39613, 52497, 101798, 205435, 63769, 13356, 20945, 11390, 155312, 107698, 71138, 12797, 29367, 1300, + 82402, 758, 56650, 14527, 90884, 61424, 194495, 23078, 69669, 175831, 14654, 112729, 44753, 232898, 27621, 99382, + 923, 15115, 22648, 45524, 16939, 11503, 36250, 76362, 59700, 80609, 92077, 81783, 164258, 147964, 93190, 7889, + 25969, 56255, 28476, 115588, 27082, 3104, 94697, 68454, 31399, 203455, 22046, 2756, 43846, 6544, 135118, 128148, + 6306, 148500, 12599, 221886, 246093, 1024, 14109, 13441, 51342, 119104, 168855, 81131, 6153, 19221, 79225, 10911, + 151581, 83444, 1795, 32090, 202801, 86292, 19784, 98045, 182731, 14239, 27382, 126354, 56475, 255797, 116118, 46059, + 162188, 48612, 34197, 23712, 89426, 12944, 100256, 15683, 141356, 108578, 128764, 17528, 14355, 271397, 13200, 5464, + 121815, 164025, 19217, 145007, 27536, 7271, 11898, 27670, 28023, 2020, 34004, 98721, 65257, 221829, 10108, 127625, + 77523, 58581, 6914, 16301, 106668, 18199, 38690, 49947, 127314, 61987, 99158, 82020, 24947, 156159, 44297, 40008, + 12790, 191552, 58504, 13996, 38796, 49380, 31168, 78568, 169698, 51893, 110268, 42109, 23555, 66459, 81335, 350079, + 10725, 11018, 5903, 58576, 44573, 24333, 7047, 15096, 183083, 25940, 30092, 21527, 42088, 116405, 102404, 91943, + 62716, 83434, 46226, 148493, 43265, 16668, 120110, 99611, 105958, 52689, 135626, 19929, 32050, 1904, 97814, 125365, + 44067, 3747, 104, 129365, 50118, 22240, 81479, 147860, 1668, 21592, 58604, 65314, 3874, 8813, 70784, 17613, + 6243, 68475, 42985, 56814, 318411, 38092, 62855, 5047, 16599, 38145, 13374, 7748, 1691, 63047, 41837, 61793, + 94999, 40014, 231631, 149314, 52378, 17178, 17229, 20594, 28671, 298969, 135669, 3725, 216728, 259469, 33874, 54293, + 123258, 45594, 132414, 22290, 2059, 129670, 52137, 8994, 34969, 83516, 130408, 123610, 69225, 72380, 90635, 47420, + 5913, 73509, 87799, 48451, 136280, 23988, 84653, 34987, 171443, 34172, 131573, 84347, 141515, 41151, 84753, 26644, + 91662, 53005, 30287, 228834, 22175, 85089, 5937, 239357, 135282, 3173, 18644, 35622, 80020, 157977, 39079, 53223, + 92270, 5878, 10450, 49133, 1663, 158962, 37020, 13802, 4808, 5512, 34099, 49182, 4482, 39140, 15951, 157071, + 3495, 132172, 122206, 61350, 34691, 43353, 72769, 74295, 5226, 24234, 167378, 239776, 109830, 5797, 48280, 64587, + 108512, 25762, 31651, 55137, 17342, 87230, 251069, 6824, 107488, 60185, 7752, 14553, 11606, 10402, 78777, 6829, + 123190, 43191, 33662, 9649, 100247, 18941, 76354, 27419, 29666, 53504, 129460, 19206, 146527, 208486, 41206, 37237, + 113014, 30191, 57416, 5183, 15794, 3541, 57420, 56560, 30894, 159900, 136009, 282298, 13224, 83119, 1406, 240017, + 39585, 278159, 141007, 6722, 243192, 21680, 25193, 108277, 22351, 16677, 47180, 52089, 9903, 7391, 70117, 38331, + 7836, 20514, 176863, 391251, 47699, 49431, 142718, 40783, 11078, 28591, 32120, 38193, 25468, 458592, 10098, 64745, + 122291, 62128, 93822, 46652, 48821, 13662, 8766, 17814, 26780, 22468, 135823, 124150, 122679, 129978, 11360, 113819, + 75521, 58918, 556, 93437, 81450, 77000, 1965, 3492, 630, 26563, 1323, 193118, 4895, 24288, 80421, 444827, + 92900, 45787, 6232, 74549, 55074, 50259, 138542, 21220, 74293, 14177, 86741, 20845, 17441, 108129, 27172, 542, + 3563, 94640, 76494, 41896, 111657, 28829, 50902, 406663, 103102, 28983, 10586, 120692, 51613, 18805, 55476, 84562, + 12318, 65439, 2351, 47041, 52370, 9553, 44550, 41604, 36191, 65155, 67526, 37264, 68245, 92017, 11541, 5842, + 34269, 49599, 26889, 59557, 40445, 96850, 1906, 135711, 41354, 29246, 70287, 106660, 122901, 45259, 163034, 61670, + 168604, 10682, 20890, 849, 182500, 7459, 14076, 9824, 62012, 17393, 43, 55379, 42557, 140724, 7749, 356813, + 11259, 106585, 56522, 117254, 24428, 38947, 52871, 9300, 115113, 20086, 55518, 10927, 86345, 43851, 62143, 258476, + 12362, 54324, 10491, 40991, 3909, 2374, 125973, 57172, 78430, 341578, 168875, 62670, 86852, 965, 24965, 107818, + 134602, 52017, 33775, 46473, 20459, 104509, 19147, 87014, 47853, 11886, 132839, 96809, 93879, 110609, 2365, 42726, + 22577, 113945, 93171, 450, 7659, 3573, 145278, 14002, 3688, 110115, 18705, 53062, 38555, 47342, 56746, 32670, + 13349, 82125, 43630, 40651, 17381, 125502, 22739, 15199, 56715, 30071, 11269, 266060, 91639, 147670, 110730, 156150, + 12493, 33798, 89625, 209004, 10895, 25311, 4017, 18602, 92438, 67412, 21469, 72030, 3142, 102900, 25974, 86939, + 37057, 197485, 58360, 6360, 28928, 67451, 1402, 28843, 2004, 88633, 12339, 31963, 36427, 70216, 321089, 288373, + 32268, 39008, 31413, 4740, 34222, 25661, 12279, 159470, 209974, 23583, 8221, 132962, 10432, 69680, 62983, 6760, + 67436, 1150, 66541, 28543, 41989, 120559, 51407, 30922, 173518, 85466, 111050, 56221, 107930, 37565, 20849, 48181, + 27079, 107182, 10708, 12667, 62729, 131120, 15921, 10076, 30908, 157502, 94904, 63675, 55558, 6617, 226273, 5180, + 5828, 56366, 13233, 61985, 45031, 2485, 22785, 51794, 14902, 861, 156114, 2399, 53546, 23616, 69393, 128641, + 8204, 20069, 47928, 35756, 144263, 9724, 28168, 77879, 60255, 33275, 30360, 2287, 14520, 139561, 6919, 38561, + 88212, 24401, 123947, 83850, 86582, 113753, 3963, 10915, 109589, 75957, 42047, 64212, 69356, 27884, 27654, 1658, + 8064, 25531, 26396, 43262, 47449, 97110, 27015, 18044, 8505, 10820, 6910, 15078, 66558, 7192, 123575, 29932, + 16886, 91286, 41104, 63367, 4844, 40914, 6359, 2845, 52817, 88113, 9448, 105511, 111260, 18932, 4691, 644, + 215129, 199771, 74460, 6293, 12941, 6495, 60621, 58746, 91118, 57660, 16174, 48568, 650, 73549, 62408, 27497, + 20770, 36570, 9691, 125291, 6273, 101510, 524873, 63355, 73089, 27873, 22599, 43928, 40618, 143174, 44188, 54641, + 62790, 6716, 38742, 153007, 2873, 81054, 70058, 28685, 35002, 11708, 63922, 161592, 14023, 143955, 9908, 36977, + 97208, 20840, 37740, 37328, 19386, 541613, 59406, 52601, 102646, 24863, 90336, 108723, 36993, 19256, 90283, 156507, + 143736, 226099, 22792, 168470, 135457, 88686, 29520, 41685, 35385, 2247, 53884, 7914, 113601, 25634, 168024, 22823, + 17893, 52964, 50078, 12971, 32627, 62833, 89378, 69345, 84439, 2950, 9888, 39211, 100619, 12559, 4264, 76283, + 56016, 57173, 88905, 18361, 6581, 257, 15516, 18690, 57264, 152842, 167732, 100142, 172160, 177909, 114841, 98812, + 45452, 152952, 146652, 57421, 111710, 33059, 83570, 109892, 203627, 18224, 13991, 11405, 70131, 54654, 97200, 72333, + 24100, 8565, 145467, 50366, 322787, 161041, 7765, 55869, 1996, 48165, 55619, 16231, 35665, 59444, 21986, 40608, + 70078, 91914, 2452, 11983, 22358, 46435, 113727, 327395, 90922, 54176, 3637, 61910, 83658, 16892, 19473, 12132, + 4097, 10440, 32159, 34709, 63200, 53820, 279, 973, 58499, 93368, 5855, 59071, 14542, 256523, 219447, 28720, + 99153, 71396, 152707, 32750, 52159, 60623, 48369, 4454, 6615, 260119, 265523, 24086, 12414, 92637, 2799, 18610, + 63415, 61306, 58297, 80979, 31986, 68389, 34835, 18261, 16823, 2311, 3203, 19828, 1579, 46046, 15243, 2581, + 65405, 113461, 50646, 108814, 137809, 9153, 82289, 158349, 8841, 506, 90330, 157611, 16898, 11327, 131769, 58320, + 48082, 107069, 6787, 166721, 259, 104429, 47373, 3486, 33014, 21454, 101722, 52682, 42375, 100879, 112359, 120319, + 12260, 113213, 38656, 173961, 179850, 20340, 86000, 5336, 73667, 112248, 73424, 114676, 91389, 8255, 33598, 167952, + 98882, 313221, 141752, 163735, 29532, 164918, 42180, 9328, 17311, 38319, 9180, 42386, 326251, 90818, 26426, 1863, + 41092, 62380, 5374, 95142, 5928, 29861, 105695, 62497, 20742, 89078, 12203, 22276, 44964, 36172, 50837, 165289, + 48019, 238111, 16046, 2104, 43505, 99836, 25312, 70749, 9317, 81582, 61952, 108704, 49265, 17815, 1489, 44835, + 6643, 115249, 21605, 88265, 192712, 163219, 138454, 48414, 48424, 56847, 23238, 77168, 163487, 20527, 64335, 82158, + 19861, 31349, 187836, 102708, 20113, 52444, 125598, 1727, 70848, 111298, 11424, 155809, 31928, 96999, 24225, 14471, + 105333, 16160, 91323, 49302, 23685, 29832, 218861, 101530, 78563, 93096, 11349, 110852, 14638, 43607, 123209, 91558, + 54755, 35628, 107357, 380618, 7158, 28912, 18284, 4868, 24142, 51577, 38642, 239650, 44018, 991, 26090, 1960, + 20774, 158291, 81907, 50593, 125255, 6485, 72743, 2966, 20331, 214561, 56587, 142157, 24280, 12387, 150, 57700, + 10163, 773, 45875, 97502, 1285, 41252, 206433, 50923, 2336, 113158, 120454, 130298, 39851, 20223, 103681, 48431, + 4299, 190321, 123059, 33343, 117269, 71169, 26222, 33921, 46714, 32383, 97444, 133272, 63816, 37830, 49882, 50473, + 87779, 17666, 112641, 59034, 159624, 26706, 33574, 47126, 11731, 63641, 16669, 933, 9971, 20216, 30684, 99703, + 990, 23911, 20195, 121307, 137317, 47148, 41928, 10318, 108831, 355535, 49454, 32292, 50994, 8122, 16384, 59812, + 74554, 32150, 56591, 174312, 162680, 190228, 9598, 173148, 23640, 61991, 45718, 79874, 131597, 38994, 5964, 48294, + 146962, 7849, 20626, 126616, 170620, 27716, 30327, 39879, 34829, 15772, 147440, 111326, 91205, 762, 39309, 61975, + 21184, 41411, 43771, 23877, 1913, 5640, 44358, 29145, 63616, 7290, 39142, 49951, 18427, 231174, 53813, 32775, + 93136, 7356, 69502, 40528, 156592, 58725, 18403, 57219, 17519, 27403, 39497, 64617, 67565, 18802, 347190, 3273, + 115882, 171128, 6465, 87835, 138220, 37443, 12843, 45934, 78622, 101762, 40426, 16768, 88535, 152513, 46216, 13386, + 18115, 79078, 49051, 26866, 2711, 79682, 103900, 31529, 33554, 8114, 97575, 47612, 109492, 22435, 45316, 90147, + 54298, 70069, 6113, 94219, 971, 203886, 171510, 54098, 24914, 22343, 6068, 91601, 25863, 143464, 64459, 38876, + 36363, 74653, 13924, 17888, 45715, 21595, 79494, 40396, 27099, 48946, 229942, 68108, 194995, 38040, 6364, 105256, + 14299, 43792, 53868, 72865, 178181, 10801, 106043, 59704, 111488, 12611, 11620, 1615, 72395, 35357, 68088, 375, + 322385, 97707, 2721, 26698, 157719, 129043, 96424, 15347, 130787, 1519, 70008, 98397, 11897, 5720, 336769, 151561, + 81843, 3436, 2996, 78953, 83999, 15800, 40841, 8897, 11369, 50915, 1888, 69080, 49280, 45808, 1062, 11670, + 118604, 172349, 10206, 107337, 40922, 127584, 10458, 54750, 61332, 36780, 6193, 84096, 110343, 6105, 74775, 66665, + 53407, 65182, 51401, 281520, 75639, 25185, 147990, 19365, 40582, 32622, 61482, 106248, 300440, 3435, 158683, 10556, + 54722, 4592, 47332, 14499, 25637, 77065, 19723, 29586, 13694, 66649, 26726, 58610, 48248, 133393, 123123, 232634, + 48278, 828, 68414, 56873, 194521, 5023, 64229, 14306, 56203, 86618, 265580, 48023, 52779, 84656, 3432, 27698, + 48783, 165900, 55898, 24382, 72627, 95034, 100547, 13185, 10953, 158080, 190907, 81943, 376, 38482, 157736, 4714, + 16733, 10368, 160286, 14619, 280238, 20156, 17445, 38808, 26351, 35060, 125894, 64807, 230789, 9972, 49181, 669, + 15132, 16923, 2631, 4706, 25168, 70685, 110447, 122549, 137270, 146057, 155535, 21929, 3588, 7274, 38517, 20678, + 63704, 12863, 1324, 50985, 73376, 43993, 200585, 60934, 94031, 33647, 19008, 24301, 74284, 50725, 56271, 104028, + 19443, 69093, 1021, 4350, 159557, 108365, 31846, 2318, 9697, 23630, 70960, 33088, 39901, 32656, 33002, 83862, + 13351, 27646, 142809, 48055, 119050, 37694, 118047, 85271, 15406, 13547, 124197, 30179, 146455, 65071, 112066, 5002, + 3460, 7809, 11639, 39385, 29556, 54914, 121220, 5596, 75195, 25679, 106873, 33104, 37673, 7246, 204139, 29952, + 102524, 115109, 36038, 5460, 92329, 49344, 25242, 34014, 47289, 113697, 6996, 24397, 98413, 185550, 45812, 60522, + 15311, 46467, 213965, 11122, 100684, 7259, 17039, 6550, 56345, 167013, 404753, 307, 7116, 56462, 5417, 73857, + 95480, 78473, 10133, 19036, 11590, 36107, 100941, 53208, 7200, 60420, 95105, 119370, 167, 15524, 6653, 43384, + 23610, 46388, 13093, 145345, 58426, 55680, 305451, 86847, 17730, 58567, 178140, 38965, 136656, 5628, 88811, 17675, + 27944, 83441, 51603, 10939, 53151, 35212, 27904, 53967, 2701, 23131, 24488, 104154, 8824, 55304, 27690, 42802, + 103124, 98578, 280054, 5662, 3017, 22793, 12906, 31495, 90744, 23983, 3464, 134399, 113588, 22474, 69068, 5099, + 53216, 109191, 117048, 132077, 79736, 676, 7774, 71253, 65940, 32772, 116614, 53644, 26931, 7596, 62478, 12003, + 498, 19827, 108263, 47076, 29568, 168754, 31319, 6482, 80540, 13955, 111749, 21181, 143543, 7033, 26026, 51353, + 21292, 85463, 42779, 24887, 1740, 14404, 130369, 63220, 59268, 97587, 33374, 6212, 16561, 43680, 36822, 42418, + 180816, 18436, 9579, 74123, 42323, 147608, 27260, 24977, 50174, 61507, 10681, 205404, 40890, 85856, 53671, 11516, + 52866, 109367, 117903, 74687, 10703, 42095, 18194, 187166, 57169, 32725, 8992, 137746, 4700, 203872, 80833, 23954, + 17191, 61462, 362182, 13934, 4424, 17464, 76670, 12727, 93511, 23063, 65868, 71113, 49698, 1101, 33252, 57054, + 166650, 125436, 48999, 126818, 26972, 5103, 5669, 11598, 48631, 1882, 120131, 84296, 165169, 35330, 3827, 13014, + 82879, 103279, 159135, 24477, 69326, 96729, 88413, 114734, 202970, 9725, 43640, 83623, 4007, 109, 63529, 18092, + 2376, 47112, 60271, 36164, 231325, 18597, 8868, 14797, 139592, 33580, 9437, 22241, 22119, 132398, 126879, 35116, + 62851, 44339, 72086, 46648, 37504, 19320, 81584, 48809, 68816, 14329, 24943, 2579, 58345, 39374, 43873, 7046, + 67398, 497785, 28225, 24722, 186643, 41257, 163961, 110230, 43331, 10072, 100738, 91543, 277416, 48225, 37125, 93902, + 53749, 3511, 89864, 7794, 15746, 112345, 53732, 9563, 23102, 195132, 38187, 21737, 17432, 246976, 175275, 118156, + 4793, 40293, 65384, 52281, 151138, 59278, 5431, 12606, 48822, 47893, 70528, 6708, 54265, 9483, 244562, 3448, + 48203, 48689, 76888, 27624, 198688, 30465, 4820, 115925, 14305, 15512, 85317, 22487, 54287, 116889, 25533, 69671, + 2291, 33041, 27717, 4838, 18018, 20643, 214146, 60357, 113378, 9119, 25030, 79694, 123260, 45067, 41450, 70902, + 7180, 58600, 349673, 18393, 97549, 78338, 21978, 83130, 87027, 776, 46804, 136813, 120085, 10785, 36277, 36292, + 2920, 3810, 8562, 21587, 76080, 26439, 44502, 4907, 8190, 14533, 46271, 56528, 102005, 40453, 71405, 78573, + 5641, 71512, 18178, 33524, 64580, 92871, 1535, 33803, 14955, 66991, 45301, 14937, 59802, 4319, 76815, 79999, + 54028, 152893, 140972, 94339, 58884, 25420, 33237, 94907, 19367, 63221, 14998, 39549, 81779, 200006, 11084, 130155, + 412567, 22144, 57584, 48001, 85957, 43644, 62293, 11070, 97053, 131816, 56871, 43893, 103637, 49559, 149443, 265832, + 78871, 127928, 16848, 80586, 29364, 10123, 96121, 61469, 27637, 122786, 76840, 137205, 141728, 152810, 275825, 164900, + 4767, 48483, 59279, 45329, 30686, 30546, 26125, 67059, 112738, 1079, 36371, 14899, 130146, 140588, 27019, 105371, + 42745, 50423, 37529, 61488, 12730, 34379, 84661, 32169, 105040, 91899, 1842, 106489, 14844, 44986, 133939, 80141, + 232, 29291, 19684, 156429, 210944, 61928, 43419, 24020, 36581, 39425, 29731, 17427, 152317, 27381, 12301, 14375, + 135543, 5697, 113605, 107512, 29744, 107850, 5566, 76470, 3129, 115721, 13033, 87708, 55647, 1993, 5259, 8785, + 58149, 147004, 120371, 11205, 46319, 4743, 14434, 21697, 27265, 15562, 219250, 35228, 17499, 95327, 51051, 18310, + 28005, 166872, 77694, 6553, 59948, 33504, 13613, 41235, 7170, 2462, 19927, 201918, 34138, 6665, 48780, 7020, + 5702, 48429, 19212, 60564, 293047, 10831, 70945, 188451, 110892, 514, 102527, 8278, 408, 60992, 54201, 111510, + 91760, 50530, 1350, 10771, 218674, 53990, 187697, 687, 18469, 10560, 105023, 46885, 46095, 53517, 5808, 50818, + 81403, 2170, 22662, 47127, 14389, 748, 107058, 67959, 4610, 94719, 30947, 36510, 35672, 12468, 50291, 29921, + 73060, 20353, 3271, 51468, 11006, 18654, 11663, 76382, 74848, 7193, 20342, 16187, 104820, 128163, 34980, 13510, + 118143, 40642, 131, 15981, 190357, 1266, 49918, 3612, 20043, 144329, 29109, 51118, 105358, 3728, 9721, 32144, + 141735, 43619, 23028, 307278, 5115, 112747, 69300, 39045, 27093, 43684, 177727, 78108, 45924, 88098, 14492, 21583, + 123073, 18169, 28525, 17923, 52599, 21514, 7009, 272, 29433, 16693, 69214, 94247, 9616, 71738, 25205, 9774, + 238350, 23631, 52998, 161274, 78610, 112631, 168968, 6162, 24851, 53162, 47254, 154989, 58858, 17143, 112681, 166473, + 26769, 71077, 10680, 22270, 31969, 225745, 12087, 104993, 24613, 70335, 68735, 124630, 18294, 157460, 57255, 111655, + 4982, 2471, 133743, 52649, 32735, 12031, 93961, 124470, 39639, 39402, 65329, 63203, 143563, 23805, 22268, 105529, + 112073, 16680, 215716, 21519, 202205, 53228, 247084, 41944, 12567, 2653, 65493, 34693, 4873, 34636, 23926, 71532, + 88601, 71037, 5571, 115122, 44897, 69538, 89072, 43808, 81503, 36583, 28920, 133940, 101648, 191184, 4910, 9942, + 81362, 14489, 27729, 4960, 34662, 1289, 52544, 14776, 85277, 65813, 125227, 8902, 17574, 23259, 36661, 1163, + 48173, 42427, 11467, 98879, 21435, 10466, 35373, 39046, 221188, 185252, 93230, 99061, 40215, 21457, 5235, 36240, + 39576, 128066, 115000, 60589, 80786, 34006, 111892, 51482, 26544, 10656, 50198, 141955, 64668, 129479, 51135, 64929, + 81841, 27845, 7128, 18886, 10731, 11965, 52276, 82304, 37733, 19729, 35028, 59846, 247986, 66135, 53707, 299634, + 149188, 117025, 39304, 27726, 127703, 9496, 71006, 9486, 495, 94820, 47250, 266544, 18382, 101150, 6281, 60703, + 54388, 207915, 132866, 58949, 72446, 26212, 10542, 172127, 43071, 62490, 4376, 56788, 30974, 24968, 91443, 11619, + 198723, 24001, 15121, 136267, 89608, 160663, 33578, 82492, 41360, 46571, 2169, 9481, 190, 29283, 38373, 141023, + 33045, 7619, 29792, 83332, 8386, 98498, 39183, 39255, 31658, 129170, 27144, 67660, 19992, 114184, 53128, 24534, + 237838, 8463, 70363, 22205, 119015, 18826, 68254, 121728, 137622, 10868, 292971, 14603, 50890, 34277, 24430, 65197, + 100913, 109419, 40103, 47081, 6460, 25365, 62207, 31961, 116233, 10816, 65775, 61031, 267230, 19629, 72115, 104486, + 26621, 27002, 49635, 13334, 104129, 8378, 107732, 16558, 65114, 17477, 179978, 4220, 14190, 224493, 16453, 41872, + 41542, 19134, 32179, 25813, 14888, 130609, 58701, 15362, 85962, 12751, 55045, 6635, 23342, 28105, 4224, 127171, + 23041, 34303, 59129, 73656, 26453, 19410, 24291, 79837, 43725, 157265, 16625, 34377, 71809, 89485, 82437, 24305, + 45186, 20785, 34798, 76873, 4770, 3923, 44346, 9874, 46452, 55654, 54665, 4785, 53894, 5025, 60017, 171215, + 56616, 15749, 17762, 16761, 221286, 105132, 20523, 56343, 18973, 1037, 602, 323886, 9038, 95606, 38396, 120502, + 109299, 77097, 79367, 136893, 55365, 8323, 10041, 75592, 19366, 65814, 15614, 83469, 26863, 51521, 18119, 15623, + 18808, 146873, 172424, 11543, 60909, 19528, 22504, 437698, 69353, 57600, 115924, 4644, 41738, 42360, 79256, 27642, + 83463, 162712, 19094, 2916, 12100, 118144, 293484, 54555, 68561, 61908, 73882, 178688, 72860, 49139, 70532, 39905, + 3980, 13190, 59763, 20658, 13796, 126559, 22696, 105248, 49340, 64706, 49195, 81589, 12332, 67817, 7722, 132448, + 31311, 20180, 151712, 35160, 27418, 221689, 96589, 5129, 4255, 9628, 16047, 98064, 53430, 33666, 22032, 208500, + 18976, 61564, 112915, 15902, 45523, 84205, 34747, 46386, 510, 76620, 34577, 24704, 14224, 18606, 4384, 109151, + 30477, 37359, 74952, 30874, 26581, 19550, 21657, 13283, 4530, 93073, 46376, 122364, 3651, 181757, 62601, 2206, + 101663, 118572, 17855, 52952, 139840, 62606, 278021, 77080, 22709, 7959, 241332, 42371, 150861, 39772, 27577, 14415, + 31996, 13012, 109120, 42856, 63923, 41223, 14878, 182608, 120623, 29348, 36991, 15117, 262522, 7183, 54934, 3332, + 3076, 65389, 287192, 49740, 10528, 35270, 56818, 22639, 2929, 61578, 23280, 30127, 14672, 3157, 47696, 21588, + 130238, 62110, 52327, 56193, 18087, 130896, 142685, 109840, 9816, 18121, 57992, 97319, 121894, 80599, 54501, 27268, + 100308, 81451, 3014, 47285, 25085, 7174, 5312, 74894, 55111, 160804, 29625, 46682, 14565, 172807, 29181, 36368, + 18952, 75109, 124625, 24016, 53293, 186013, 58761, 87814, 2042, 89459, 35087, 63052, 369988, 86526, 54374, 118097, + 23674, 2122, 110119, 15845, 61789, 10677, 100968, 183270, 133529, 81233, 103267, 198839, 28783, 73169, 17965, 361713, + 108293, 136324, 42487, 26373, 35477, 218558, 27158, 71527, 47119, 331883, 45160, 78970, 36448, 156591, 71832, 96730, + 71049, 146, 111991, 17781, 40015, 99088, 5627, 90890, 33055, 23338, 4252, 167723, 78598, 149826, 35613, 11228, + 198442, 31532, 169906, 23323, 1833, 125495, 8102, 12897, 159937, 4522, 13990, 177607, 40654, 227310, 20082, 31598, + 77444, 15467, 109179, 17723, 189245, 6218, 103555, 3422, 113153, 7920, 38581, 148584, 8621, 19962, 49090, 28195, + 18599, 79279, 133222, 12709, 38553, 16730, 103630, 60913, 35223, 13228, 67419, 28730, 166072, 36003, 6493, 36652, + 2375, 50657, 9527, 81998, 11659, 334160, 15168, 51380, 21786, 15122, 59275, 119745, 89523, 67219, 2185, 46735, + 6032, 47447, 137690, 5166, 12116, 23018, 5022, 48313, 63046, 10829, 83429, 2746, 159398, 67335, 98418, 53999, + 18454, 31569, 37892, 109132, 3678, 81620, 104491, 54681, 32521, 109973, 42533, 52504, 47626, 42504, 36807, 2633, + 11411, 159915, 60573, 111310, 103527, 128644, 103027, 285252, 38896, 10950, 44585, 164005, 42946, 28131, 45971, 32359, + 15696, 11312, 106951, 2917, 26370, 12048, 16052, 74680, 10185, 98893, 89672, 24790, 8413, 87026, 851, 4549, + 37080, 174727, 18289, 200644, 165583, 57864, 69857, 29259, 4331, 19717, 91462, 13011, 63555, 21391, 1990, 44481, + 14907, 7340, 15774, 49591, 72220, 3931, 6418, 53568, 50056, 9288, 4106, 48438, 6623, 5117, 1012, 41437, + 62236, 84002, 33918, 65542, 36565, 92277, 39629, 127582, 49783, 44146, 117628, 5155, 10049, 42692, 4608, 187456, + 17503, 74015, 34318, 12962, 100581, 33867, 21832, 157914, 55951, 106858, 14475, 188259, 146244, 57414, 21163, 26020, + 24724, 5253, 161736, 121717, 9626, 38446, 431177, 1188, 17969, 46978, 157547, 62747, 25524, 30400, 177415, 12289, + 109300, 175621, 49009, 166322, 173965, 132234, 51768, 26032, 99994, 15755, 29800, 72351, 101056, 240613, 21289, 15214, + 46459, 46348, 58410, 30396, 43647, 89731, 28915, 142785, 53737, 14946, 5545, 24313, 277968, 72809, 33232, 8640, + 8347, 16712, 3946, 25575, 123521, 39179, 9991, 145543, 74858, 137200, 34491, 8629, 33829, 23820, 68921, 35423, + 44762, 49871, 133748, 46991, 77574, 166611, 31020, 11875, 877, 44133, 33877, 3257, 81377, 73657, 30727, 46882, + 222525, 70076, 137697, 69291, 123532, 18001, 25423, 188796, 30602, 16643, 53078, 60931, 43881, 38283, 3221, 355554, + 53145, 80702, 59009, 39689, 2973, 60883, 77304, 1117, 16284, 24073, 22669, 122990, 81940, 39844, 12369, 18781, + 61281, 133978, 1107, 20779, 127044, 44416, 157395, 5868, 63620, 24846, 35621, 48921, 9875, 41926, 56949, 22208, + 14756, 163188, 3847, 68103, 114829, 35532, 76171, 42852, 19032, 34658, 126394, 15837, 9202, 15195, 40076, 88927, + 52759, 150700, 23019, 8077, 119141, 224558, 88676, 134306, 23928, 48525, 115416, 76405, 120551, 21229, 51681, 188804, + 19607, 12750, 33280, 20117, 3599, 57084, 87953, 20205, 33401, 81760, 13064, 16835, 76821, 64466, 84841, 46288, + 73233, 87118, 9264, 13911, 117430, 86790, 13771, 24443, 39968, 82124, 15778, 63864, 36539, 3066, 24571, 55240, + 7071, 149040, 28850, 48106, 5446, 69537, 2674, 42691, 121735, 29802, 93801, 222967, 194059, 30066, 53170, 4677, + 15206, 107943, 39854, 31639, 45283, 53779, 59063, 2348, 6706, 195398, 48495, 2005, 15603, 165902, 265960, 30910, + 65615, 207693, 136663, 32249, 1207, 87403, 19215, 33091, 165723, 49438, 43462, 11067, 92275, 62699, 44678, 45289, + 34773, 17412, 48972, 1858, 104447, 181545, 1001, 119266, 8396, 153464, 132289, 115529, 32353, 7992, 172028, 54069, + 205240, 29592, 91739, 119994, 164323, 113460, 108440, 30929, 13600, 17141, 120659, 18146, 60555, 88204, 146500, 273964, + 79205, 25175, 90166, 44725, 25532, 113251, 4468, 67884, 22907, 53542, 14201, 46217, 33410, 54978, 26399, 177809, + 57480, 121936, 45917, 11309, 107111, 19480, 6478, 34292, 69630, 3736, 25310, 244475, 32137, 29438, 9982, 103486, + 47832, 52490, 37250, 2562, 70913, 5179, 136415, 91803, 33161, 9255, 18432, 13038, 20321, 49485, 6575, 115334, + 2371, 33789, 7808, 45775, 117348, 14605, 42941, 98279, 10714, 95052, 76922, 21407, 86246, 16306, 223273, 15316, + 1625, 250738, 2661, 32095, 11763, 40925, 49073, 29377, 17900, 1538, 17882, 8, 268, 90833, 50429, 806, + 78457, 6040, 177458, 105879, 99175, 24946, 48684, 55552, 97940, 1832, 81954, 48107, 101092, 1204, 34337, 93721, + 86660, 78345, 46377, 86370, 32221, 80601, 7480, 46341, 14041, 65429, 7269, 59499, 128504, 5442, 8744, 14012, + 125080, 161674, 6784, 14584, 53744, 130216, 6841, 122602, 124263, 31593, 18807, 113744, 31017, 885, 20344, 220948, + 13897, 791, 83914, 15043, 403, 15272, 20903, 38772, 31859, 100639, 117558, 55130, 21964, 75452, 165550, 31144, + 5633, 18734, 4133, 25708, 111630, 157700, 635, 222034, 5547, 90494, 37283, 12982, 77329, 31122, 83775, 43379, + 17961, 19477, 64730, 37597, 18241, 81216, 57923, 24610, 84995, 11492, 22811, 22078, 25984, 270901, 50439, 91195, + 12983, 70850, 98931, 154004, 67491, 66969, 83799, 168671, 62168, 141041, 109484, 189545, 47262, 124643, 76032, 11984, + 5241, 73085, 225, 35278, 297, 65251, 11750, 150754, 51191, 49847, 161229, 99388, 7351, 7406, 114189, 8814, + 8967, 13541, 57080, 44100, 147212, 116349, 149864, 40247, 82060, 41029, 37322, 87508, 16821, 59609, 20679, 68392, + 782, 43253, 332, 39938, 11033, 23732, 188925, 38934, 82431, 93785, 15532, 191109, 62957, 63022, 27531, 12252, + 5026, 3833, 2431, 21465, 43459, 25953, 192718, 3308, 77963, 89356, 168314, 168685, 203477, 49146, 7655, 23219, + 53528, 24982, 48685, 54631, 6247, 793, 74260, 48185, 191852, 3095, 172876, 179270, 87774, 154528, 7635, 7686, + 74164, 17007, 112569, 121364, 215654, 111268, 13329, 18967, 13467, 122533, 140331, 36832, 1522, 10077, 18418, 42747, + 219964, 6033, 9858, 108306, 28589, 3913, 123863, 178765, 244104, 135651, 14684, 90, 16242, 77118, 55574, 8651, + 117821, 131103, 73529, 284295, 67725, 19246, 36974, 62269, 72570, 115836, 24484, 7412, 156792, 33054, 573360, 105103, + 17186, 54299, 4686, 38563, 15979, 5729, 49563, 346297, 26990, 61110, 74313, 185151, 44128, 30657, 15134, 17195, + 193014, 25751, 282233, 137910, 35276, 2087, 2963, 41221, 57125, 17228, 387574, 26559, 16212, 16812, 134202, 221232, + 166451, 26462, 936, 117519, 68017, 293962, 22383, 16552, 6905, 277202, 9279, 27507, 77608, 63540, 29132, 62909, + 16364, 5654, 9548, 22674, 53777, 85931, 21483, 80245, 75921, 20622, 81952, 19224, 76426, 285538, 17463, 125910, + 37975, 57734, 50774, 69447, 26203, 50278, 2138, 85944, 269296, 11227, 222528, 187351, 64099, 65932, 14212, 61685, + 84122, 91003, 37581, 16099, 12077, 59461, 40868, 28123, 38533, 44075, 2588, 36016, 830, 15331, 10265, 75145, + 4407, 11384, 111778, 70114, 20139, 984, 26926, 342, 963, 46653, 91781, 6704, 43028, 132517, 82755, 31140, + 38902, 59578, 21048, 53790, 42911, 10083, 25044, 133733, 37503, 46985, 29071, 44261, 83343, 70437, 43357, 137819, + 85045, 60306, 103907, 440, 16979, 162235, 12666, 4636, 1165, 37911, 28310, 153557, 60835, 38997, 64064, 5185, + 137387, 32320, 2362, 99853, 58380, 69321, 213523, 50218, 86990, 100948, 61020, 50950, 110066, 13348, 19342, 25918, + 134540, 64060, 4579, 40959, 56331, 1462, 8265, 82320, 193845, 39437, 45216, 6839, 81238, 14382, 79046, 111178, + 17922, 29276, 57365, 149704, 163093, 60574, 16827, 8286, 38744, 18611, 99128, 34812, 110641, 21069, 171408, 84764, + 12502, 14627, 116407, 2999, 56404, 1193, 183004, 176249, 34862, 85940, 4493, 112158, 26865, 58572, 118875, 48672, + 125964, 15384, 25939, 117101, 12965, 233142, 40136, 48959, 111648, 66007, 37557, 264020, 25547, 33774, 91392, 32277, + 7771, 19724, 17197, 71573, 27196, 92454, 77107, 15576, 136980, 15649, 12242, 7975, 9555, 9118, 86424, 147261, + 29551, 122983, 9583, 13549, 107158, 97365, 31540, 14053, 57885, 2089, 19688, 78428, 18831, 57090, 7772, 6023, + 37705, 42279, 17489, 45843, 35505, 9504, 28240, 38671, 101742, 14357, 11556, 49552, 13970, 125707, 62503, 78, + 102109, 90600, 803, 39636, 62548, 22745, 124819, 19312, 124657, 21863, 74065, 15736, 23328, 7341, 72910, 29289, + 11124, 153626, 25328, 14621, 89592, 68952, 122844, 51529, 146376, 37056, 32205, 119308, 248050, 105130, 5385, 9375, + 6241, 68714, 119962, 30149, 22033, 32348, 9826, 10506, 18337, 1194, 11043, 20440, 80685, 7291, 6655, 39825, + 29898, 34836, 89830, 2183, 11908, 77399, 230, 65020, 216623, 34971, 221825, 14543, 67721, 227257, 14033, 27652, + 106162, 21776, 20020, 28180, 146610, 80549, 55471, 219506, 21377, 61817, 7021, 305922, 15085, 106336, 54962, 10257, + 91552, 89739, 15956, 10310, 42041, 12349, 21713, 99929, 62560, 8880, 23249, 59633, 122532, 7065, 9930, 6740, + 125336, 55124, 109173, 72328, 102365, 8889, 8209, 56963, 121537, 88838, 40700, 83507, 142559, 9284, 11988, 12130, + 29693, 99684, 1851, 53782, 223919, 30273, 31841, 19809, 11515, 73342, 35465, 93163, 110495, 23975, 49885, 92376, + 18776, 32648, 125596, 132210, 22494, 29891, 297641, 6024, 5895, 22238, 100417, 18472, 185059, 48669, 125656, 10048, + 103592, 49510, 59226, 104204, 229351, 165617, 46718, 48516, 51220, 41125, 102032, 77586, 100102, 19702, 25341, 13898, + 37027, 2385, 82750, 60716, 257855, 90865, 4079, 14003, 29359, 297913, 47142, 98916, 54123, 14508, 12634, 40487, + 36066, 129289, 2029, 245241, 106493, 5810, 109583, 17298, 12244, 60441, 17678, 23488, 79258, 151032, 54077, 112594, + 32002, 5397, 25330, 17562, 432, 34826, 2236, 30268, 56205, 22097, 10644, 6211, 94836, 184579, 8566, 12259, + 90182, 61802, 2022, 29431, 6726, 4718, 91806, 456527, 14762, 39564, 99255, 8125, 29391, 128901, 131938, 180961, + 48938, 149835, 7017, 26179, 26864, 38950, 491, 20535, 38083, 43407, 78246, 137337, 60364, 35466, 21672, 57096, + 3310, 39531, 1033, 46613, 60192, 3069, 21262, 288395, 14766, 77635, 29064, 99405, 205567, 154227, 179766, 14046, + 57504, 99249, 29131, 64556, 110760, 88476, 228776, 3720, 22649, 302613, 85204, 95561, 24666, 38247, 51104, 2156, + 46333, 117076, 191749, 48805, 21517, 16529, 57284, 34877, 3430, 71129, 30911, 56215, 13135, 122327, 141377, 4502, + 28873, 12208, 183048, 148459, 27052, 44260, 166866, 49683, 158809, 197069, 27559, 78004, 11597, 48052, 1373, 159150, + 20529, 206142, 12875, 61289, 6695, 68881, 22031, 2730, 23138, 107005, 59860, 1509, 22960, 36899, 42173, 70639, + 37137, 51908, 72392, 61903, 45574, 102776, 400, 53585, 6545, 7660, 71705, 48771, 305877, 115853, 19814, 37969, + 43423, 3477, 6989, 9730, 26153, 133461, 19482, 123699, 24769, 73107, 44361, 86286, 59844, 213778, 53573, 18022, + 14501, 100344, 128105, 108914, 10430, 136734, 154864, 41259, 134352, 60618, 70173, 3694, 56169, 29875, 7038, 16127, + 13213, 3067, 10956, 12306, 103432, 86864, 35882, 158228, 49523, 48003, 10223, 41242, 35181, 11563, 115948, 71725, + 13435, 59826, 26330, 104459, 12408, 31199, 198177, 173879, 129475, 28833, 287276, 88609, 64620, 90587, 23797, 20565, + 230854, 40940, 38005, 64403, 77390, 30876, 41640, 111472, 51990, 21480, 5502, 71562, 15653, 50942, 53320, 213339, + 83248, 57060, 47482, 99892, 33466, 94511, 35134, 39153, 44571, 79902, 26028, 77887, 117828, 65872, 48866, 22019, + 51481, 20695, 6816, 15468, 2187, 56218, 56453, 72760, 10559, 23157, 89968, 337727, 68019, 20454, 32792, 91871, + 18021, 19811, 54181, 19785, 54895, 74209, 9868, 171144, 48247, 7928, 165277, 1104, 18354, 39238, 60324, 127799, + 33737, 46441, 21125, 16451, 4554, 138540, 104181, 92183, 108595, 35070, 341328, 119439, 37288, 53479, 244682, 62059, + 39767, 90048, 55516, 10156, 116707, 177198, 93755, 76515, 9175, 160572, 523, 82789, 3726, 1887, 5577, 952, + 108877, 14261, 74693, 168738, 21616, 69086, 93069, 2668, 83684, 168326, 89980, 40475, 49862, 993, 36286, 134733, + 1938, 1799, 17016, 106908, 8543, 213196, 68388, 64036, 276466, 31847, 29247, 15775, 20134, 20323, 38300, 6990, + 108498, 60387, 10008, 41213, 48770, 96703, 53469, 375632, 102254, 16286, 56810, 71072, 31914, 93491, 252525, 99457, + 131520, 360748, 153227, 148240, 185291, 21359, 5538, 120533, 100559, 7782, 22500, 110558, 51890, 8141, 109666, 34083, + 209, 2223, 43177, 12541, 19526, 38778, 39701, 58422, 76471, 57840, 69298, 168436, 50544, 120544, 5588, 4904, + 71814, 6780, 99075, 11363, 99351, 96073, 4464, 71241, 8172, 116428, 160177, 72875, 198526, 85587, 21266, 28059, + 28816, 58105, 282844, 53464, 20419, 81082, 260276, 6827, 9109, 38541, 171515, 229032, 98389, 150153, 15498, 10140, + 136777, 110487, 92940, 42991, 76479, 19523, 10372, 21173, 75596, 28853, 81504, 63448, 30635, 135311, 13245, 254897, + 165417, 61091, 49656, 45428, 48216, 44529, 18414, 915, 120220, 22939, 12949, 64687, 25955, 67099, 25048, 22602, + 211071, 8500, 87865, 14625, 39314, 101395, 81563, 141087, 24308, 43453, 50836, 133590, 32164, 3576, 59438, 10284, + 2559, 731, 9526, 222252, 146280, 87697, 10901, 52647, 43403, 137838, 74591, 33197, 9233, 63356, 132528, 799, + 17947, 41186, 9082, 7417, 90585, 39849, 50863, 90290, 1786, 8152, 144979, 35008, 86920, 156078, 59343, 104076, + 125662, 127008, 111645, 36851, 2457, 87365, 8649, 14841, 64741, 69960, 475364, 42995, 32152, 78057, 117362, 50703, + 32918, 3669, 219900, 142020, 122882, 25093, 37632, 59532, 78538, 31923, 14695, 27152, 44001, 30500, 34140, 87080, + 31723, 21082, 71987, 36698, 56426, 225949, 5796, 1809, 23375, 125781, 9456, 168878, 103172, 15329, 86662, 97895, + 88177, 17270, 67948, 56739, 145697, 32730, 25167, 10080, 52506, 49488, 112733, 42661, 49319, 110135, 27599, 51073, + 68016, 69079, 90323, 101226, 31664, 20967, 163445, 48192, 41488, 41500, 17028, 27408, 18486, 193593, 92824, 60145, + 110400, 6284, 10148, 99305, 7030, 8019, 15366, 78496, 28241, 25688, 95511, 86416, 986, 31151, 29745, 215965, + 109199, 10425, 23864, 62113, 19900, 54264, 8171, 31738, 42147, 21664, 39774, 126916, 56864, 9927, 2688, 85157, + 65287, 33019, 23537, 6543, 49183, 29757, 1968, 29750, 7858, 70193, 1211, 62048, 24000, 35355, 53720, 246, + 30453, 1963, 267877, 27516, 840, 101181, 36344, 426, 16673, 136627, 20509, 19180, 25907, 70101, 11239, 56975, + 68916, 9194, 87686, 38354, 89927, 16469, 7125, 48706, 6309, 37837, 22274, 71780, 158335, 39840, 82643, 75960, + 36407, 49670, 1989, 14982, 199737, 14639, 22843, 120678, 130464, 46783, 21419, 13639, 13137, 127847, 144799, 159573, + 59603, 17603, 2824, 59934, 201778, 13312, 36351, 40000, 195292, 19891, 2315, 33443, 21015, 43785, 3891, 2636, + 42466, 7039, 20196, 103143, 179062, 154987, 42658, 47584, 172561, 40055, 48647, 12739, 89492, 8870, 14895, 170930, + 11075, 73935, 31376, 34999, 180407, 46254, 20836, 59075, 31868, 41037, 39680, 25099, 72493, 14732, 26639, 55562, + 20998, 611, 41758, 4739, 60217, 85527, 54988, 99046, 9865, 99927, 2352, 15721, 19530, 86459, 1125, 66190, + 39274, 9720, 82870, 3364, 130266, 15887, 122881, 84869, 54539, 14106, 293942, 63127, 21623, 10194, 31975, 38172, + 12535, 149, 62630, 75752, 13505, 13805, 8770, 88088, 40641, 208378, 14393, 38913, 73375, 44350, 32840, 22198, + 4087, 10333, 34430, 44812, 85633, 122365, 38608, 134765, 2153, 192171, 2383, 3536, 3117, 54286, 96428, 28420, + 70680, 12317, 147982, 4363, 55788, 39340, 6445, 40899, 92096, 24356, 221898, 26647, 47509, 10792, 131852, 24256, + 98493, 19086, 25386, 75057, 37490, 10028, 45730, 184141, 271936, 286712, 1729, 81050, 151475, 2718, 8857, 7068, + 3032, 70545, 10863, 172184, 16171, 61405, 54726, 3906, 96642, 17628, 2754, 24338, 34106, 1369, 8054, 98154, + 78425, 52080, 132116, 61228, 125761, 72327, 27960, 23032, 19591, 19659, 80147, 103860, 3366, 33018, 35292, 39070, + 19316, 88042, 12272, 53033, 54508, 18901, 21506, 21455, 24183, 122461, 20317, 27159, 50786, 118677, 19298, 4344, + 194248, 97414, 153639, 16051, 91528, 37589, 38898, 5339, 33253, 113074, 39403, 51508, 34622, 24505, 11212, 76907, + 108355, 126229, 77678, 8205, 41741, 10599, 69948, 101917, 705, 49260, 68715, 39750, 3814, 111125, 108544, 115867, + 3883, 144663, 33293, 7255, 108929, 13737, 90748, 14774, 13203, 10588, 17244, 84607, 67831, 29180, 50860, 106727, + 10142, 30125, 120708, 58131, 59754, 66460, 103711, 43126, 68208, 9476, 5110, 156651, 29128, 145052, 14949, 151686, + 84820, 79229, 57233, 1002, 56880, 47904, 10010, 11807, 38794, 32039, 2140, 3550, 24972, 116011, 47660, 76086, + 48571, 130683, 18131, 4450, 40821, 39353, 38655, 33743, 40476, 135664, 142086, 29489, 18137, 67555, 45205, 115281, + 164254, 261470, 1105, 128217, 24064, 28118, 111832, 44643, 236309, 17929, 3024, 138171, 79181, 14368, 22266, 37872, + 11282, 10626, 56113, 43632, 395, 41031, 1026, 152342, 39169, 124364, 66473, 4684, 2013, 12270, 120501, 142296, + 51587, 29013, 3177, 129452, 28551, 23498, 8358, 22595, 9645, 92008, 40742, 106126, 701, 194074, 1806, 54493, + 109513, 2084, 33125, 5622, 115899, 14353, 11000, 60472, 113566, 150803, 15891, 40791, 12762, 53130, 36257, 13420, + 62045, 128143, 272516, 213527, 58322, 32400, 115427, 101476, 103726, 20176, 21502, 355183, 41343, 23892, 15753, 2312, + 40866, 54294, 101785, 55146, 244102, 32705, 27994, 13284, 143816, 157993, 81010, 16933, 2490, 6072, 23250, 13602, + 70346, 112144, 297560, 55477, 40973, 56881, 19001, 130715, 52618, 12974, 11195, 11003, 15412, 7646, 11175, 7316, + 30720, 44809, 123585, 100767, 104315, 95294, 104855, 7533, 38917, 11608, 3316, 171486, 42027, 49423, 58544, 56254, + 93676, 12514, 18792, 45592, 17513, 1245, 48068, 120907, 107418, 26207, 55327, 89145, 20706, 175898, 72652, 100555, + 123890, 3322, 87742, 9460, 13399, 59577, 26311, 16724, 97727, 64005, 14658, 4457, 24044, 49860, 32409, 21567, + 87962, 27289, 14624, 50763, 65606, 23475, 207482, 25541, 44250, 48058, 12271, 88998, 98044, 55548, 2331, 52575, + 65276, 9350, 77725, 56779, 74790, 14114, 85990, 42433, 101473, 7844, 52309, 157284, 19350, 39922, 12466, 21036, + 91570, 95031, 74187, 95245, 1326, 34475, 71765, 28558, 87790, 354, 27116, 254543, 172042, 18412, 35291, 39776, + 7577, 28592, 29654, 42244, 100813, 7588, 48319, 48295, 86896, 40289, 229422, 2570, 85891, 10264, 76701, 52231, + 41512, 127376, 230534, 93605, 108130, 23424, 36739, 233035, 27794, 95965, 35884, 151494, 14875, 843, 49833, 38010, + 71431, 219272, 82428, 214192, 12835, 127240, 14345, 138207, 156250, 50941, 99644, 99417, 58135, 7353, 39821, 29812, + 3759, 7415, 14572, 53830, 22476, 49857, 78175, 14915, 42176, 77853, 6530, 12126, 115873, 21641, 122781, 72383, + 34686, 27915, 136002, 76130, 56523, 25687, 6527, 134727, 73643, 74722, 67478, 8251, 108505, 23843, 145891, 48731, + 51491, 16182, 53915, 16603, 20838, 7395, 115375, 138355, 12721, 58670, 15892, 3735, 32863, 124677, 12604, 11997, + 45700, 14999, 258154, 2720, 29496, 271082, 85521, 39973, 13700, 113046, 44485, 36482, 34294, 47751, 134797, 155904, + 55360, 134690, 21770, 91135, 29206, 144711, 48744, 44674, 155942, 59875, 17918, 64497, 123812, 9144, 234966, 164534, + 7706, 5270, 25090, 20339, 163234, 3097, 133900, 125161, 203, 10715, 9667, 87129, 132720, 92850, 3501, 14636, + 49358, 17266, 21111, 30612, 144431, 122860, 101497, 17673, 8130, 11884, 91167, 88623, 175788, 32729, 35605, 166925, + 35818, 36536, 38809, 2716, 3270, 93973, 82603, 23366, 76832, 61965, 62245, 13893, 25710, 144091, 98814, 44208, + 54095, 236277, 53367, 34834, 97274, 2172, 16858, 49284, 28779, 113183, 4643, 106217, 94621, 164943, 16845, 19253, + 74396, 22592, 87503, 34996, 19092, 146507, 19116, 134652, 128242, 22736, 1007, 132190, 58067, 27936, 48566, 40563, + 20885, 33771, 80664, 989, 14670, 14315, 21661, 187703, 93255, 27617, 245729, 44376, 15107, 49824, 93604, 106721, + 63291, 5606, 153280, 101864, 23654, 28688, 6737, 43584, 126900, 7137, 67499, 145087, 129421, 24707, 105699, 311580, + 59294, 11582, 211232, 92185, 262659, 717, 4752, 31126, 9798, 18631, 28374, 3367, 3251, 154411, 52363, 51023, + 67344, 70678, 261560, 78059, 28600, 18070, 79850, 53359, 44629, 30869, 19073, 64045, 50672, 63508, 37203, 78992, + 29072, 93421, 104033, 26081, 26999, 121749, 113974, 301732, 31526, 3016, 52083, 135740, 23183, 10650, 107815, 49863, + 49175, 1554, 10166, 34286, 165843, 102866, 56807, 29193, 175455, 36495, 50639, 18134, 17282, 14831, 3286, 19214, + 175411, 85620, 44203, 2339, 32022, 31760, 24711, 84552, 45989, 38675, 25767, 121791, 30298, 7929, 8128, 324, + 90690, 46242, 120990, 8574, 78118, 72361, 11333, 68279, 83156, 26766, 288, 27097, 23749, 6805, 96767, 122167, + 35636, 198501, 41641, 29661, 31317, 217715, 8631, 12460, 7069, 78590, 46516, 87449, 80381, 45698, 49298, 44290, + 94561, 24990, 13323, 11057, 133756, 4423, 12607, 21852, 14960, 88023, 10455, 58146, 97404, 38753, 9405, 216304, + 6138, 24563, 206624, 146948, 41065, 115571, 46443, 96844, 78041, 42616, 20236, 11182, 32843, 47724, 12361, 122382, + 16601, 14468, 3252, 1927, 34123, 58038, 88840, 63230, 9559, 119391, 241176, 12638, 146529, 181367, 152506, 46831, + 123377, 113569, 91896, 1930, 96395, 154527, 81091, 102845, 54441, 92585, 4800, 95396, 42012, 27534, 62635, 8447, + 84257, 129409, 12110, 25104, 123541, 51131, 36346, 44078, 10745, 2994, 120391, 4597, 22139, 55241, 51317, 125652, + 106459, 267836, 35138, 15293, 11720, 7525, 66335, 62591, 150883, 14682, 152240, 38920, 172651, 34616, 1931, 48367, + 154996, 53262, 13826, 3119, 110538, 5436, 54461, 84681, 4728, 16350, 250445, 69950, 53447, 94853, 13819, 81759, + 25704, 73967, 266094, 36612, 2009, 10054, 152119, 181823, 71152, 47335, 164288, 150538, 119354, 57186, 395013, 2941, + 21166, 126317, 18555, 208602, 66604, 161278, 109157, 190726, 1429, 126459, 64236, 8007, 216162, 49573, 170973, 56109, + 8637, 53811, 49583, 16200, 122250, 46869, 24350, 5723, 63520, 11915, 9305, 41525, 27180, 215488, 96890, 13022, + 29172, 40217, 11913, 5985, 36124, 147278, 26010, 452445, 276428, 85611, 7347, 12127, 107787, 9651, 6341, 2892, + 77184, 25332, 29107, 20014, 4680, 81466, 12892, 58258, 14952, 10238, 70027, 35013, 104903, 128969, 46523, 15173, + 24418, 103787, 139284, 30348, 14793, 9646, 156700, 29992, 51561, 15377, 15544, 84114, 52931, 20387, 129004, 6592, + 8371, 57164, 36658, 17268, 26342, 292, 19324, 116062, 48526, 23357, 38167, 14524, 7118, 263554, 155894, 242917, + 92066, 33509, 27485, 107820, 67280, 197352, 32547, 18520, 40653, 68664, 90091, 69796, 8847, 81434, 4751, 106853, + 34597, 37923, 18335, 221713, 105438, 25972, 14464, 111785, 14198, 134281, 5575, 71227, 50163, 102398, 17570, 101686, + 61188, 22480, 52263, 56951, 146286, 15008, 39203, 25408, 50315, 2155, 73767, 13685, 41205, 94069, 6395, 180692, + 170829, 2835, 2103, 11342, 161496, 89214, 6026, 1302, 585, 38860, 110361, 40025, 197359, 39048, 82914, 220968, + 95056, 26946, 162638, 65364, 1687, 34647, 33433, 75300, 365794, 57865, 121192, 320880, 91349, 18120, 6915, 106455, + 48507, 20917, 41330, 101793, 5804, 46924, 91838, 39919, 49263, 77778, 119562, 26736, 5146, 16996, 48515, 149815, + 104902, 87328, 6533, 151190, 96365, 32904, 4018, 7595, 117343, 138520, 33658, 181860, 132222, 36765, 26173, 59136, + 46084, 53703, 43164, 216719, 96919, 95045, 33006, 96990, 16875, 83496, 45441, 27720, 8073, 17015, 124868, 179271, + 262381, 118057, 199816, 4275, 79982, 123100, 54391, 90904, 52663, 27953, 77709, 4272, 13928, 39051, 71112, 413271, + 16056, 87986, 33056, 18596, 153908, 120302, 27857, 128911, 15145, 60790, 4586, 67318, 109256, 79780, 45306, 11932, + 132308, 106229, 37412, 1507, 18763, 133449, 58137, 37961, 24904, 86499, 21760, 41606, 167644, 77227, 120713, 43226, + 13618, 151594, 108301, 101213, 40750, 66225, 16687, 80402, 18686, 48613, 45656, 10805, 147124, 34576, 19977, 157309, + 114709, 36792, 223317, 58062, 150038, 9205, 150642, 21252, 52849, 184323, 41421, 43314, 2938, 99855, 60463, 129217, + 12568, 75505, 125705, 141476, 48617, 43014, 23373, 19138, 8778, 94674, 178893, 8058, 5459, 94724, 266341, 80369, + 44202, 274013, 86858, 78320, 44591, 24273, 35983, 13078, 74914, 24190, 202665, 27165, 17183, 37327, 73294, 34055, + 248689, 18437, 74717, 975, 13878, 8774, 64644, 71823, 7822, 6524, 56622, 7221, 80060, 16273, 88677, 19383, + 23116, 127134, 154899, 68336, 194037, 241, 4615, 387990, 18487, 147941, 15391, 26006, 2067, 26484, 4709, 66156, + 7798, 2820, 26469, 48765, 43077, 66027, 3606, 27342, 33678, 18421, 7829, 55334, 244028, 32856, 8103, 147672, + 31320, 49696, 49702, 69018, 74273, 120264, 122020, 31808, 2794, 1867, 52845, 55295, 19466, 19329, 26414, 52167, + 8218, 210642, 13654, 127707, 36280, 64707, 8564, 114550, 183997, 115833, 23481, 2828, 48124, 16340, 247, 309535, + 19416, 66487, 105120, 17809, 29656, 47021, 201858, 160017, 19280, 78447, 21406, 39940, 98734, 122438, 36909, 83000, + 7715, 134747, 24473, 75401, 18311, 9336, 15781, 55262, 30701, 9503, 53173, 8524, 133602, 90200, 85568, 13433, + 150307, 24618, 45822, 23920, 126956, 23395, 51943, 161287, 7378, 37763, 121912, 32196, 2933, 19681, 9297, 23283, + 79903, 162071, 55832, 23137, 13178, 209527, 9418, 18467, 12593, 119121, 83307, 51772, 86571, 52216, 23349, 61608, + 26604, 94854, 7575, 204746, 92446, 22189, 28677, 83268, 13574, 65266, 57786, 61446, 44205, 26661, 107681, 91716, + 65699, 82006, 71722, 70318, 427599, 64286, 24112, 101430, 21118, 144724, 36334, 80349, 8245, 44912, 45640, 4276, + 14407, 1378, 31347, 22711, 27877, 62250, 191999, 8464, 47936, 361021, 134866, 3976, 33542, 48109, 10852, 237219, + 7916, 122443, 17484, 70725, 26460, 109271, 187381, 128014, 117762, 246379, 22586, 82080, 21596, 9055, 4112, 313930, + 37818, 163471, 108924, 39581, 2249, 13074, 7431, 75393, 127359, 35494, 111020, 59720, 209394, 14051, 37123, 41719, + 60044, 39589, 172595, 143831, 47677, 59508, 24424, 103244, 308089, 7173, 30716, 90486, 36791, 60870, 101299, 108569, + 154971, 20715, 53888, 53901, 31417, 53959, 2728, 97125, 6998, 145934, 80463, 20412, 150042, 45242, 187078, 7592, + 174360, 31180, 135274, 14872, 12255, 11730, 93361, 83265, 43009, 49328, 20215, 21789, 29335, 115863, 28254, 39036, + 48739, 17655, 180783, 12531, 3912, 8728, 149240, 87661, 101398, 77276, 44669, 83920, 53340, 37769, 16150, 114794, + 2580, 157793, 7167, 176552, 146939, 5647, 16877, 13855, 151295, 90625, 68053, 91363, 45360, 176357, 42521, 30918, + 125275, 57667, 44098, 88883, 15273, 61531, 4145, 110202, 45383, 253407, 75996, 20238, 27456, 20964, 4620, 8638, + 48761, 12194, 52351, 99825, 23314, 86104, 80185, 11840, 8750, 45404, 19895, 71824, 60801, 13493, 12198, 37669, + 85823, 77592, 114135, 4393, 104759, 210654, 28711, 86484, 27894, 35912, 20614, 184536, 123685, 134244, 44348, 10862, + 66968, 165136, 18647, 32424, 39480, 60719, 93213, 164629, 26917, 39935, 75223, 75930, 55290, 22975, 122323, 10513, + 83305, 99747, 19500, 77541, 2696, 77867, 101290, 65344, 98390, 12816, 83594, 39244, 57569, 8468, 34263, 35071, + 145853, 12146, 76941, 17746, 340733, 25910, 37273, 43127, 4919, 60464, 1992, 49562, 20024, 15497, 109739, 14126, + 52268, 76976, 6239, 8384, 30884, 19155, 135974, 107147, 7413, 64546, 38363, 186999, 203685, 81795, 108201, 36022, + 70989, 30403, 116774, 5379, 112855, 28461, 9025, 58269, 4129, 17604, 104476, 174522, 50536, 1225, 10900, 36288, + 349518, 20856, 101414, 45229, 68205, 77322, 197569, 10875, 332641, 23358, 85554, 14077, 159581, 34721, 18791, 98700, + 135361, 167675, 50000, 42501, 236026, 75571, 5992, 17097, 37563, 20166, 101652, 40721, 176404, 19063, 36155, 59023, + 64899, 33196, 98793, 70148, 6578, 69024, 242235, 52605, 122033, 81904, 12809, 15158, 63871, 107782, 1840, 4872, + 1850, 29779, 34125, 98682, 85234, 116592, 82990, 12554, 82089, 63993, 168833, 149888, 66124, 10489, 196009, 53058, + 74145, 69106, 89715, 51982, 121098, 13559, 95458, 107427, 107351, 113368, 3064, 70286, 12687, 4839, 22981, 48279, + 36881, 55099, 198175, 73274, 117334, 75733, 3562, 5484, 13136, 64209, 184119, 61973, 14698, 16622, 73579, 36910, + 85933, 42774, 90601, 40132, 93866, 19937, 45703, 23982, 18047, 5362, 11140, 39687, 32620, 25714, 60265, 77976, + 310, 77325, 31712, 280974, 15094, 86261, 43015, 13789, 46000, 100529, 1116, 137536, 88451, 20938, 134125, 17791, + 23632, 63779, 256927, 29623, 36645, 219053, 83430, 48090, 27940, 27300, 14780, 109338, 87618, 34615, 102225, 42200, + 80520, 6619, 74506, 194075, 58892, 76807, 27201, 60732, 20976, 110501, 170773, 84913, 27702, 21417, 5342, 72467, + 140090, 178520, 122111, 33447, 96075, 4699, 53512, 17029, 67841, 174853, 17388, 16526, 103292, 6179, 27759, 37717, + 238964, 22906, 23675, 59996, 87778, 65471, 88269, 10094, 107338, 46577, 43856, 31927, 17019, 15, 159648, 72870, + 83427, 14710, 153353, 15418, 67522, 32362, 44785, 3937, 7302, 108965, 41424, 55752, 8261, 138626, 43498, 8777, + 47570, 24353, 207472, 131958, 116787, 3156, 34108, 58412, 8730, 6898, 90267, 28837, 80484, 59214, 9439, 19087, + 61772, 5722, 23159, 81837, 174422, 59842, 18102, 171770, 56005, 80656, 119313, 22496, 131193, 22339, 3665, 14366, + 52875, 107272, 15645, 70022, 14588, 34482, 10876, 36184, 28471, 60828, 82588, 55312, 59817, 102390, 8845, 87428, + 9586, 17179, 96409, 90979, 15720, 107181, 33777, 113545, 158155, 4326, 32422, 188604, 51307, 29601, 10067, 16201, + 109734, 3998, 129448, 141025, 15196, 18904, 83270, 17683, 11025, 19743, 21493, 1552, 59331, 7581, 25652, 89130, + 3884, 119328, 75611, 26479, 52626, 13590, 195115, 6807, 102602, 52190, 72507, 52467, 84797, 6467, 2334, 47785, + 25158, 132580, 142359, 76578, 27314, 24766, 3969, 27500, 4437, 254621, 65367, 168149, 20488, 13843, 62310, 21136, + 76214, 12671, 23644, 139645, 189248, 55341, 3302, 14123, 35023, 48936, 11475, 55049, 114952, 51698, 297764, 80767, + 157376, 20881, 13336, 53207, 2827, 59849, 16592, 52991, 62439, 275657, 47305, 117196, 102878, 60777, 83027, 10464, + 129749, 26707, 251779, 125019, 36405, 33132, 25562, 70233, 10329, 28189, 12385, 71977, 109339, 157513, 195426, 70901, + 108633, 55857, 3229, 108445, 36662, 75267, 19054, 102039, 1254, 2391, 173276, 29374, 13267, 51771, 225351, 7237, + 5470, 98738, 14316, 209876, 87105, 107056, 16157, 44140, 58004, 88474, 31907, 11831, 15397, 11396, 39700, 90254, + 10434, 316, 22682, 60529, 159667, 46855, 41392, 55643, 21864, 20640, 1483, 48142, 52022, 14052, 14307, 23845, + 179464, 105497, 8112, 170979, 3013, 22501, 58766, 9748, 32147, 168407, 69486, 20613, 31496, 139990, 50815, 4616, + 116832, 5525, 13988, 136236, 18494, 72230, 42950, 16966, 105502, 65693, 12439, 22036, 129227, 24413, 66758, 60339, + 107267, 12682, 60700, 199944, 50033, 64809, 169, 18106, 13481, 63773, 97251, 21927, 9954, 10994, 61753, 132964, + 24267, 269121, 315805, 17090, 22141, 89553, 60, 41720, 16257, 267739, 11800, 148430, 116154, 107691, 1495, 7110, + 36185, 30330, 15572, 9879, 950, 164278, 12417, 189181, 115685, 238527, 61115, 3001, 11305, 185925, 125509, 80597, + 176708, 23407, 47313, 198512, 2048, 137403, 10323, 39029, 178671, 3975, 76003, 84147, 112573, 78996, 69042, 6992, + 287867, 46759, 5275, 30027, 162328, 18790, 274565, 55200, 497663, 89305, 12413, 26108, 95170, 13648, 15748, 78466, + 50979, 40335, 43390, 12453, 193861, 466, 66161, 87201, 50987, 94748, 32988, 22018, 30368, 175641, 225636, 53920, + 136257, 72869, 274820, 12673, 31830, 21228, 5225, 4705, 46549, 105854, 3199, 16143, 15119, 48337, 134864, 57495, + 169876, 45311, 13298, 54322, 23788, 14950, 42143, 49801, 17462, 217840, 101953, 63412, 249887, 9473, 75292, 729, + 57377, 66067, 6712, 114078, 1949, 92149, 44063, 86259, 35448, 19529, 20598, 19485, 14791, 73304, 47309, 366, + 43769, 14667, 98883, 25339, 210091, 45717, 49613, 6739, 3783, 165357, 28175, 87394, 34612, 90151, 838, 17747, + 282103, 59822, 30329, 68354, 88380, 37787, 19846, 9765, 245190, 63635, 236, 10025, 5457, 17502, 12156, 204683, + 20491, 39054, 8070, 50911, 98908, 99317, 12186, 49875, 11402, 152980, 22961, 22075, 86899, 34073, 14755, 13282, + 117916, 33225, 72465, 158235, 16028, 64557, 36732, 123058, 162584, 20479, 31391, 63733, 60644, 62683, 9917, 17310, + 320177, 239647, 43989, 8876, 156096, 31699, 114739, 105603, 31065, 78174, 95969, 9220, 55876, 38786, 58411, 214643, + 22000, 140310, 88748, 26085, 77655, 28585, 109281, 7624, 9992, 7234, 98903, 60178, 23397, 75321, 53549, 53032, + 13757, 109032, 64081, 19046, 317623, 1946, 71713, 62867, 63978, 3151, 56924, 47304, 215255, 182040, 74590, 48012, + 2443, 24268, 69427, 33, 17648, 12518, 40668, 140511, 93231, 72823, 28791, 47052, 27388, 55205, 30906, 57350, + 104529, 63338, 26728, 100624, 93807, 107269, 82876, 163825, 55505, 18547, 30605, 14974, 140477, 81686, 69012, 120278, + 12046, 5605, 47923, 212928, 112040, 108335, 21336, 38303, 70887, 143240, 19855, 87583, 40152, 15587, 66375, 29394, + 94365, 54674, 43272, 133291, 112353, 210955, 121711, 331352, 25063, 35707, 69840, 56454, 114679, 66792, 86743, 446056, + 266061, 12010, 82324, 96915, 71248, 22037, 64756, 25091, 119555, 13915, 28628, 6675, 15589, 106123, 53880, 102126, + 2244, 55885, 39035, 5738, 617, 441160, 19553, 2220, 14129, 86275, 107256, 166687, 211431, 81495, 104573, 290527, + 70110, 99357, 25187, 33256, 100652, 31225, 142056, 82578, 7777, 23449, 58276, 64423, 4383, 192266, 134799, 66044, + 85911, 76876, 90974, 103947, 89221, 15961, 109118, 116095, 21010, 92406, 79525, 73542, 120615, 156252, 287243, 95281, + 58357, 14409, 38018, 33736, 86405, 83837, 55695, 9631, 37554, 27881, 106045, 6071, 41647, 35485, 108389, 46354, + 18, 34159, 180231, 126085, 15143, 13910, 144853, 93597, 69662, 64563, 23068, 38208, 60491, 41204, 62238, 141119, + 14714, 38001, 3276, 56189, 186134, 91982, 65866, 6904, 148344, 128140, 61946, 3683, 42347, 4969, 38485, 12705, + 5410, 78157, 91933, 103926, 168175, 15606, 288735, 6045, 44535, 20558, 34571, 13823, 42449, 6093, 11546, 37307, + 343894, 81874, 112517, 50738, 129417, 141962, 5390, 118181, 99682, 16804, 16660, 107424, 20659, 45287, 22461, 55188, + 27272, 10167, 12088, 17401, 140483, 17330, 78850, 45339, 63455, 16978, 199211, 80486, 222159, 13457, 1094, 18869, + 17536, 103284, 31050, 17962, 13722, 71867, 76368, 286932, 42637, 10567, 2, 199412, 62324, 36676, 8181, 6401, + 11976, 162, 87196, 78376, 114691, 1307, 30802, 3711, 148109, 74365, 57920, 81357, 2283, 214679, 12573, 18531, + 32057, 44583, 212119, 44264, 182393, 154286, 35753, 8258, 4295, 13186, 83291, 4736, 147364, 121944, 59024, 73352, + 33705, 31001, 88498, 32882, 2075, 163404, 10634, 11500, 44303, 65212, 13112, 34394, 30274, 20847, 64779, 22883, + 28331, 12500, 41997, 11945, 63740, 75992, 39703, 69806, 69740, 66087, 128693, 81128, 29148, 111583, 148435, 31535, + 10346, 159917, 58414, 28273, 44862, 59, 33364, 21068, 33716, 46470, 1804, 73963, 73937, 219049, 103305, 218321, + 153333, 233125, 63775, 105390, 12930, 127548, 60730, 109562, 38784, 53330, 627, 73923, 247159, 30199, 38362, 48641, + 2515, 19205, 34500, 48846, 41053, 21212, 105356, 15212, 20256, 3616, 21849, 17211, 83368, 97044, 352396, 108368, + 256189, 196693, 4226, 14922, 54639, 25114, 35849, 320, 115240, 41400, 60330, 23377, 5096, 75542, 3178, 56889, + 24661, 12431, 135214, 63314, 175419, 239496, 41215, 13379, 153552, 73270, 37517, 105147, 26516, 27776, 68242, 1357, + 141, 71029, 155582, 76053, 138176, 8168, 4134, 134075, 63885, 2519, 46027, 51951, 34115, 79709, 5576, 10203, + 47222, 15435, 34313, 43154, 55709, 9408, 82175, 88231, 2765, 52283, 66501, 65412, 28479, 574, 36643, 57430, + 38875, 293893, 16069, 28162, 236608, 26442, 8613, 1622, 12229, 37482, 74496, 33264, 22921, 79056, 39384, 129956, + 77291, 20463, 58349, 109725, 54426, 106360, 47581, 90881, 45388, 31487, 128197, 71530, 2860, 39582, 46028, 81826, + 57787, 259117, 67523, 307547, 114579, 104215, 18723, 115699, 295139, 4428, 43172, 26618, 105782, 14781, 30313, 18939, + 17826, 45064, 69594, 13204, 71066, 58366, 15231, 157682, 19119, 15787, 91984, 89607, 54364, 23607, 79019, 145509, + 69385, 6604, 40313, 81481, 16568, 32756, 79187, 77128, 12323, 56725, 270449, 8124, 28057, 84040, 65629, 72616, + 33346, 127078, 185998, 104454, 34919, 182273, 14609, 19947, 124763, 41923, 73459, 52863, 155533, 54653, 13742, 70467, + 101386, 30438, 9573, 50346, 31644, 14370, 50550, 451, 8627, 280645, 27470, 64783, 49001, 44841, 60011, 84880, + 303600, 29723, 249679, 20808, 29868, 17505, 9061, 24139, 63213, 37901, 19817, 58724, 9103, 3211, 42909, 2761, + 77280, 71424, 62334, 7307, 71333, 27508, 6331, 83663, 9696, 179679, 27464, 32253, 138789, 36870, 23709, 126072, + 37059, 130604, 83178, 283517, 24823, 22443, 72508, 24678, 5057, 22926, 13846, 74055, 21352, 14335, 3461, 51988, + 32368, 11407, 71381, 7895, 114208, 12515, 35745, 29717, 56803, 108507, 67583, 54834, 19424, 38668, 35778, 15261, + 10445, 67937, 56244, 3340, 58514, 22440, 11243, 68666, 8661, 40929, 111708, 37637, 209508, 124495, 206903, 91080, + 26187, 11633, 66771, 30871, 171838, 197948, 8289, 57706, 10460, 80625, 36923, 31612, 63454, 35105, 59184, 7032, + 14016, 40322, 27055, 86917, 122504, 79371, 130575, 6177, 41328, 5209, 78779, 4821, 21329, 41539, 62219, 83753, + 46618, 7613, 15027, 118360, 32493, 225969, 819, 120335, 38225, 84581, 17131, 15188, 7855, 3105, 30340, 78018, + 31763, 5801, 31461, 10241, 7945, 13882, 30152, 4527, 29876, 21438, 25462, 64433, 8734, 24877, 74945, 14455, + 6438, 107317, 52662, 50955, 24205, 109805, 38229, 13383, 97490, 25051, 32380, 7096, 139977, 101853, 1428, 42064, + 130740, 33630, 82143, 110070, 47323, 64635, 163313, 6594, 33195, 6607, 12008, 3521, 85390, 10031, 28761, 7750, + 57194, 65848, 171501, 76792, 13813, 65534, 81423, 13132, 60600, 4409, 31583, 87558, 21313, 106333, 155905, 153496, + 96251, 3406, 49514, 9434, 7699, 87327, 46359, 66114, 27584, 74162, 19886, 252277, 170521, 22092, 82253, 115261, + 139271, 47533, 52680, 202618, 1363, 11026, 363255, 99673, 4402, 33612, 20302, 28039, 336738, 1226, 38272, 13620, + 129223, 13205, 3958, 19963, 84983, 18752, 42515, 68608, 69150, 39813, 30549, 41506, 13147, 4080, 5087, 15836, + 3590, 70019, 16566, 11738, 163929, 27964, 83106, 22713, 207225, 34956, 53824, 5181, 155260, 90376, 13809, 102964, + 55916, 44273, 1261, 26537, 20288, 96003, 17325, 62927, 4503, 126513, 63919, 15556, 8398, 46320, 36814, 33274, + 98490, 42874, 152755, 42575, 11773, 27872, 33828, 220575, 27512, 172804, 16418, 95543, 37113, 708, 88986, 499, + 84976, 72480, 35781, 122997, 86558, 582, 113958, 53612, 28365, 4495, 11592, 12890, 11756, 81693, 108202, 42686, + 116005, 805, 108429, 99193, 182148, 10373, 83443, 91669, 13733, 56065, 75316, 204077, 115313, 322052, 15302, 23067, + 47644, 13460, 67103, 57307, 67208, 156322, 235278, 94136, 85069, 1418, 167462, 56633, 9347, 2641, 44606, 21287, + 14995, 12747, 17526, 494, 226141, 75964, 18039, 39483, 14704, 59304, 1316, 195645, 101835, 141208, 116613, 223219, + 41159, 49846, 61222, 43002, 35314, 80457, 23691, 63429, 13113, 36373, 32330, 8361, 63526, 11029, 32963, 93376, + 214039, 18769, 114006, 1087, 29978, 114423, 64428, 31774, 50446, 49295, 34202, 73765, 83339, 74913, 27287, 43834, + 17440, 83606, 93489, 60736, 129441, 57951, 84464, 118033, 72522, 2386, 5028, 276747, 118641, 55191, 48057, 16621, + 97816, 39006, 3084, 207838, 24907, 92233, 46363, 526, 73844, 2610, 25911, 60919, 15717, 85425, 136040, 70858, + 118884, 18348, 172680, 47190, 167255, 11777, 29552, 95596, 96509, 115491, 19228, 15022, 162793, 7055, 54334, 45726, + 30847, 5690, 94006, 54915, 36849, 132591, 172983, 1103, 51297, 9369, 114578, 16962, 78974, 112537, 1229, 44919, + 77793, 29231, 3317, 48815, 10427, 19768, 100349, 70835, 1873, 26539, 93804, 85247, 2972, 161694, 16704, 323037, + 9999, 22014, 49315, 152394, 35074, 21009, 44279, 9515, 28190, 8647, 79457, 53333, 64297, 16040, 25316, 4727, + 146836, 234761, 5304, 67997, 46298, 88700, 122826, 40595, 60038, 132292, 15881, 9158, 163007, 47825, 221096, 91414, + 108919, 98428, 57919, 266818, 61219, 43879, 63697, 17096, 2403, 17970, 81556, 16083, 75022, 59692, 100052, 43977, + 127339, 40049, 119563, 17907, 4233, 35651, 47435, 66632, 110389, 15505, 25565, 177269, 69022, 40996, 21600, 19932, + 9833, 15084, 84838, 139091, 128097, 47428, 7050, 1360, 88016, 32635, 67695, 9688, 79390, 38684, 76616, 96096, + 222936, 46245, 114060, 64752, 22570, 74272, 1778, 64322, 94657, 115063, 64956, 78933, 28462, 67295, 52520, 30887, + 56956, 34609, 42753, 10281, 38803, 27122, 98080, 117097, 81536, 9071, 86592, 83849, 30474, 44691, 14751, 101014, + 152794, 5331, 6917, 12448, 19566, 55177, 112969, 39493, 16481, 84170, 80483, 83854, 147408, 10964, 69929, 25567, + 74574, 33765, 31137, 41456, 81895, 68446, 44249, 35692, 20731, 44729, 68861, 17500, 1918, 8121, 12733, 11511, + 1366, 110805, 162699, 45509, 76367, 53890, 22608, 6608, 187321, 92727, 87863, 8920, 54494, 50474, 3355, 13177, + 24366, 3750, 4900, 105058, 21690, 24548, 61391, 41587, 61696, 71254, 21133, 115118, 33283, 43646, 8976, 11273, + 107477, 5228, 145257, 193829, 77499, 89840, 14294, 15416, 31112, 552645, 70476, 18585, 414383, 91301, 197650, 85075, + 74362, 201919, 31008, 6857, 18463, 18747, 80565, 258617, 218441, 23173, 30916, 2323, 120929, 143751, 107064, 20153, + 59848, 60391, 289834, 31449, 258629, 45824, 75786, 49114, 201924, 23563, 35265, 68991, 69269, 10313, 17390, 67981, + 454, 162917, 7572, 144990, 19989, 55673, 279099, 65066, 13054, 54343, 55277, 3568, 59894, 15271, 53443, 417392, + 3623, 434, 45943, 1256, 58908, 173322, 64122, 81605, 20681, 59366, 25442, 39040, 35723, 390351, 35866, 146738, + 78523, 61914, 82593, 35139, 102680, 249269, 46578, 8727, 38988, 4624, 48520, 58695, 184112, 2027, 12940, 80034, + 108087, 27491, 213336, 944, 50944, 22396, 85571, 42349, 132704, 181305, 10849, 1371, 52966, 36306, 27461, 21214, + 21699, 5289, 15632, 19188, 18860, 17496, 112885, 63208, 96349, 12436, 34610, 13227, 201411, 150107, 21944, 52213, + 82697, 377613, 11201, 44987, 85395, 150187, 38121, 21649, 95658, 8788, 60763, 9623, 5093, 194516, 47673, 70064, + 6427, 97874, 32248, 15210, 177894, 45245, 129556, 10575, 44191, 36758, 23545, 141857, 32755, 22539, 162901, 95737, + 26961, 59210, 28436, 11081, 155739, 67439, 17384, 48388, 6249, 69756, 55378, 34764, 31310, 113162, 97192, 14330, + 81030, 18838, 35438, 13932, 26574, 120400, 101846, 130645, 84311, 174890, 23927, 28384, 120155, 7519, 67818, 55584, + 86730, 120665, 55022, 167671, 113535, 159014, 209287, 66396, 7424, 10800, 50330, 93120, 48888, 11155, 16081, 102627, + 13516, 33788, 137813, 74841, 45747, 35841, 21356, 6047, 98098, 4647, 73346, 95355, 20077, 37795, 143033, 12452, + 183995, 86627, 142356, 34744, 81945, 11420, 26913, 10645, 43210, 5817, 54484, 55730, 26704, 91611, 17668, 52476, + 40420, 51581, 44960, 150119, 75831, 205414, 177424, 80186, 45648, 15047, 63973, 15971, 11180, 3401, 50574, 45327, + 6855, 127, 294368, 30972, 57927, 224035, 22536, 5728, 65528, 17692, 2498, 20700, 124096, 78896, 81810, 53646, + 34851, 17737, 67858, 144208, 2598, 69506, 82387, 32829, 156633, 1376, 87537, 38787, 107572, 47042, 34484, 111751, + 127352, 97800, 89687, 4576, 38169, 10102, 247669, 1385, 123845, 61475, 17048, 8281, 60142, 15760, 189275, 5043, + 62722, 70423, 23446, 224950, 105584, 6317, 280103, 26981, 232364, 55457, 64267, 136900, 23211, 118951, 845, 54134, + 68120, 21238, 286023, 351719, 1601, 9442, 185113, 17851, 22169, 76287, 138957, 35304, 89299, 75522, 56601, 68253, + 747, 100548, 4081, 2246, 258039, 9694, 51725, 15738, 80572, 37864, 139906, 92854, 7258, 4446, 72431, 8572, + 152249, 3376, 43649, 25854, 11862, 510329, 182162, 28232, 101204, 130724, 77756, 137694, 8834, 65401, 19289, 56727, + 121434, 112217, 15745, 51403, 33761, 31323, 43153, 136892, 19175, 85899, 25269, 38059, 133142, 52989, 1777, 1223, + 46343, 18613, 28695, 87511, 40178, 223203, 15033, 135709, 48723, 7442, 84744, 52715, 3589, 26620, 71258, 37458, + 41977, 8646, 41763, 91920, 30210, 33735, 64017, 14173, 38868, 120841, 22212, 152812, 62257, 17607, 2045, 16253, + 10087, 16559, 33187, 61003, 82658, 60687, 45996, 131329, 87827, 28306, 207690, 248902, 90646, 20868, 109813, 9386, + 16415, 77631, 94695, 12715, 47552, 53324, 61324, 84409, 351723, 34328, 65573, 16236, 28298, 8448, 4731, 7418, + 72225, 87876, 82665, 6012, 91146, 32182, 85776, 95303, 272760, 52003, 18667, 6217, 1701, 78472, 2900, 125100, + 11295, 44716, 79152, 6181, 1652, 8164, 31057, 77402, 109651, 90467, 194601, 7484, 300747, 17565, 61525, 99466, + 51863, 9753, 72732, 128898, 198800, 907, 5759, 42875, 29446, 20325, 107060, 165430, 11794, 312, 5807, 20106, + 32345, 138270, 25904, 39695, 37538, 97877, 31477, 37750, 22356, 11523, 124253, 73049, 33102, 10210, 142709, 60863, + 37590, 128516, 64551, 64303, 113544, 313515, 181738, 31113, 37970, 55419, 66610, 21171, 11478, 7770, 228067, 47156, + 179743, 798, 147431, 50652, 25454, 19412, 37683, 4903, 103417, 111432, 203788, 3756, 59905, 35566, 26345, 21937, + 221970, 77922, 9738, 6303, 105196, 61175, 21598, 10726, 145604, 59463, 162519, 29420, 7817, 116768, 16102, 128966, + 164809, 62009, 21801, 6296, 102360, 37967, 27376, 20294, 16974, 106654, 129570, 55893, 75840, 88896, 68684, 52692, + 255333, 13102, 29474, 23608, 56902, 95016, 21221, 14815, 6659, 24894, 19248, 23158, 1954, 30789, 75920, 14310, + 645, 139270, 54809, 114378, 59400, 18159, 138353, 504, 67769, 40337, 187608, 1166, 7689, 50368, 74658, 121130, + 18675, 55085, 92265, 193168, 5215, 27373, 68051, 32611, 13793, 36800, 37117, 18010, 20536, 292596, 68283, 17340, + 27852, 177093, 366025, 172, 3387, 59699, 119854, 49684, 29523, 615, 8943, 120515, 259718, 38781, 65830, 116121, + 16860, 30836, 32413, 2567, 94625, 94075, 101496, 152484, 43143, 15550, 8257, 43438, 29245, 8300, 137692, 67582, + 15848, 41480, 7018, 78578, 233581, 95833, 94811, 680, 22685, 16034, 79357, 33983, 63631, 172275, 30827, 99510, + 78557, 35720, 123406, 2852, 22836, 77392, 107120, 19910, 133302, 7738, 237843, 59764, 84513, 10802, 76294, 191768, + 1348, 1723, 835, 44328, 51826, 15011, 147582, 11622, 47129, 100405, 10694, 21708, 98836, 9941, 210066, 1364, + 58284, 76266, 30451, 238107, 1830, 218, 64337, 500320, 1749, 129373, 99321, 63068, 94642, 60327, 22930, 25615, + 10933, 11881, 32269, 125198, 6145, 6468, 88776, 9729, 12506, 25930, 46197, 67476, 10975, 30498, 22641, 6110, + 13879, 228714, 189675, 30112, 103781, 142137, 12074, 72148, 144434, 17332, 1205, 55723, 10268, 84346, 739, 7900, + 28409, 142772, 86335, 34405, 32346, 5957, 99727, 159495, 52968, 69914, 49976, 116713, 121567, 45867, 1035, 114241, + 107374, 16112, 22042, 28431, 77268, 110960, 20030, 87679, 23686, 164921, 11944, 46255, 35097, 13908, 12674, 144017, + 10501, 8182, 68576, 13277, 155275, 111289, 84842, 25219, 15303, 77277, 85173, 34541, 47136, 169489, 134845, 66303, + 21102, 68903, 56191, 22964, 168741, 12959, 7590, 12803, 55332, 44576, 35459, 106903, 90385, 5415, 140923, 1080, + 15996, 137579, 8958, 18617, 84817, 54000, 41832, 10520, 681, 67009, 192636, 36305, 137803, 34054, 76848, 21244, + 25054, 15186, 30898, 30597, 142275, 43375, 42593, 28736, 6163, 62732, 73310, 84072, 38175, 49734, 9130, 100431, + 8056, 31178, 23491, 108251, 124296, 54748, 48773, 67591, 240642, 30480, 55118, 280935, 65621, 36603, 32046, 51350, + 4934, 31903, 121291, 176064, 178205, 68901, 29505, 71720, 16101, 99503, 28716, 1623, 62803, 17988, 139992, 3625, + 60964, 35799, 20217, 10717, 18230, 122058, 97858, 147682, 100622, 42915, 18203, 67908, 76465, 68188, 84589, 230422, + 44689, 25437, 16646, 1939, 14545, 62476, 16816, 55294, 9543, 86343, 30341, 131304, 47514, 35443, 171825, 56555, + 16852, 117154, 33897, 115642, 93380, 27861, 5302, 4455, 28048, 30630, 34058, 166130, 12047, 143095, 91785, 86862, + 107106, 33521, 3018, 20571, 37575, 98074, 42118, 36747, 101485, 21122, 7995, 35412, 77047, 174662, 44717, 27754, + 57326, 45416, 100544, 40237, 34819, 147882, 8009, 40696, 96137, 22034, 12537, 89019, 76916, 16381, 260347, 20562, + 6469, 99814, 6504, 8878, 46264, 28993, 135328, 351396, 115983, 162985, 9453, 27223, 75768, 129072, 94641, 114365, + 87668, 163393, 133224, 129378, 69942, 52468, 102771, 87719, 13027, 63666, 4880, 269088, 165, 102664, 209256, 8459, + 8373, 115431, 33913, 89243, 114231, 39718, 48970, 84535, 26434, 89763, 13534, 52518, 52844, 4523, 118415, 84250, + 42799, 85012, 51296, 129398, 182044, 164191, 23430, 172125, 23580, 17068, 100097, 165599, 146254, 103774, 23886, 5517, + 38081, 6556, 39590, 7165, 43236, 9564, 11470, 79503, 33883, 6422, 32630, 507, 146220, 14032, 54871, 27774, + 382894, 111499, 100220, 85905, 14606, 67028, 127208, 96410, 46035, 45103, 89938, 72023, 36481, 10463, 84711, 2184, + 166621, 34621, 71767, 6005, 35417, 18397, 12328, 6245, 95382, 74094, 6382, 42236, 2957, 19896, 58715, 27397, + 59384, 32742, 2546, 93465, 60428, 48668, 29303, 20252, 36358, 149322, 3193, 36701, 66343, 73618, 24279, 42201, + 75378, 214932, 112583, 62412, 22267, 87725, 1442, 11196, 22950, 229298, 116569, 171221, 83528, 29153, 38927, 69087, + 17577, 73532, 64199, 281267, 56474, 107520, 35241, 9230, 25285, 16464, 54961, 820, 4619, 89654, 59498, 85252, + 179691, 2434, 26357, 9414, 75355, 113594, 58673, 61757, 95836, 68318, 54415, 28188, 53295, 26258, 1129, 9855, + 34588, 10951, 29459, 270568, 171410, 122449, 84225, 40183, 4487, 93745, 119118, 40504, 14679, 59040, 261948, 112143, + 84208, 36024, 77263, 53688, 44015, 109308, 151516, 53139, 18562, 27509, 122, 78679, 109133, 27618, 9307, 11687, + 54101, 37528, 73589, 23837, 11531, 15405, 27569, 34097, 86052, 88898, 452968, 106351, 174479, 16086, 12337, 33522, + 303157, 114465, 20251, 93451, 28095, 29793, 32562, 4865, 9953, 17465, 73959, 11715, 35642, 25146, 120955, 73779, + 14564, 70652, 194775, 127647, 39802, 8232, 15171, 32361, 16145, 146754, 54118, 51290, 77606, 61329, 94469, 20733, + 117406, 168386, 13963, 5919, 53038, 27673, 29890, 70790, 121117, 125451, 109176, 9417, 53624, 27539, 61759, 90921, + 22062, 12907, 8858, 21259, 1212, 82256, 83446, 126781, 7632, 49742, 4924, 99538, 127157, 121931, 106183, 86599, + 237292, 72098, 87000, 4747, 189087, 63407, 75261, 28941, 10478, 97792, 6128, 23277, 127345, 38013, 46884, 1492, + 102515, 12723, 3978, 16950, 181997, 58167, 67025, 121398, 86752, 24702, 76144, 13670, 87623, 143137, 34358, 58216, + 10966, 15260, 79095, 10693, 121602, 149602, 4024, 88047, 68783, 112717, 12525, 7881, 68681, 9491, 38696, 70997, + 83042, 12348, 7661, 10266, 114380, 152092, 105889, 5453, 138349, 17021, 6772, 17024, 191305, 9666, 12566, 121832, + 67176, 6357, 20860, 13799, 50085, 58837, 33490, 31519, 39016, 25779, 4404, 75055, 1427, 51784, 37042, 40467, + 42384, 128165, 26665, 15628, 1412, 114798, 10282, 15463, 67118, 108331, 37941, 49355, 122616, 11721, 123403, 55963, + 72389, 45854, 157529, 86342, 25260, 17774, 101303, 13857, 2237, 12444, 17435, 9912, 13576, 18041, 9931, 109677, + 137346, 2419, 5631, 153579, 19938, 7345, 26359, 32429, 20304, 111232, 63629, 62874, 2191, 41851, 33540, 17568, + 68759, 206421, 20421, 8918, 5373, 10979, 52747, 28961, 61364, 35687, 21405, 9253, 238507, 56648, 20973, 119843, + 75814, 58786, 192, 23147, 23931, 351555, 317690, 34008, 69565, 12383, 22156, 37312, 38993, 98373, 54103, 108574, + 131741, 105495, 23252, 10516, 38364, 418691, 1022, 31867, 12528, 57830, 10554, 28925, 87762, 73786, 45238, 3871, + 5679, 49309, 1866, 15962, 129853, 7444, 10743, 34937, 5310, 18691, 186923, 15486, 186831, 78192, 56399, 34869, + 32653, 116247, 7899, 91437, 90338, 131084, 26080, 707, 260176, 152285, 65289, 108488, 389531, 49652, 28839, 250236, + 108118, 976, 52394, 48642, 26843, 1434, 7539, 14145, 43985, 32680, 56851, 16604, 50175, 15484, 133420, 201965, + 30563, 171323, 10408, 5106, 25106, 3832, 22052, 30595, 56965, 126120, 10162, 79454, 18130, 125828, 52128, 6763, + 140428, 50288, 3359, 75168, 4542, 67789, 21815, 25917, 165503, 152273, 62307, 3497, 117991, 32786, 40567, 896, + 24219, 83611, 40133, 65241, 229605, 317863, 103959, 90507, 1819, 29545, 15591, 32868, 129663, 93792, 44783, 13032, + 1240, 5795, 26358, 21931, 3797, 229975, 8527, 45899, 76093, 223890, 32636, 329905, 18398, 40622, 43735, 5700, + 71339, 108607, 70873, 19592, 51919, 16423, 4832, 81475, 93043, 49045, 105473, 57292, 27175, 161447, 88028, 82073, + 47060, 1862, 6336, 34528, 216257, 49542, 1523, 13933, 6483, 12498, 143270, 12904, 35051, 32513, 38643, 137631, + 1217, 10305, 15169, 34487, 16512, 91140, 101318, 54337, 80798, 39094, 72004, 164938, 129064, 45885, 110177, 68422, + 13225, 211848, 134863, 65992, 69339, 9421, 7754, 97572, 8548, 19362, 46011, 26566, 237079, 65134, 52432, 108604, + 72298, 156269, 99270, 75206, 2575, 146135, 49731, 81094, 34280, 39812, 5510, 134730, 51379, 103016, 61853, 94676, + 117910, 64, 7934, 95122, 55671, 3205, 58333, 173791, 53345, 20418, 92151, 39998, 247552, 69993, 40897, 8758, + 29486, 48394, 44428, 19361, 39328, 12796, 110850, 20923, 140821, 98505, 27484, 87170, 34681, 8696, 6881, 178272, + 57045, 10428, 21956, 22625, 60177, 192367, 30337, 97408, 5004, 22803, 112181, 36456, 90269, 45503, 47341, 44581, + 78522, 63489, 59424, 114517, 2479, 35959, 56238, 267536, 322607, 86378, 13479, 1160, 48474, 6182, 17033, 29824, + 61296, 57555, 82202, 35730, 13057, 15270, 72386, 73223, 31558, 45570, 36450, 117667, 4678, 14390, 62433, 28769, + 59271, 15277, 15374, 25119, 6699, 9480, 26668, 33155, 27044, 64427, 178, 36692, 31988, 5800, 18108, 209160, + 35944, 71154, 35918, 167895, 12503, 1771, 16989, 56793, 83480, 39460, 212938, 8159, 4389, 5594, 58690, 40182, + 136508, 1899, 67361, 119842, 3781, 42329, 32531, 37520, 114121, 97367, 52218, 49794, 70279, 62339, 116961, 90247, + 4488, 183020, 1828, 43025, 155829, 86696, 4847, 194990, 42214, 37848, 22958, 11578, 2898, 43626, 50811, 14189, + 68191, 47328, 4117, 163237, 75695, 118135, 64031, 37784, 305850, 99832, 84067, 112381, 45041, 38270, 15120, 240636, + 74344, 6693, 36080, 91318, 106509, 219791, 38255, 7554, 30087, 101966, 6713, 51615, 17429, 102583, 207618, 156, + 93292, 28386, 62288, 34520, 12477, 25385, 50776, 72166, 290, 7825, 17764, 23980, 23080, 74965, 22645, 203791, + 114802, 45119, 156694, 216, 35714, 42020, 117883, 147910, 18751, 59077, 92531, 63106, 26554, 23194, 21689, 105051, + 105424, 2395, 22296, 6139, 17775, 18788, 78110, 84290, 2144, 18630, 17328, 34740, 2412, 84397, 47585, 9344, + 100610, 186751, 213784, 22382, 65192, 94882, 115271, 162177, 113975, 16918, 4108, 47999, 52975, 14387, 51692, 40643, + 180272, 25467, 117999, 146655, 135050, 1246, 11255, 45090, 129815, 13155, 69900, 30778, 76238, 51833, 50960, 175019, + 106483, 105526, 121590, 62888, 21440, 37725, 6971, 74563, 63186, 4385, 6132, 28738, 4260, 7667, 208949, 44659, + 46189, 4885, 2107, 8295, 9711, 33755, 18758, 47913, 28249, 76357, 33947, 20500, 4169, 156995, 46268, 30937, + 23429, 30285, 10916, 52362, 23390, 2897, 68316, 35544, 8324, 51699, 132392, 78433, 141585, 44776, 84808, 4462, + 63809, 59925, 104776, 132583, 67668, 101503, 35715, 119985, 38457, 47365, 56836, 187538, 38063, 17773, 101921, 26244, + 39226, 27892, 7402, 164108, 59972, 87376, 323527, 169672, 1189, 114898, 48985, 100235, 203916, 95, 21963, 6130, + 62368, 812, 54658, 54753, 14403, 18099, 9204, 80661, 16949, 16703, 239430, 18692, 61767, 58048, 87844, 125829, + 85801, 177571, 8915, 209065, 1739, 136288, 19402, 9552, 40147, 51786, 57185, 93178, 35049, 55789, 6563, 35663, + 76757, 32960, 20303, 80517, 33124, 173105, 7464, 51755, 62102, 25470, 21379, 115520, 15780, 117655, 47580, 25076, + 103593, 140889, 141531, 170341, 103009, 155483, 236319, 50667, 53484, 1496, 133657, 34260, 22952, 46940, 6610, 65362, + 67973, 155084, 121252, 1114, 114645, 46684, 81892, 70092, 6566, 122003, 120281, 934, 5245, 88201, 9873, 128289, + 50462, 186108, 122319, 93482, 7601, 179944, 197940, 172496, 8288, 2228, 103865, 63562, 3513, 16971, 44832, 8458, + 194571, 18586, 21840, 94414, 80276, 279833, 284818, 117971, 1908, 62558, 8177, 5618, 54592, 34875, 38914, 80151, + 5124, 5315, 93652, 14782, 58571, 5164, 45076, 67898, 2513, 49234, 56032, 253677, 6800, 85426, 46580, 21278, + 273997, 56465, 57545, 120308, 193904, 19317, 27718, 67291, 1119, 22758, 15825, 65236, 17991, 81823, 6750, 11437, + 117245, 9177, 63553, 197299, 2508, 97808, 35037, 55401, 129156, 37796, 50612, 379962, 82366, 53364, 48505, 19654, + 26278, 19171, 20254, 13206, 71465, 142569, 27518, 4583, 63341, 506954, 106046, 14062, 56943, 1023, 4159, 99329, + 39662, 25053, 62754, 40102, 106116, 281486, 54055, 90158, 94966, 45145, 149583, 64918, 156875, 121779, 78151, 69085, + 9736, 35396, 9131, 26784, 2204, 8896, 115808, 87466, 122308, 77182, 75953, 30431, 94418, 20569, 21625, 175, + 27134, 19042, 38016, 95890, 1280, 29709, 5584, 120804, 24539, 41701, 33794, 262040, 49022, 72657, 12577, 34963, + 45314, 122802, 119410, 33620, 3764, 35300, 297940, 221821, 50904, 31890, 33498, 679, 46424, 24998, 9549, 17126, + 30699, 3402, 46901, 107591, 28087, 48118, 17943, 10684, 293839, 72628, 116809, 24752, 9400, 43553, 51404, 46957, + 33646, 107115, 59781, 66313, 40165, 60892, 19582, 17483, 822, 7964, 22063, 57161, 147499, 47679, 50833, 9574, + 50263, 102698, 56893, 336655, 116179, 23193, 11473, 52072, 29085, 2120, 164190, 67171, 11863, 14812, 284781, 18622, + 31314, 107961, 27863, 20352, 5578, 15425, 16772, 6733, 17797, 45919, 177948, 52326, 5104, 101129, 118088, 134374, + 12454, 238986, 175627, 243562, 1604, 162707, 28340, 5806, 15342, 103576, 54301, 104313, 219206, 110037, 13405, 101976, + 10232, 224569, 40292, 169439, 67800, 219782, 23823, 58633, 94261, 4775, 217506, 19292, 25872, 158455, 309664, 54744, + 13565, 217649, 145860, 19153, 90339, 68476, 52350, 11091, 78971, 60510, 6347, 95417, 75377, 62774, 238643, 45079, + 26649, 121377, 31224, 120575, 41184, 92773, 38918, 55073, 47695, 123557, 42923, 124821, 11514, 104093, 123806, 114651, + 35369, 65591, 36023, 60482, 20767, 40839, 12635, 10552, 14227, 109847, 53556, 40945, 41953, 4497, 82444, 74167, + 309396, 11706, 101696, 39123, 148270, 9459, 44262, 20639, 147938, 46790, 89576, 30166, 33074, 58222, 75050, 18854, + 14453, 79479, 32094, 56822, 27499, 8684, 27993, 15551, 109019, 360070, 45828, 143786, 39018, 134316, 51248, 33214, + 25738, 56409, 48044, 5319, 240196, 56286, 31884, 103159, 158931, 46088, 113738, 5489, 52820, 62007, 122208, 50310, + 8612, 192204, 14081, 25757, 95853, 28293, 144772, 72058, 21524, 28179, 52909, 10275, 137010, 29363, 50509, 211571, + 84901, 15663, 36633, 159109, 70869, 102773, 15903, 250288, 70021, 118, 136728, 103089, 116794, 6302, 4006, 31162, + 48404, 98849, 21638, 10331, 38771, 92331, 17759, 19077, 6732, 77161, 31843, 1947, 1070, 61852, 19400, 133326, + 70990, 71182, 53741, 56003, 187297, 38802, 52928, 47866, 49140, 42448, 132456, 52558, 5238, 95978, 34737, 124934, + 576, 52602, 99772, 2211, 3564, 107992, 7854, 37619, 253975, 33502, 97130, 7194, 16027, 44625, 176640, 70109, + 16483, 69386, 84445, 189507, 2811, 2911, 42607, 29686, 37775, 48856, 7024, 32934, 19034, 21522, 16906, 106835, + 25259, 237775, 26587, 28815, 4053, 8604, 55942, 126681, 2000, 45110, 26993, 6934, 70083, 18404, 97123, 242951, + 95774, 28200, 35245, 126075, 19713, 54541, 26771, 41892, 33431, 6278, 43932, 28670, 92703, 22776, 5711, 39966, + 91314, 25582, 90115, 25964, 42381, 57282, 8595, 60098, 288770, 91568, 25698, 84737, 48194, 43690, 3154, 56885, + 95985, 54802, 176675, 182814, 3991, 18462, 12526, 110444, 77418, 13087, 68801, 15335, 13406, 7781, 13313, 67395, + 241328, 90575, 70480, 44887, 245086, 30235, 7491, 81635, 56533, 280976, 128226, 12240, 35275, 20723, 261812, 18827, + 62725, 32865, 19772, 70441, 9246, 31671, 4690, 16830, 51924, 18082, 12155, 52604, 70181, 56355, 9343, 45521, + 95331, 89906, 123743, 26046, 16163, 794, 32226, 38434, 31410, 124030, 18573, 21412, 79016, 51513, 69499, 483, + 39312, 5830, 39528, 16907, 120878, 146499, 169663, 2790, 119371, 14322, 112030, 52038, 275987, 14651, 179020, 32135, + 80124, 13936, 24740, 53966, 27712, 86684, 12262, 6516, 9186, 18255, 29820, 140583, 220, 99643, 29371, 88024, + 23598, 104635, 329921, 84019, 146167, 73134, 35547, 58064, 85209, 47039, 3925, 35890, 68238, 152444, 111395, 26790, + 282190, 22569, 83839, 77937, 57048, 132950, 95253, 79911, 31273, 2689, 38285, 60446, 30555, 86725, 98060, 37473, + 80913, 23265, 231272, 356070, 17594, 117027, 84460, 79300, 75779, 181354, 106863, 2092, 59160, 174624, 41803, 31994, + 135002, 52912, 60077, 41514, 101219, 16751, 143391, 28891, 189377, 73103, 110543, 14827, 29225, 18801, 72180, 15635, + 96735, 20123, 8653, 54778, 60126, 24504, 72006, 22210, 62522, 119650, 6858, 20841, 104000, 97893, 37534, 99349, + 27620, 55847, 127814, 128781, 86814, 56767, 22957, 8365, 17240, 17411, 175172, 58526, 147533, 178431, 17806, 93628, + 11001, 10386, 11849, 45857, 5425, 535, 4471, 45522, 43682, 11326, 69505, 150707, 410, 109497, 103689, 22109, + 49460, 340469, 41561, 30428, 87270, 69036, 297676, 195760, 69480, 40499, 5201, 173640, 46315, 3997, 90953, 30510, + 59448, 243249, 347379, 83361, 1816, 4707, 31440, 4789, 76201, 1555, 28507, 130215, 9431, 5428, 7565, 108357, + 11788, 21925, 7664, 17680, 87960, 37326, 70316, 157989, 29063, 16049, 114035, 14852, 65539, 7305, 58413, 1877, + 47347, 17700, 121084, 175922, 11678, 14590, 26324, 40935, 33846, 20487, 2960, 82286, 7008, 140230, 95020, 131093, + 196704, 19819, 214983, 15557, 9895, 61765, 2774, 23793, 6753, 15070, 23136, 103206, 8633, 6988, 6539, 24357, + 120892, 28654, 43247, 90291, 59970, 66616, 39279, 57954, 572824, 154282, 103289, 83945, 115934, 30357, 45667, 105824, + 6646, 68979, 29029, 11138, 202559, 233721, 58053, 123432, 892, 61869, 24559, 4513, 48351, 9869, 14927, 39859, + 37611, 43939, 23103, 59284, 251282, 8496, 98455, 54160, 57823, 36455, 13031, 54764, 67263, 54173, 12367, 186498, + 57750, 24251, 33450, 28263, 26527, 3389, 48830, 62567, 34485, 22568, 19435, 87958, 90747, 51443, 111255, 9133, + 7685, 14251, 4357, 34121, 88370, 40692, 44187, 15871, 6144, 133877, 39726, 20248, 64182, 49145, 10818, 50541, + 1709, 19747, 172835, 1815, 41969, 4563, 131019, 10784, 21458, 34631, 34642, 27451, 62327, 10502, 64817, 21040, + 181657, 3356, 25303, 57684, 49247, 69433, 4964, 30900, 225330, 137978, 66824, 79436, 122600, 89064, 2877, 43195, + 114574, 122442, 100287, 41921, 107124, 25840, 145878, 21565, 85361, 235410, 5467, 28770, 111833, 74398, 71263, 46983, + 63243, 138675, 140, 48949, 71420, 26887, 24503, 148456, 15655, 33138, 34734, 7409, 191178, 14, 9258, 150881, + 72430, 90902, 102763, 36586, 18063, 309662, 7191, 149392, 51425, 83681, 24587, 13296, 54002, 210523, 16626, 112474, + 12364, 63416, 36624, 26506, 53225, 21831, 31787, 34, 86557, 998, 49738, 86372, 18193, 23048, 18612, 8630, + 97580, 38441, 26833, 25418, 41232, 78390, 16663, 190608, 138398, 35615, 30762, 41936, 67821, 38100, 49578, 39714, + 128724, 43474, 548, 16126, 8944, 23862, 54057, 149293, 233212, 265993, 19560, 43377, 101353, 40854, 29299, 30997, + 52099, 84221, 76059, 31238, 42127, 24282, 80418, 19725, 14006, 28929, 62417, 365698, 120107, 2583, 165916, 29207, + 32789, 70495, 14426, 38831, 32132, 4652, 155719, 175722, 3498, 105465, 149732, 73487, 18123, 17955, 25073, 7848, + 33758, 212117, 244871, 8698, 56058, 78012, 176282, 143192, 5779, 143466, 322846, 64979, 128760, 44417, 7522, 81933, + 59888, 57516, 87550, 22759, 98869, 19786, 24780, 17277, 18445, 71895, 46515, 17698, 84702, 95788, 12963, 2088, + 51911, 34015, 139375, 18024, 13234, 31480, 33157, 4088, 218379, 20152, 163491, 28846, 20093, 78589, 10362, 134905, + 39031, 1665, 31913, 27636, 8074, 44238, 29175, 3880, 70195, 32932, 86265, 35934, 20708, 49901, 110247, 9629, + 23462, 71547, 25903, 28239, 24355, 4901, 138539, 79668, 131384, 12295, 92413, 90944, 60189, 41168, 22999, 20952, + 26390, 12629, 62314, 162675, 10403, 221931, 3072, 11764, 41060, 197102, 26233, 17299, 7140, 4, 23363, 5513, + 10781, 164646, 18296, 151517, 49410, 18780, 28010, 94985, 42261, 103149, 80050, 34361, 87202, 65486, 9245, 50882, + 82566, 8189, 29402, 49903, 41663, 200708, 115537, 287472, 43105, 88272, 5751, 34285, 60276, 72903, 27444, 11115, + 2768, 40213, 17769, 32834, 5733, 17012, 31302, 65627, 74176, 65762, 220572, 9443, 28329, 49080, 12480, 25022, + 2297, 13079, 51120, 130422, 145430, 108651, 137217, 132944, 131632, 45724, 62952, 163637, 83615, 68196, 126016, 32802, + 122915, 52473, 182224, 14962, 105441, 51662, 20871, 19690, 655, 19897, 140506, 6328, 224102, 127751, 71453, 69604, + 5284, 37207, 87714, 44437, 136426, 34177, 153918, 197294, 67763, 66574, 112376, 9811, 16294, 44836, 174988, 254354, + 188511, 2252, 79267, 9326, 32538, 195, 43583, 8887, 61049, 21719, 26734, 11871, 27893, 192270, 45063, 30090, + 3394, 20502, 9111, 20810, 13951, 78445, 8154, 14291, 159099, 36402, 6237, 37096, 28542, 24459, 73277, 130650, + 17930, 20147, 11211, 3687, 145360, 109503, 113181, 11464, 9492, 132383, 106568, 69961, 190122, 21790, 110379, 8476, + 32285, 32541, 17180, 45498, 78855, 35803, 91050, 89637, 26440, 3026, 120111, 12708, 13570, 45990, 40699, 99704, + 58648, 92464, 151437, 6667, 73908, 1742, 3256, 33631, 4239, 45909, 170962, 320179, 124561, 64828, 19263, 102210, + 2444, 89057, 81206, 34535, 74172, 132424, 73378, 52206, 53131, 11865, 27025, 47218, 11468, 17209, 232859, 291876, + 10794, 27550, 80459, 63364, 73566, 231061, 98585, 163639, 11623, 81336, 209888, 37838, 35343, 51998, 144543, 217838, + 64710, 105694, 183409, 44047, 30481, 27765, 3985, 96950, 4163, 39768, 18053, 21140, 10328, 45473, 19463, 485, + 38309, 119763, 13419, 18090, 29901, 52576, 186587, 4918, 10538, 151025, 65973, 352, 154377, 192678, 90225, 68987, + 76132, 45679, 99334, 22260, 92405, 1735, 27185, 48079, 24839, 129186, 20096, 21327, 11679, 132020, 9799, 26969, + 3465, 457, 58718, 36333, 13449, 152147, 4317, 24297, 11637, 32905, 8133, 40341, 7824, 26227, 23026, 45709, + 2337, 33165, 12703, 29202, 57754, 193021, 33150, 59750, 1260, 52775, 3601, 206, 14458, 29558, 61780, 10854, + 41118, 3988, 39264, 86434, 19878, 45295, 164578, 92891, 38661, 105334, 321632, 27923, 13416, 145240, 66977, 20857, + 159180, 21051, 75795, 119241, 37074, 56804, 62179, 4633, 163164, 62782, 27211, 8479, 54137, 80694, 29848, 30457, + 28627, 13295, 19382, 20644, 52134, 10036, 217, 6449, 184900, 22218, 8094, 95155, 8520, 119873, 16010, 180016, + 40385, 43460, 98015, 169032, 29546, 317, 16757, 61276, 30502, 211577, 27875, 3900, 22386, 11672, 18862, 7483, + 66527, 116135, 41410, 25526, 107458, 48136, 4627, 31485, 6850, 6851, 18832, 87836, 24022, 18326, 45152, 4289, + 47983, 4584, 50159, 5227, 30603, 9757, 27848, 14186, 35083, 178388, 76847, 56645, 8934, 26884, 969, 107665, + 304066, 106975, 94078, 22863, 39500, 79589, 54285, 52346, 9, 105974, 10324, 22271, 28261, 14018, 68550, 223406, + 33026, 33723, 167920, 84931, 77251, 18488, 61468, 140862, 9374, 16225, 134571, 126469, 44833, 2686, 49718, 124647, + 116312, 1215, 83962, 5572, 34990, 71606, 73680, 62194, 29236, 49106, 112907, 45328, 63563, 57200, 134223, 119112, + 125639, 20182, 135593, 81539, 135405, 182962, 6376, 2944, 165398, 71975, 43868, 92182, 159055, 10997, 72330, 18563, + 55690, 2114, 39931, 35390, 88141, 54250, 18546, 28114, 69643, 110033, 69602, 11752, 236964, 2628, 45862, 81263, + 31983, 28246, 80175, 121337, 25572, 22559, 17702, 19278, 20436, 10543, 47657, 113444, 36746, 10480, 87410, 12082, + 60896, 959, 39265, 183075, 31850, 51426, 10828, 84058, 16179, 1273, 128039, 147289, 11828, 25587, 131393, 39952, + 5888, 121379, 76269, 37043, 3043, 76094, 45039, 146616, 66368, 17415, 378876, 51710, 9750, 1764, 52374, 71144, + 31167, 15312, 56281, 89753, 7915, 15678, 93391, 87084, 53111, 56512, 105225, 38442, 36430, 51574, 77376, 65903, + 1333, 89929, 814, 75533, 64344, 10164, 325602, 55313, 93659, 2487, 16394, 59473, 20061, 3479, 15298, 124327, + 60596, 150371, 102582, 138574, 180191, 71830, 13827, 98326, 51630, 12630, 135, 27533, 6792, 27597, 14192, 33715, + 30244, 143897, 92657, 4323, 43509, 2215, 32766, 14515, 101058, 82207, 7222, 111758, 22409, 5186, 141296, 24798, + 420, 100838, 20522, 137775, 44210, 32883, 95696, 537, 109783, 90107, 20074, 2531, 43223, 14612, 84993, 171076, + 27030, 753014, 54033, 23994, 72477, 11870, 108492, 74863, 72831, 78381, 313, 219089, 32679, 44756, 39365, 53656, + 29235, 112804, 4612, 118248, 7675, 9325, 61217, 115300, 47556, 1974, 176307, 5313, 12258, 24359, 49934, 241815, + 39907, 4311, 18866, 14709, 149412, 89233, 46619, 83155, 84926, 117576, 37441, 44149, 118247, 9581, 245890, 19548, + 24692, 61039, 81797, 23206, 71717, 37350, 97838, 6965, 105038, 174607, 122384, 5456, 86009, 3823, 64576, 41024, + 45941, 19151, 16982, 5901, 41189, 12737, 28662, 9093, 89453, 59974, 36378, 119022, 29856, 38530, 16274, 104168, + 52543, 25096, 44843, 20335, 30627, 25217, 83023, 20190, 226798, 32906, 48839, 122264, 67303, 25118, 78432, 77680, + 59230, 99203, 52453, 97225, 67415, 116979, 48969, 59468, 34408, 89209, 24629, 71301, 1367, 86361, 58029, 35735, + 99685, 35427, 110370, 109975, 16867, 184273, 35211, 185023, 128419, 6443, 43697, 58373, 52147, 56592, 116955, 32140, + 4111, 14363, 213044, 135637, 125381, 38275, 50143, 17997, 117881, 101169, 57994, 9105, 16173, 1311, 63238, 37077, + 44093, 61457, 62582, 8321, 102224, 43737, 126639, 70604, 31575, 13061, 20600, 75637, 23234, 165387, 187842, 19370, + 24870, 41729, 18660, 37654, 83790, 66390, 28158, 66508, 127407, 6641, 102989, 100976, 239098, 111562, 135358, 12923, + 3200, 3321, 50588, 83626, 994, 45525, 35195, 6785, 1255, 50764, 123683, 15659, 100903, 138375, 23653, 10925, + 242275, 55742, 47613, 60776, 117266, 29543, 19014, 83593, 55116, 48171, 71871, 34800, 38205, 1776, 35402, 102985, + 16140, 81921, 23789, 138566, 29662, 15032, 205698, 158837, 11307, 35577, 20517, 75918, 40414, 115830, 81388, 35444, + 208793, 18557, 12624, 88971, 123355, 7306, 709, 13579, 56470, 36205, 56962, 82710, 4862, 23826, 108392, 175539, + 75600, 107919, 62674, 112679, 30119, 36318, 35323, 3583, 58218, 42961, 39675, 9447, 70828, 40967, 25394, 52990, + 24075, 6264, 32106, 43005, 26974, 2988, 36004, 49055, 7802, 84004, 24170, 31281, 192353, 47725, 12122, 11485, + 4851, 11565, 12034, 19433, 5475, 41012, 20120, 169743, 78720, 54606, 9059, 80180, 66596, 18422, 7869, 44705, + 3409, 26996, 13290, 28943, 28573, 92913, 21345, 102017, 64396, 222153, 58203, 28422, 30381, 11224, 105757, 65847, + 30690, 156282, 70213, 52668, 59859, 46136, 55861, 28532, 88256, 167753, 157143, 34619, 5406, 46785, 110077, 43667, + 99945, 9622, 81243, 40902, 103064, 15805, 16903, 64832, 34463, 71025, 48166, 42913, 37727, 31054, 199439, 94651, + 24238, 84843, 164414, 59355, 86643, 74925, 68161, 10544, 60088, 90967, 94387, 626, 4057, 59963, 60891, 3100, + 23741, 51614, 57271, 43352, 5967, 76881, 11240, 21366, 162904, 80651, 76535, 13325, 38240, 4001, 24457, 67064, + 28356, 59452, 277744, 118863, 93858, 59807, 37070, 46501, 25510, 47161, 7751, 47987, 122879, 281048, 12303, 65298, + 6897, 37593, 95760, 68584, 3278, 26040, 46338, 79738, 7057, 17560, 16525, 65646, 11971, 76085, 47810, 37627, + 4400, 76728, 5019, 19862, 35461, 120836, 69334, 34264, 211413, 192855, 77060, 103398, 21395, 73312, 37471, 37865, + 59615, 99094, 60234, 101902, 39471, 232524, 96009, 116049, 87233, 34141, 2404, 47909, 55795, 180204, 127761, 116132, + 128426, 111843, 49823, 18679, 3051, 65130, 52071, 17872, 22470, 42507, 67834, 142462, 41950, 81020, 33544, 172729, + 14705, 32808, 43131, 39318, 3974, 48407, 145523, 8491, 180108, 104130, 40283, 86994, 80476, 22927, 7120, 102446, + 78442, 34442, 85410, 33747, 204996, 201697, 144383, 153362, 91987, 218233, 24213, 28647, 15634, 10985, 37691, 14620, + 67610, 11910, 119218, 53184, 139015, 23331, 10937, 4794, 142373, 40819, 40365, 31288, 35611, 63904, 95756, 235215, + 51134, 76276, 11957, 30265, 10387, 46958, 85389, 177045, 4353, 39988, 117622, 35975, 153456, 14265, 42420, 3485, + 57749, 23217, 14856, 159478, 181039, 125928, 75417, 128987, 14183, 7005, 18444, 37640, 68447, 15249, 32753, 189408, + 151532, 34992, 102201, 115188, 21107, 39324, 56039, 38368, 36452, 94812, 113679, 87546, 20551, 11761, 60872, 2888, + 3186, 69267, 53191, 52996, 46247, 44225, 33896, 98339, 46383, 32566, 16275, 88963, 129666, 176, 31427, 38271, + 88736, 11275, 58147, 11767, 140662, 14422, 96597, 45066, 146243, 18516, 134527, 23160, 2066, 107751, 37546, 10292, + 8360, 95441, 58117, 98151, 7978, 116856, 906, 79063, 64818, 14019, 109894, 9029, 106963, 34922, 1651, 42838, + 17896, 207329, 33682, 4066, 47801, 29346, 56026, 102181, 10723, 23963, 12845, 30084, 114821, 24728, 27351, 96712, + 223295, 24805, 8840, 155257, 74192, 14731, 60882, 14446, 3293, 36369, 62442, 16908, 3393, 24144, 93518, 169099, + 16987, 21052, 64385, 3253, 74064, 34395, 37448, 95011, 11277, 73486, 191440, 28196, 91622, 56473, 327982, 27442, + 4270, 7603, 34045, 68072, 29828, 36418, 35213, 46824, 27951, 92979, 6344, 294949, 387869, 35280, 6512, 67496, + 103235, 38638, 120453, 15900, 1374, 3046, 370, 32475, 61988, 25666, 311175, 38789, 120083, 14581, 62496, 17476, + 477, 95410, 36028, 79370, 145892, 21060, 64274, 8593, 128378, 29734, 47799, 41023, 11779, 189177, 291116, 68655, + 211263, 3096, 34673, 28182, 61354, 70851, 185841, 5926, 18221, 58964, 5624, 148475, 17869, 70577, 270, 72725, + 46530, 32794, 6345, 53885, 83061, 319994, 77613, 55607, 108538, 2964, 83165, 113358, 157981, 31861, 142777, 32134, + 90608, 77733, 12681, 67299, 67199, 12519, 151204, 80716, 95080, 143, 88673, 73243, 49064, 70975, 92554, 194270, + 195814, 37976, 26210, 15538, 12302, 590, 123979, 34036, 66307, 32917, 51270, 50898, 10348, 192749, 78998, 142567, + 231346, 74689, 141501, 68754, 160732, 81034, 15915, 13236, 112859, 19495, 33767, 7063, 63633, 6869, 25702, 6584, + 146558, 146882, 202224, 116085, 21271, 11451, 46600, 73942, 31037, 225039, 32805, 68328, 198802, 8912, 109275, 23107, + 47622, 21867, 446, 51547, 12862, 75989, 33036, 7283, 95710, 83109, 38497, 1301, 3910, 40681, 44384, 46771, + 77850, 49203, 95089, 139563, 73961, 6370, 84201, 1459, 85585, 161489, 169649, 79787, 34752, 252385, 3378, 55032, + 61000, 3120, 4672, 121269, 4082, 53803, 18080, 73246, 24595, 42389, 13731, 72203, 103679, 101774, 38710, 34206, + 71107, 64573, 16007, 64268, 8208, 29590, 28108, 9501, 79568, 11666, 168288, 83632, 150019, 35196, 5403, 5176, + 16615, 93441, 6104, 17287, 24961, 112281, 14480, 245496, 139857, 9685, 75837, 34655, 32664, 20702, 201412, 31683, + 197366, 202353, 26591, 31589, 4559, 27910, 10695, 29436, 54735, 144360, 113352, 52336, 32696, 146974, 38124, 34893, + 4126, 3855, 115855, 6057, 162019, 10972, 91491, 48259, 75698, 34626, 7354, 16587, 13916, 6749, 69092, 102728, + 70108, 121378, 4399, 25253, 159638, 105250, 11905, 98411, 19834, 40029, 8256, 251243, 9349, 57694, 5558, 143250, + 24675, 51457, 65260, 24058, 175560, 27907, 52475, 49070, 49643, 4320, 66836, 90101, 18206, 8928, 62901, 87851, + 52459, 14454, 36601, 64627, 27992, 417, 30565, 19686, 10809, 231322, 36540, 40887, 88865, 49589, 161033, 58107, + 401975, 102386, 190339, 87577, 133172, 54023, 98944, 20896, 29000, 84825, 33172, 11781, 34558, 116897, 58352, 53344, + 30915, 21521, 89, 241320, 3658, 45946, 115393, 212186, 25834, 50333, 3228, 37254, 42430, 1886, 49132, 81794, + 36562, 28818, 44722, 31564, 125265, 149984, 17397, 128685, 18182, 3056, 21825, 25140, 10155, 16556, 115060, 11990, + 40149, 184644, 50253, 367370, 97082, 13560, 14932, 13300, 208980, 36379, 448, 106133, 19575, 21267, 68757, 93452, + 60853, 5169, 34427, 16161, 90529, 142432, 73614, 192405, 66545, 49751, 90066, 65857, 9600, 4098, 80224, 55076, + 789, 3798, 125169, 73968, 46420, 59540, 51655, 28547, 2317, 95529, 72038, 360298, 88593, 260668, 151609, 120315, + 55595, 35600, 10778, 130706, 98980, 30010, 17825, 47077, 115302, 28222, 240315, 7342, 5742, 15051, 17234, 17561, + 169155, 93735, 18784, 51808, 1073, 87638, 22486, 96835, 177901, 34933, 43751, 134471, 3472, 60107, 8319, 10366, + 11189, 23719, 97590, 116678, 63711, 15095, 47410, 210265, 78643, 127925, 2850, 146459, 65472, 435141, 43043, 51101, + 50459, 17859, 2597, 1029, 127979, 110999, 140586, 131287, 93, 204861, 49176, 53532, 42202, 49335, 38681, 33970, + 67053, 39163, 64804, 7867, 21720, 57269, 35446, 82892, 157650, 75789, 40884, 15694, 11145, 39407, 108823, 27310, + 141378, 15460, 227, 20019, 42033, 6822, 24630, 75665, 22824, 20941, 64884, 46365, 85705, 3773, 32852, 75547, + 79114, 70646, 91197, 26198, 35584, 16783, 11785, 51303, 15974, 155610, 115243, 9935, 1510, 56327, 108654, 387235, + 54172, 2660, 22731, 69831, 28562, 32500, 45536, 94042, 12451, 40584, 20876, 3006, 104226, 49255, 2735, 68192, + 19190, 61831, 13307, 161983, 97151, 164345, 4367, 129297, 73024, 45668, 60180, 126542, 20948, 27490, 11817, 6231, + 5151, 13473, 7833, 109917, 81741, 27736, 71450, 26743, 21499, 244239, 47508, 47275, 29006, 25257, 7357, 18329, + 84183, 112580, 118605, 3793, 198074, 159004, 32283, 17771, 54003, 17983, 52972, 70724, 45120, 24182, 47404, 99628, + 170125, 8794, 46876, 41664, 26240, 122522, 32698, 169875, 35177, 43594, 40030, 96952, 28389, 22133, 1219, 1627, + 64863, 236968, 142742, 94455, 79974, 94146, 13684, 19300, 60778, 15176, 21046, 112121, 176915, 177946, 121876, 170733, + 232183, 45571, 52925, 2209, 45342, 34927, 19843, 1825, 2038, 21606, 30609, 39291, 80253, 114805, 22110, 67637, + 41564, 11345, 145262, 126398, 40703, 91578, 60920, 14383, 32689, 114171, 33321, 22821, 5430, 118359, 18515, 41276, + 100689, 31373, 44534, 57652, 5366, 38077, 63639, 246112, 23007, 50484, 65271, 114639, 134279, 23472, 42037, 36268, + 14266, 3805, 20110, 106077, 26712, 83068, 3745, 48789, 73993, 11755, 10138, 6567, 24934, 1686, 24404, 64978, + 64242, 102615, 49674, 64915, 52113, 102967, 47100, 123729, 102887, 34493, 5532, 124741, 61801, 68500, 69254, 101322, + 46415, 189970, 103910, 112087, 201049, 82512, 10888, 99736, 54251, 19508, 119320, 20798, 62133, 100781, 96032, 438249, + 122757, 112255, 29169, 17982, 164883, 72196, 54239, 5731, 30815, 71455, 40605, 9088, 139966, 231725, 23187, 27251, + 2319, 15295, 22292, 9157, 30842, 13822, 93710, 35112, 766, 17075, 23218, 37794, 13362, 14364, 86554, 81170, + 10287, 35041, 29695, 20186, 134518, 87266, 22035, 118809, 86111, 54826, 39227, 34957, 81665, 785, 130193, 5422, + 82440, 165380, 20305, 170800, 28333, 3085, 25031, 63282, 43019, 46076, 6077, 19050, 18963, 12418, 78312, 37302, + 8804, 89631, 234705, 12675, 161944, 14463, 408482, 32815, 23439, 151647, 193595, 35885, 102144, 76974, 201816, 48443, + 101145, 61088, 67380, 55214, 80029, 65112, 236169, 11787, 39052, 61274, 38785, 128619, 248708, 117197, 271163, 7859, + 30350, 146035, 2253, 131342, 117340, 101888, 58102, 64465, 11878, 89316, 34052, 26133, 128467, 54644, 138482, 21706, + 974, 21544, 138848, 29501, 138625, 13861, 594984, 69791, 63961, 7884, 49187, 75381, 5237, 3488, 22429, 62564, + 74778, 10035, 87433, 84117, 61834, 89252, 99881, 82523, 67040, 20877, 122047, 11631, 43814, 146638, 2274, 23298, + 13690, 16936, 14388, 11469, 65947, 133203, 65149, 34934, 33809, 57431, 38649, 14721, 232476, 110369, 46966, 79425, + 115258, 73854, 38503, 168461, 181745, 103268, 1590, 73761, 28824, 23871, 104328, 10944, 94013, 11048, 79005, 77074, + 9510, 89479, 89361, 8471, 10246, 73385, 17529, 51173, 93722, 40211, 452, 112593, 81976, 33181, 50233, 31287, + 7217, 11605, 5815, 49818, 114383, 88600, 20714, 201753, 3493, 31374, 99143, 103003, 16014, 10855, 96263, 3215, + 69045, 19108, 17235, 118289, 72692, 54171, 110308, 6337, 12145, 34602, 29627, 82100, 80981, 1942, 94550, 34461, + 9507, 33064, 52111, 30229, 6692, 117458, 68421, 89525, 1620, 80775, 26613, 26281, 60820, 8784, 48514, 22303, + 330444, 101568, 44180, 117514, 35474, 54648, 52855, 24987, 33962, 73393, 65642, 67569, 4797, 67532, 33609, 31625, + 7053, 54521, 80888, 3839, 295463, 120005, 12078, 28541, 46445, 187065, 33145, 165188, 27026, 38584, 77641, 63748, + 12491, 8511, 283918, 4716, 77988, 19152, 55724, 61518, 49524, 87047, 241122, 13621, 35675, 5771, 49017, 17443, + 90947, 23528, 8917, 92592, 29114, 23367, 38345, 16221, 166705, 19640, 54603, 106502, 101385, 34100, 50779, 50417, + 133782, 46043, 51856, 22308, 32704, 72281, 6041, 33229, 6186, 116558, 164648, 67105, 84595, 74801, 41853, 10043, + 176031, 39148, 41713, 36877, 185623, 77660, 93112, 18322, 45966, 122806, 108388, 45184, 151302, 14234, 62763, 55556, + 63069, 244438, 9876, 276168, 1699, 17487, 9500, 35260, 107491, 149204, 1781, 17579, 947, 100336, 84387, 8106, + 15458, 23215, 396223, 29684, 74452, 123641, 92338, 31473, 196212, 62391, 1527, 3879, 6046, 20893, 200851, 3778, + 10498, 7764, 38750, 23256, 12163, 76975, 57473, 59530, 10239, 23275, 35839, 158531, 35191, 44209, 40678, 3596, + 243951, 73764, 16266, 15283, 9277, 164, 9335, 59492, 9090, 7474, 66893, 9054, 29539, 12905, 6948, 246436, + 54460, 50334, 52706, 46765, 22820, 125379, 163547, 191059, 26514, 8972, 72494, 117307, 112549, 40233, 12782, 33432, + 60372, 35994, 32356, 367449, 51753, 39462, 62458, 34885, 48756, 795, 117374, 148644, 21812, 14674, 1704, 10995, + 70861, 98992, 38556, 29671, 260326, 35167, 3569, 19709, 41, 10312, 47172, 133362, 44222, 19304, 3828, 39543, + 10441, 32694, 2925, 24827, 16961, 14477, 106226, 11249, 48148, 641, 182648, 47579, 138771, 11517, 1834, 5495, + 216194, 89574, 56707, 54758, 5914, 36178, 72805, 5272, 52153, 156729, 2240, 13954, 53400, 68748, 63310, 189742, + 212036, 48502, 106813, 55645, 56519, 64696, 48413, 131579, 26245, 26436, 62198, 36864, 10117, 183822, 41455, 133034, + 45888, 134173, 151941, 23987, 15294, 9465, 151116, 213962, 138019, 23156, 20652, 128889, 90913, 19364, 219476, 43092, + 26368, 135383, 5308, 63585, 43842, 38292, 83051, 52878, 42111, 91099, 61824, 86809, 23348, 16103, 1661, 1463, + 6082, 2926, 10571, 10834, 194845, 212415, 28967, 55622, 161089, 61297, 359805, 83283, 156206, 39861, 2052, 124838, + 51546, 15485, 2858, 116911, 11647, 165218, 246102, 28861, 30759, 90050, 115426, 107685, 302912, 221668, 43284, 28078, + 262094, 15913, 108493, 34355, 8635, 56521, 33938, 107963, 78876, 130940, 120485, 7453, 26535, 294502, 79190, 217099, + 35283, 16638, 827, 171026, 54183, 254986, 24436, 15975, 31183, 71531, 8757, 197433, 85484, 10671, 7791, 98566, + 147873, 81926, 428971, 27061, 12989, 34063, 127862, 4570, 5197, 115338, 86529, 11135, 6356, 99904, 45008, 275758, + 72894, 23300, 64365, 137852, 65347, 47170, 28144, 96791, 20150, 159397, 90399, 1362, 27370, 98131, 31669, 28527, + 73787, 117782, 21103, 19726, 1493, 47288, 66521, 29621, 45918, 29807, 100648, 6707, 12366, 36485, 45413, 16410, + 190217, 44950, 2222, 75028, 20724, 41617, 33388, 41062, 13858, 83447, 87017, 3916, 10981, 5912, 19979, 5885, + 67449, 21903, 300914, 5663, 81213, 18877, 80072, 19338, 7553, 143149, 63928, 73907, 14115, 297533, 1424, 109439, + 72242, 21063, 30733, 14100, 271517, 34775, 24860, 63700, 11842, 20737, 50067, 22773, 48310, 11235, 43957, 13183, + 88743, 32209, 23321, 97092, 143726, 17704, 33289, 144394, 22177, 68374, 111967, 982, 3290, 170312, 8945, 39607, + 243231, 56247, 9319, 54873, 58452, 181674, 44954, 10669, 62937, 133402, 31264, 14854, 12592, 70761, 4902, 2472, + 1654, 39097, 17353, 115270, 40066, 92333, 7620, 158039, 33477, 80720, 2135, 38418, 13751, 164773, 39322, 114253, + 9921, 17904, 79739, 5389, 128442, 4659, 46992, 27136, 15868, 109620, 82964, 2344, 7106, 102119, 3920, 13567, + 75236, 23651, 9863, 2844, 83773, 92000, 12422, 95077, 10775, 142605, 120027, 9048, 36938, 81154, 56545, 4754, + 10482, 28907, 16084, 21873, 170465, 19577, 24905, 58130, 17368, 70780, 14531, 55034, 17469, 23293, 260571, 52064, + 161508, 63795, 195965, 205503, 32752, 145584, 138232, 298398, 98340, 15352, 275003, 75638, 800, 306996, 12394, 36516, + 19824, 101316, 192935, 84781, 264456, 14840, 73856, 120271, 3901, 50990, 48385, 372041, 87319, 2227, 95681, 56225, + 2867, 24632, 65645, 172829, 26782, 83816, 46844, 37867, 9630, 11329, 76756, 138080, 113102, 2296, 84207, 55323, + 185815, 5210, 2986, 109112, 24197, 70199, 64611, 7066, 44584, 96679, 3292, 11955, 86366, 7796, 105090, 121463, + 40224, 45210, 50967, 43523, 3636, 166133, 21094, 57178, 140916, 4748, 13764, 3777, 31731, 104179, 416810, 188439, + 267731, 86931, 13966, 5018, 9567, 283051, 3232, 53171, 53678, 6228, 32833, 99759, 72984, 37061, 70162, 70738, + 29389, 1868, 122000, 13671, 27963, 37380, 69277, 37341, 17106, 45005, 12564, 183, 50282, 3809, 57468, 23868, + 284911, 75942, 28036, 2447, 60170, 8119, 51479, 25171, 8322, 48880, 42721, 33419, 12608, 11361, 18423, 24958, + 23374, 141776, 2304, 149128, 89652, 68789, 36035, 45613, 5268, 442, 159096, 63783, 39044, 4432, 34374, 26718, + 229766, 59526, 23706, 15072, 8869, 12775, 73562, 76513, 151350, 46040, 29341, 58942, 31436, 8742, 195663, 155875, + 177342, 9409, 11232, 106379, 12269, 40493, 135684, 74614, 183212, 41598, 44475, 43186, 120418, 263054, 22473, 36531, + 116270, 43205, 64216, 41880, 2843, 3299, 84033, 48575, 78888, 37197, 53158, 44555, 69192, 29213, 80495, 156103, + 7865, 86885, 75088, 21642, 184099, 132386, 508, 948, 1086, 69319, 44488, 3613, 129897, 33311, 51024, 2449, + 18383, 28970, 50210, 70726, 70508, 87097, 118602, 146382, 20242, 182430, 197233, 39937, 18508, 14350, 24901, 165385, + 229924, 33832, 136628, 151243, 124569, 88492, 127154, 62624, 35749, 21909, 91751, 8060, 50589, 954, 105732, 25763, + 55626, 146107, 1247, 64529, 9884, 13632, 16172, 1335, 83115, 126164, 36224, 170872, 40971, 2035, 7394, 46551, + 30671, 53142, 107311, 75486, 18135, 69492, 12903, 96202, 14452, 18152, 49690, 77016, 38861, 2226, 385130, 21131, + 17844, 57784, 84, 36610, 201826, 14842, 131564, 77270, 5549, 102741, 193961, 183742, 26413, 2474, 194304, 464074, + 17189, 226650, 166968, 718, 13561, 32539, 7182, 35594, 38539, 16307, 188634, 18219, 10679, 72826, 161909, 63158, + 143331, 23378, 14208, 25491, 3314, 73171, 63331, 5500, 36785, 38973, 614, 1580, 171194, 97209, 137201, 91854, + 49685, 50187, 10733, 91809, 187713, 62737, 172516, 6666, 67506, 4776, 58141, 30188, 4618, 392, 4013, 235640, + 104039, 39136, 56441, 250251, 17060, 48306, 57483, 66360, 195080, 59703, 90208, 74747, 50648, 37806, 27833, 30715, + 33159, 66182, 9356, 34911, 19238, 70986, 114677, 17409, 67559, 80594, 47694, 8643, 134840, 79148, 5452, 14070, + 28599, 144163, 50434, 27535, 157523, 13810, 155, 61478, 17130, 128237, 70556, 20046, 38064, 108184, 48322, 65305, + 117398, 36136, 43677, 27966, 94355, 126236, 109335, 98906, 31918, 54262, 1145, 25681, 13575, 60774, 45912, 122417, + 34538, 24034, 131365, 51834, 40326, 21250, 21866, 17508, 13997, 117733, 16819, 89310, 3494, 11178, 60427, 34853, + 348283, 6846, 88202, 17221, 62481, 8359, 10088, 62152, 26862, 12931, 13414, 151495, 3603, 33643, 178131, 46655, + 104426, 109103, 4105, 146806, 244363, 64272, 15471, 93101, 153709, 21364, 73754, 69185, 112487, 8238, 52934, 80498, + 304612, 124178, 39721, 198761, 199674, 132729, 36479, 5833, 41239, 3609, 28387, 262648, 35545, 67430, 89355, 18851, + 54869, 54495, 5118, 27316, 293005, 36308, 43061, 110038, 28223, 7186, 33116, 18945, 26277, 73156, 23311, 41090, + 26899, 300052, 100683, 8894, 4533, 2915, 66118, 31965, 18518, 75755, 209592, 62868, 15492, 99745, 54220, 111614, + 38587, 26556, 107426, 95125, 80488, 64232, 13265, 23009, 70485, 25431, 69000, 3572, 160395, 98028, 42484, 42093, + 263, 6747, 244525, 132822, 60162, 171913, 124805, 29698, 11382, 8416, 48535, 37248, 222152, 16593, 47020, 8816, + 4696, 24033, 36100, 38399, 250751, 10483, 19137, 143486, 51921, 74917, 130735, 79897, 182609, 69122, 71667, 39358, + 10707, 133977, 56093, 112728, 48463, 33430, 9299, 48097, 46243, 217556, 63573, 36225, 1227, 221850, 32274, 14107, + 49111, 29646, 36094, 63601, 111564, 49362, 24638, 17610, 46502, 41875, 40909, 4262, 33342, 32777, 32103, 28673, + 56846, 6159, 154445, 47336, 68541, 153151, 3385, 58607, 63559, 108092, 46159, 13477, 858, 2316, 24161, 128413, + 139927, 11071, 88393, 110949, 16654, 34235, 73235, 37210, 229375, 76831, 194659, 71471, 76759, 4725, 28276, 29321, + 26478, 117042, 46296, 101434, 33205, 67157, 6462, 3168, 95828, 54864, 23765, 55657, 23399, 39175, 47265, 10732, + 92945, 59744, 14719, 35115, 2637, 23170, 162802, 33876, 35630, 38052, 9197, 83972, 28470, 19030, 22779, 259343, + 143992, 229198, 23683, 129194, 50214, 30041, 34367, 134697, 14174, 184114, 144727, 23833, 21456, 25715, 53623, 10461, + 166191, 57432, 28691, 157504, 65665, 67457, 16937, 12529, 1711, 168178, 4710, 9397, 21594, 15069, 102984, 295316, + 78019, 30013, 49809, 108210, 97599, 11539, 20109, 22376, 111701, 47783, 890, 213373, 36, 229081, 150468, 50773, + 147151, 857, 99218, 78079, 110246, 83001, 17917, 78802, 189022, 112785, 40738, 2303, 43021, 10227, 101710, 85107, + 30397, 3429, 72517, 49710, 40757, 160978, 8573, 100346, 131935, 103276, 38380, 105759, 42065, 28883, 402, 95838, + 73335, 67055, 20011, 29906, 48039, 54843, 18117, 35768, 26596, 8518, 143614, 74994, 28984, 30865, 51327, 59362, + 15102, 44276, 89118, 714, 2361, 214493, 160772, 63295, 7421, 71563, 196497, 21076, 202167, 92741, 103226, 106818, + 69744, 89977, 100205, 98932, 43766, 76931, 6843, 16584, 52826, 291470, 15946, 28322, 3642, 204, 258863, 106515, + 83304, 32946, 12706, 41427, 33873, 27151, 56397, 63503, 75140, 34306, 73149, 29926, 63169, 91359, 77492, 74847, + 192389, 42353, 148, 138944, 36551, 31192, 181456, 63005, 92748, 24635, 51573, 52932, 13039, 4741, 79294, 69836, + 123959, 107948, 11123, 170654, 233220, 54352, 20575, 40997, 21738, 120351, 106134, 155424, 84447, 124076, 35541, 706, + 77230, 51748, 8442, 40109, 20228, 6042, 20963, 107207, 187852, 127504, 15461, 33093, 19095, 5987, 21488, 118624, + 25799, 182308, 23326, 20103, 92136, 54838, 10713, 126470, 108774, 19203, 61066, 63342, 29237, 9113, 45526, 43035, + 53947, 312166, 11117, 25827, 2299, 27218, 55737, 42929, 118106, 61182, 16888, 118952, 2687, 159550, 3496, 59443, + 8830, 51688, 78340, 103766, 42331, 73399, 58020, 1666, 202924, 2854, 12678, 5891, 33667, 17, 36329, 9733, + 2023, 40389, 124099, 86266, 73763, 17618, 9348, 9584, 30704, 89527, 11379, 88708, 19363, 71305, 80907, 163488, + 19779, 4406, 18155, 125871, 16737, 4313, 24007, 71010, 35629, 59187, 181773, 171755, 48081, 282188, 44159, 41703, + 24068, 63512, 20026, 20079, 101013, 4611, 19597, 169832, 162338, 158889, 21816, 59081, 291912, 51657, 16911, 142760, + 13749, 22555, 50557, 197345, 24745, 28474, 5713, 171571, 328289, 6161, 7725, 36994, 167679, 62776, 62795, 52274, + 70086, 34392, 130952, 4930, 48299, 107549, 36520, 19777, 23306, 51480, 107581, 7841, 16732, 37422, 72074, 99464, + 17801, 125916, 10597, 40408, 43322, 87432, 24862, 37097, 54589, 32282, 167348, 17898, 3586, 19826, 146462, 1988, + 63653, 28763, 132195, 94493, 43624, 52107, 37692, 42666, 53474, 25957, 8805, 222451, 925, 3055, 104713, 68687, + 109177, 128137, 51766, 12112, 251316, 83233, 11015, 24963, 43805, 189880, 86830, 77075, 13082, 33163, 73548, 44568, + 19511, 108517, 64550, 26047, 86565, 54739, 5095, 68246, 142182, 40984, 140026, 1750, 92461, 2773, 725, 19988, + 17117, 106417, 21066, 147935, 101033, 174174, 12814, 99891, 103319, 85050, 87912, 55182, 64589, 75793, 37143, 22056, + 4022, 81127, 18968, 52899, 4351, 50009, 17573, 31885, 235897, 86925, 127636, 149633, 5352, 170945, 13870, 65479, + 82705, 21354, 52980, 4573, 107142, 167201, 689, 25288, 46391, 60641, 14497, 192600, 156084, 61637, 16433, 20054, + 5860, 781, 55170, 27826, 61365, 81069, 119317, 27123, 10558, 95773, 129802, 55875, 13045, 36330, 234176, 18058, + 7717, 148000, 254, 11979, 18357, 24975, 8185, 10718, 33922, 75655, 63067, 62116, 12590, 26192, 24136, 12861, + 33065, 175633, 5001, 83234, 6928, 37537, 39570, 797, 46993, 106751, 3390, 24316, 783, 56130, 98315, 10024, + 46937, 84478, 81419, 119922, 67846, 100404, 45582, 91490, 8952, 68431, 40501, 170957, 26295, 27641, 117433, 78412, + 6107, 100280, 74320, 156460, 119656, 6001, 172558, 13585, 18799, 5324, 39108, 12471, 17458, 37351, 39676, 88115, + 50747, 61250, 125687, 5267, 4229, 23899, 71256, 32369, 179559, 203977, 81060, 24631, 112727, 201216, 111416, 67474, + 118080, 91, 35301, 125372, 20683, 74602, 93673, 189634, 41464, 103487, 832, 23478, 125468, 73805, 119649, 29439, + 51560, 85326, 160218, 36794, 49749, 182459, 10783, 266813, 44231, 9294, 15428, 71516, 7359, 56127, 165213, 174634, + 35339, 38024, 193163, 64310, 62988, 135393, 50470, 543, 136487, 82389, 59047, 12586, 67015, 11644, 14120, 11086, + 5208, 84496, 75693, 97070, 29150, 13909, 133818, 28826, 24956, 25564, 136586, 132437, 105186, 4035, 36426, 22688, + 48858, 283040, 60007, 145362, 6143, 70935, 12860, 88632, 18097, 251246, 43147, 88692, 6972, 46173, 87229, 69837, + 16404, 61816, 34813, 41161, 73489, 67221, 80668, 80317, 58742, 40484, 31106, 50580, 97196, 213949, 58374, 30823, + 36357, 82430, 16088, 18269, 164616, 424, 215366, 24797, 5834, 261900, 3248, 32765, 32267, 19570, 240015, 43546, + 13746, 95242, 296368, 118754, 147733, 19239, 38636, 84832, 15113, 3902, 193061, 119038, 132091, 99171, 11021, 33759, + 34127, 5030, 3999, 6802, 106298, 88781, 43305, 22438, 39729, 5081, 25109, 217719, 106426, 21384, 30890, 42599, + 22294, 25962, 25245, 30497, 9780, 59534, 29560, 5927, 15602, 52507, 34738, 56980, 36213, 7273, 98748, 10870, + 71502, 69490, 359492, 120808, 42808, 83335, 17061, 250432, 66802, 102331, 12384, 25660, 599, 64618, 47661, 61634, + 60755, 54355, 107300, 30007, 5851, 79666, 18845, 114230, 39120, 65244, 33679, 16370, 67363, 870, 69190, 700, + 108623, 7199, 25596, 17155, 126368, 82661, 16291, 21557, 72770, 55038, 123010, 8616, 91263, 105277, 4143, 14045, + 32486, 45280, 15555, 228392, 30596, 287, 109234, 90030, 151717, 60950, 12314, 165069, 7951, 13304, 63878, 123573, + 52002, 27428, 9709, 27443, 43103, 89429, 81209, 45635, 11768, 65127, 12093, 62760, 68942, 4189, 59598, 70512, + 40901, 34082, 60981, 18280, 39344, 77285, 4183, 14054, 24037, 4113, 474190, 227569, 127500, 33437, 1737, 48639, + 116890, 82909, 62991, 47953, 48403, 63792, 44541, 194078, 16926, 13540, 54497, 288031, 86750, 30818, 25377, 51309, + 17745, 9988, 98587, 47808, 48648, 15607, 51298, 11671, 159545, 43990, 97815, 27393, 34460, 59095, 50213, 40288, + 58419, 77017, 108711, 106974, 5634, 27302, 23195, 55246, 114317, 20787, 2176, 34170, 67865, 144530, 7099, 31816, + 31462, 136214, 45619, 105094, 23352, 133565, 14615, 72015, 24010, 107270, 1347, 151444, 98185, 20971, 63491, 86427, + 125708, 141576, 126803, 83272, 69686, 130286, 27346, 20481, 68337, 143641, 11161, 32803, 13610, 94361, 17084, 170992, + 26271, 23139, 77797, 5076, 70691, 1893, 34857, 35003, 2980, 189255, 41107, 6458, 4768, 19194, 182508, 92031, + 27225, 316351, 48574, 15987, 102402, 14334, 46503, 138005, 75453, 29119, 31677, 97327, 28106, 72279, 7983, 6958, + 8104, 6430, 66373, 51695, 6931, 86975, 52340, 108203, 1176, 75897, 106677, 21721, 6274, 7082, 13018, 139466, + 6475, 73402, 49676, 46350, 112635, 1121, 31018, 39288, 22498, 157722, 11740, 4237, 6176, 145143, 8139, 77918, + 238686, 12912, 106531, 13530, 26832, 8235, 79497, 23768, 28893, 17853, 435790, 33701, 90319, 24688, 121374, 13130, + 14441, 44878, 2951, 60805, 15682, 368968, 59313, 5764, 15087, 226921, 19420, 46906, 39517, 28151, 141168, 18755, + 45270, 39523, 7813, 6413, 109134, 51672, 66939, 115004, 104440, 22565, 91467, 22538, 45965, 62165, 14231, 46295, + 47645, 71747, 30576, 43847, 81772, 99248, 106345, 17063, 7876, 115604, 42345, 60383, 52683, 130873, 7327, 83603, + 87720, 43843, 117, 49018, 12898, 24681, 9075, 145197, 4505, 69956, 86690, 8844, 185665, 73412, 8843, 2050, + 2769, 61489, 84788, 765, 113401, 48729, 111522, 169599, 15664, 24750, 131177, 29882, 57592, 149057, 45527, 23022, + 105229, 1754, 10064, 14643, 137381, 30538, 7740, 43774, 97059, 30153, 99882, 62539, 119268, 49329, 86015, 5395, + 6876, 63855, 14385, 103785, 43309, 58548, 24380, 31077, 33886, 37135, 15365, 50313, 128363, 48659, 56802, 25981, + 35476, 24927, 75162, 26509, 144249, 18559, 42480, 122475, 67013, 7834, 35825, 193197, 143587, 15263, 5498, 114311, + 83367, 56008, 102437, 38184, 25703, 112278, 26497, 8942, 91436, 71042, 111589, 153704, 59347, 5258, 57333, 17950, + 53236, 28574, 30025, 337749, 2289, 1610, 194412, 103935, 16519, 41580, 29330, 37510, 19844, 35382, 26043, 158342, + 46309, 11301, 106938, 96214, 58558, 67686, 163604, 7639, 99834, 7952, 12195, 58864, 23313, 15895, 25042, 10540, + 218816, 17583, 10486, 66978, 231303, 9956, 202649, 2197, 36388, 7432, 55426, 2224, 51333, 16871, 119452, 179548, + 183535, 153812, 138057, 9695, 109792, 28563, 126806, 52451, 139277, 13092, 48380, 9516, 54306, 67549, 14696, 110080, + 90139, 139123, 3236, 10377, 18235, 70961, 50653, 11129, 8275, 44976, 10027, 18291, 32710, 28012, 318969, 288958, + 37677, 67929, 68821, 98990, 82464, 10181, 44498, 70197, 86025, 8013, 28549, 83043, 92204, 6171, 76895, 4556, + 88842, 98320, 60377, 71627, 117723, 32809, 5961, 12916, 37570, 27325, 108946, 2654, 128723, 67148, 211765, 80004, + 234242, 154947, 40300, 196564, 76350, 3941, 18870, 24018, 73795, 84945, 197515, 8338, 34896, 58219, 146191, 58500, + 148247, 85921, 101683, 117085, 58424, 66764, 17133, 63784, 11105, 84682, 86491, 32061, 11744, 40778, 32197, 195238, + 45746, 2576, 39893, 67918, 63372, 3938, 36907, 4069, 17118, 24568, 96382, 59551, 49772, 244074, 72416, 53854, + 199520, 69813, 60862, 51724, 81902, 122032, 234459, 29839, 38004, 36647, 114474, 13978, 22911, 158690, 15088, 63654, + 33752, 29885, 83141, 89560, 3125, 12877, 21039, 57564, 1995, 38775, 1392, 70857, 53792, 84731, 5519, 86340, + 4689, 95886, 119040, 180041, 26909, 84982, 2582, 213702, 108150, 123127, 35785, 15401, 146062, 49004, 67250, 232281, + 69674, 142589, 97906, 160196, 41811, 23716, 17807, 35367, 161444, 15020, 30993, 67673, 84855, 49859, 39473, 23557, + 8999, 127662, 19183, 120775, 28561, 79489, 57003, 62500, 16731, 29948, 30509, 54712, 93937, 166789, 11042, 61414, + 3189, 128681, 363904, 9363, 21967, 135864, 94929, 23174, 24890, 236852, 51310, 35602, 22943, 58275, 6351, 33135, + 1356, 27798, 41855, 110968, 145300, 139274, 56821, 8658, 51569, 19894, 24832, 4253, 28802, 5732, 52333, 338701, + 517, 144012, 123400, 70750, 118679, 112674, 109716, 66301, 31703, 84657, 45777, 1745, 40607, 17239, 226055, 50256, + 48098, 24528, 28411, 109729, 108854, 16675, 111456, 14807, 25003, 27471, 42491, 22378, 10233, 14158, 70447, 13850, + 73969, 15024, 24742, 25518, 177495, 27226, 176504, 38550, 5248, 41612, 65904, 91342, 24516, 41883, 18419, 84650, + 215347, 15434, 75579, 9614, 146192, 82954, 25501, 30483, 48712, 34315, 70905, 29488, 60626, 66089, 51329, 5601, + 69188, 18936, 21518, 23440, 40735, 224481, 33618, 40631, 5866, 927, 128437, 30586, 586, 52791, 76586, 141284, + 101541, 81564, 12333, 65243, 6509, 6267, 176039, 133405, 47590, 16079, 254143, 27357, 52129, 34758, 9267, 15970, + 5969, 57732, 7254, 86956, 222045, 17428, 16267, 26799, 110933, 58017, 142888, 143524, 25733, 55763, 16175, 9560, + 24223, 247240, 55864, 48197, 65339, 12856, 21320, 46799, 62812, 40007, 188763, 14523, 2414, 31539, 49494, 58075, + 155418, 90186, 99708, 24554, 35819, 75001, 757, 39520, 16022, 59445, 3713, 46416, 78423, 112394, 18048, 40416, + 43138, 69398, 67029, 137948, 20995, 20115, 42968, 1859, 128255, 8554, 13664, 13508, 240673, 36331, 63579, 21029, + 46745, 152929, 19469, 227297, 236093, 41575, 99905, 13097, 72176, 7570, 11923, 37300, 57085, 104327, 59863, 51790, + 97841, 7674, 89187, 121357, 61248, 15021, 6833, 112841, 107, 73638, 39990, 166290, 36068, 78500, 50124, 104807, + 193177, 49274, 90762, 21097, 105427, 14711, 17114, 26796, 55726, 20684, 123636, 20366, 215229, 140553, 26789, 7139, + 20446, 136219, 5009, 2402, 47228, 5882, 154075, 61745, 100420, 43241, 49334, 149951, 87091, 86136, 329294, 49115, + 14429, 27425, 36818, 111800, 121708, 18542, 6570, 27157, 23605, 12467, 6000, 70237, 21157, 88964, 42071, 18521, + 187721, 24444, 86, 29324, 21880, 145192, 303927, 17373, 2997, 108310, 171873, 60425, 203976, 18220, 116526, 69373, + 99166, 6387, 84942, 187663, 95068, 21687, 241311, 45047, 25877, 42751, 35432, 9804, 7724, 82982, 23192, 146653, + 98925, 132997, 92522, 87415, 83401, 53505, 150646, 8142, 4829, 58649, 25534, 26193, 13182, 71621, 73803, 70797, + 18229, 39127, 69038, 57676, 13718, 58332, 31672, 65795, 239662, 77166, 190337, 3939, 38653, 96399, 34620, 237425, + 116505, 13199, 102812, 5543, 153497, 1676, 111555, 5249, 30589, 10038, 44022, 32064, 89029, 4156, 84630, 183016, + 38962, 20874, 41135, 5896, 181302, 141985, 28318, 41843, 43853, 109018, 101914, 14232, 78872, 97030, 13738, 107743, + 180301, 8364, 79955, 206838, 4786, 1843, 19029, 45663, 248240, 2387, 51445, 145895, 7401, 82908, 16435, 28522, + 106136, 4571, 2405, 140262, 112590, 26994, 36399, 3894, 77745, 48157, 81325, 9424, 19731, 7606, 25832, 161377, + 60880, 3462, 68402, 63020, 77789, 8989, 94980, 140615, 125748, 101040, 138, 61603, 135487, 19491, 9072, 42404, + 5975, 4730, 23228, 147617, 48627, 88470, 18481, 32183, 34084, 197974, 15306, 16406, 12419, 25554, 80789, 50074, + 215770, 77760, 67732, 15820, 47557, 8552, 32891, 3397, 254582, 51747, 37440, 13256, 10364, 19078, 197381, 38702, + 106495, 126239, 69247, 4048, 21856, 4277, 25578, 128895, 67539, 63586, 54687, 80647, 88981, 92208, 26195, 51852, + 38805, 50151, 28772, 79952, 21428, 21251, 116522, 15445, 48732, 44111, 50224, 95470, 42316, 106832, 48425, 377321, + 12149, 3533, 41847, 71691, 16078, 249001, 118133, 10711, 52808, 33393, 18850, 60881, 25327, 125536, 35507, 17128, + 51322, 25298, 64567, 14087, 33850, 17830, 26983, 37516, 51147, 63624, 6036, 75728, 12253, 42565, 30641, 60123, + 122354, 7767, 33831, 15668, 46077, 207921, 704, 228032, 56483, 154155, 19104, 43429, 254553, 40400, 19915, 39707, + 115417, 1959, 8797, 59126, 81834, 6291, 43802, 41057, 150991, 132620, 67404, 31385, 94662, 35042, 22728, 29984, + 86668, 80841, 166714, 23521, 7381, 18863, 127695, 23731, 12841, 34184, 955, 46179, 100650, 105059, 92227, 35881, + 18218, 34994, 30732, 65296, 15741, 79032, 12811, 2842, 22372, 120408, 11638, 298925, 68294, 83360, 60616, 1270, + 50705, 35353, 39160, 65700, 15535, 87701, 7971, 17998, 84660, 24834, 3600, 57330, 61887, 43556, 70547, 21033, + 22553, 123308, 92138, 46071, 72299, 43807, 86552, 3952, 31361, 45177, 11621, 157425, 24824, 87145, 1530, 1015, + 17743, 64397, 14528, 84960, 46820, 135812, 40268, 205321, 64288, 83124, 142613, 20892, 31582, 178130, 41319, 47604, + 77006, 38648, 45265, 69293, 111674, 38866, 30288, 90253, 116384, 11710, 162727, 119339, 30760, 74575, 99191, 114910, + 80920, 74030, 166787, 23839, 86149, 70396, 8817, 71462, 77192, 61144, 7550, 263557, 51979, 2741, 12376, 38498, + 79691, 27990, 88220, 46311, 60342, 115770, 32907, 654, 122805, 22347, 45779, 35595, 103800, 61077, 11173, 7981, + 240873, 127729, 60554, 100208, 160744, 278120, 46400, 47854, 233114, 14783, 28068, 50186, 78962, 21368, 149837, 32533, + 54920, 67698, 16575, 42220, 8608, 244187, 24441, 16118, 3484, 29636, 35155, 100272, 316104, 399, 13004, 70176, + 72548, 2188, 3176, 10044, 24337, 146534, 171223, 44154, 5088, 100828, 173780, 92915, 230040, 59854, 91355, 69382, + 21926, 88289, 10494, 133339, 10172, 99597, 53605, 17770, 36838, 15150, 30766, 12102, 26, 22751, 93985, 48775, + 86221, 110954, 26896, 56128, 83458, 19243, 10858, 11338, 102176, 1734, 27656, 45449, 12062, 47678, 227191, 104843, + 17571, 33218, 31175, 80, 41929, 75064, 823, 5915, 41170, 26266, 21858, 74328, 28428, 46729, 53037, 208149, + 68239, 44371, 128012, 14846, 41750, 121730, 939, 16024, 103930, 2667, 3749, 72822, 2634, 17905, 21653, 37065, + 18313, 12459, 26288, 15851, 53019, 237454, 82804, 43717, 34825, 6324, 223813, 34763, 97837, 74764, 131779, 54108, + 63115, 77477, 133465, 158834, 24606, 172748, 8241, 11219, 73157, 67543, 21979, 44698, 152474, 62783, 25538, 151168, + 14715, 17653, 20409, 10177, 91439, 51243, 33807, 21982, 37033, 28498, 20946, 82195, 109806, 89357, 35843, 62764, + 140259, 29524, 15905, 148965, 30668, 242609, 18782, 55072, 174760, 95402, 12389, 60205, 380, 39535, 99410, 68744, + 135597, 29770, 37761, 9074, 95673, 61075, 167448, 81798, 136073, 92495, 63964, 71292, 65073, 16100, 82788, 3903, + 134249, 666, 114606, 13925, 13829, 14923, 22844, 73642, 17279, 39192, 82814, 14279, 122305, 98412, 2819, 90185, + 4420, 211793, 88571, 343220, 46444, 31428, 94176, 75136, 10237, 43041, 121311, 109668, 64848, 79724, 95455, 406446, + 203623, 49760, 35347, 15313, 70728, 55604, 64355, 9274, 10349, 116949, 76977, 10948, 182885, 140337, 63627, 148647, + 65075, 60013, 4856, 3391, 24519, 53746, 165940, 8600, 25783, 64942, 35809, 14075, 40318, 2510, 34997, 36980, + 34139, 23025, 39457, 7315, 22222, 75794, 8923, 194881, 63394, 25194, 37165, 85475, 55266, 208468, 18378, 53662, + 102764, 38595, 7896, 34791, 41422, 49686, 92984, 25098, 20126, 17645, 88907, 226875, 65100, 60009, 15638, 21283, + 90408, 4537, 139878, 112661, 53640, 5071, 42553, 9995, 35128, 46262, 76889, 67947, 48932, 16991, 106940, 167117, + 11192, 66889, 6670, 104891, 38935, 1875, 45170, 3303, 96839, 772, 3134, 41094, 34782, 66145, 43963, 48995, + 39492, 21237, 117116, 33731, 19396, 265866, 122508, 109994, 41332, 31277, 72923, 726, 6250, 12016, 13536, 75815, + 5511, 102922, 12522, 133050, 19492, 24257, 18746, 2693, 51304, 63505, 129615, 231652, 25936, 33108, 79906, 94200, + 104466, 80492, 72337, 73422, 54099, 254560, 176028, 6993, 73771, 49079, 55319, 58712, 86115, 97967, 23109, 55938, + 5080, 244577, 48923, 66103, 7669, 640, 49551, 74043, 30891, 80537, 202612, 47981, 111700, 26871, 4345, 17399, + 13931, 293811, 135578, 107640, 25276, 30158, 17676, 15676, 72289, 37101, 1637, 43083, 135447, 37641, 14254, 111332, + 14820, 13404, 34584, 56626, 258641, 7240, 63894, 83112, 25265, 17841, 32376, 48491, 31005, 66732, 30950, 9648, + 281179, 112290, 34755, 61683, 75286, 5189, 100077, 59697, 393, 103531, 23185, 179430, 95359, 298178, 110282, 125995, + 14623, 78807, 24189, 26684, 13584, 47803, 47440, 29923, 6680, 25153, 12281, 81189, 101227, 5727, 57666, 53928, + 80173, 157148, 23623, 17510, 44933, 56582, 107749, 28680, 76666, 75185, 175076, 32262, 54542, 14210, 77349, 27496, + 13244, 83199, 3441, 55821, 39348, 3757, 3667, 123147, 458, 15000, 19818, 5639, 25379, 68555, 51878, 6205, + 109451, 7850, 13287, 9188, 134348, 386526, 16856, 19356, 81143, 264611, 26487, 30169, 6959, 42394, 96934, 7084, + 65554, 79211, 22545, 21576, 12027, 4118, 60397, 13483, 51311, 75590, 42156, 36096, 8716, 11493, 42998, 11218, + 57589, 275790, 7172, 33265, 140731, 69517, 43030, 8376, 28467, 43930, 2234, 31591, 23316, 47974, 14197, 146070, + 17272, 6751, 33924, 168150, 30458, 113416, 293380, 11766, 25980, 203311, 28924, 162345, 55229, 20334, 34079, 27402, + 77197, 13365, 186022, 69870, 83798, 55050, 364150, 25353, 28302, 1155, 109582, 70417, 114784, 7067, 16416, 132275, + 7428, 45143, 48146, 46692, 34548, 35154, 92593, 5358, 26241, 23637, 54860, 9482, 14712, 7966, 32576, 13535, + 39336, 35734, 47925, 187574, 103304, 90255, 22548, 13788, 18928, 36142, 63464, 150312, 54080, 263654, 319602, 6537, + 12870, 133946, 9773, 20050, 334, 130222, 30305, 136258, 87722, 40831, 167627, 13993, 15208, 85494, 50771, 220399, + 16895, 50769, 10053, 113498, 142098, 93461, 17165, 99681, 114262, 41550, 192972, 66158, 39820, 17436, 87519, 144390, + 83913, 82212, 14723, 8746, 57817, 78233, 11144, 30225, 28682, 86362, 276167, 25943, 7721, 38719, 161361, 102297, + 14900, 88287, 14336, 12092, 108672, 42339, 328, 10290, 11250, 44623, 111087, 145880, 62246, 20511, 67542, 263445, + 42849, 24396, 94945, 30646, 415188, 26446, 102124, 18065, 1724, 4925, 110914, 163915, 26555, 176996, 8050, 33583, + 24549, 11288, 16296, 29023, 25505, 6867, 86739, 11159, 26443, 84520, 68545, 10696, 107450, 65107, 90951, 10518, + 145899, 31404, 52435, 29234, 61035, 11336, 53944, 64679, 43528, 83757, 4052, 13189, 6901, 39247, 35310, 26976, + 60726, 185599, 8030, 4198, 65906, 57296, 259345, 122777, 267741, 2857, 142950, 19003, 21338, 112410, 33257, 200700, + 147590, 74901, 51360, 32601, 42079, 29847, 124456, 34389, 18924, 20790, 120555, 65991, 73017, 171882, 21281, 26841, + 135236, 5978, 4123, 303, 15393, 27267, 28700, 249892, 5206, 105391, 162130, 107419, 4026, 62796, 18843, 50664, + 84185, 9681, 10383, 108809, 1531, 34176, 8061, 39095, 5988, 39057, 7403, 4419, 113890, 60683, 85058, 11712, + 82647, 76332, 51237, 903, 303391, 133929, 25009, 138549, 7386, 175781, 132183, 3037, 69844, 21065, 30442, 4101, + 71611, 155271, 265989, 32740, 189865, 56230, 135927, 48500, 76523, 108510, 11776, 16685, 31877, 27734, 41614, 24689, + 13315, 15066, 48022, 4309, 19314, 41098, 90569, 30515, 198575, 24381, 154303, 42859, 32821, 78665, 30662, 14747, + 1928, 59755, 28149, 70209, 67641, 20901, 5264, 50251, 25913, 66241, 490439, 175537, 104475, 97516, 78264, 91266, + 103489, 23865, 183520, 34766, 3297, 275917, 146670, 25323, 70391, 25755, 49964, 164202, 18406, 31978, 16441, 52632, + 15446, 24429, 4215, 37736, 113347, 8883, 22563, 15500, 19295, 41760, 78521, 113283, 93790, 25764, 24081, 23658, + 27856, 43669, 81754, 11052, 1792, 147034, 105048, 59257, 167471, 86802, 148695, 15116, 116449, 115822, 22405, 24926, + 8541, 22171, 31801, 33192, 4408, 12297, 301197, 138987, 41757, 44743, 115490, 73003, 63233, 12310, 113745, 80287, + 25765, 1137, 45241, 12509, 86680, 100507, 15502, 82114, 64501, 29571, 9042, 4784, 27034, 836, 106118, 79642, + 24816, 19191, 71859, 10806, 34975, 35721, 20447, 33671, 6079, 126054, 58217, 78753, 4486, 35660, 45492, 39072, + 49693, 135128, 38873, 1595, 36229, 21988, 86413, 27520, 16917, 83041, 32578, 42649, 21581, 17612, 3706, 5582, + 62426, 61684, 21930, 147493, 27862, 16374, 25590, 69477, 11612, 15240, 18552, 19226, 54284, 19154, 205, 44618, + 35702, 62029, 11975, 135778, 194034, 34324, 9287, 92145, 355, 83533, 389, 11125, 24277, 28651, 33600, 110599, + 48262, 80091, 24087, 86535, 87411, 65839, 48531, 5435, 70504, 1680, 141541, 34304, 310164, 9214, 109239, 74125, + 118018, 80462, 100258, 37839, 12516, 18111, 111964, 15304, 47559, 22475, 250341, 55009, 43502, 72785, 26068, 56283, + 57433, 145320, 83034, 101357, 107139, 13166, 65124, 29871, 9290, 47434, 20163, 28721, 66533, 101179, 26384, 119496, + 80863, 26599, 33186, 50921, 14634, 49049, 8156, 90368, 34312, 71503, 2924, 84269, 91725, 54206, 70953, 60570, + 28606, 1961, 1020, 118183, 21342, 60064, 25713, 117531, 67241, 26343, 257386, 77026, 72355, 28646, 61026, 94224, + 43244, 94932, 4601, 230976, 375789, 103456, 58534, 48852, 37402, 24109, 241400, 52782, 174015, 1515, 35127, 236213, + 105070, 41444, 3868, 195472, 8342, 37810, 28026, 30469, 44167, 123934, 17110, 49127, 67494, 4950, 89802, 22448, + 1890, 32145, 62103, 193571, 16365, 8100, 2759, 59208, 11723, 30626, 54047, 111425, 271002, 34847, 30791, 102173, + 1865, 152807, 44228, 16334, 47918, 19851, 52637, 48405, 8350, 22131, 69413, 35540, 45564, 53848, 57537, 202520, + 27742, 16511, 37103, 9857, 25110, 80964, 59758, 10709, 125803, 10945, 60525, 12999, 8553, 3885, 21820, 165805, + 49504, 26657, 12487, 30455, 81925, 76254, 4388, 51128, 62211, 301599, 142773, 27276, 4534, 106190, 11978, 19483, + 15491, 115826, 50411, 58796, 19011, 32938, 119108, 220904, 80373, 67031, 70541, 4859, 206920, 6090, 19310, 22573, + 667, 55921, 9933, 6880, 102405, 3647, 62961, 136965, 128623, 63897, 23416, 79705, 245524, 144775, 47359, 10859, + 5553, 97850, 6803, 18191, 113309, 30019, 22922, 29253, 192739, 61644, 10879, 93327, 65766, 71215, 147457, 80167, + 19567, 55770, 29797, 29274, 22832, 23356, 42325, 44027, 261958, 72646, 19852, 9637, 29679, 36046, 49336, 14687, + 21293, 77708, 14113, 74893, 71134, 200672, 39308, 12740, 20962, 86248, 26029, 50842, 105123, 136390, 98208, 22087, + 24721, 49911, 106064, 73490, 860, 163439, 14873, 41067, 21752, 30501, 145265, 76566, 33448, 28437, 8815, 16951, + 18372, 74873, 29462, 32916, 157167, 37777, 218069, 57242, 94822, 93459, 63003, 77897, 35770, 25963, 42205, 118099, + 173224, 15519, 76989, 16637, 232737, 22211, 31315, 67805, 75729, 4140, 57334, 9310, 28937, 79865, 138213, 106821, + 46828, 51030, 76484, 117312, 28062, 12545, 71393, 159499, 25453, 210547, 151602, 22228, 5207, 75071, 53864, 71005, + 140366, 13537, 2178, 11825, 36665, 45071, 70308, 57129, 30652, 16553, 302183, 10738, 6169, 43148, 24995, 57331, + 67920, 86667, 244672, 341687, 150458, 19053, 961, 107389, 92040, 192870, 41097, 22344, 23186, 119577, 34986, 45018, + 184604, 177949, 6669, 18473, 92330, 10137, 20330, 189512, 20891, 13257, 66265, 48954, 176492, 72915, 219860, 2494, + 49427, 18529, 56158, 30214, 27828, 171123, 69463, 40254, 38305, 23967, 79164, 66024, 42495, 299257, 23031, 106341, + 143982, 353, 39736, 75709, 49560, 70040, 243406, 1642, 25503, 56434, 81502, 48303, 90043, 52859, 24462, 43046, + 29747, 41457, 23434, 42918, 65328, 52708, 5329, 21975, 47830, 3326, 160281, 95290, 12932, 95952, 35520, 107324, + 11068, 52610, 109869, 64849, 77721, 9674, 61370, 154578, 9003, 27427, 87582, 116020, 25213, 95646, 34677, 3719, + 94205, 2145, 19568, 65295, 140426, 3088, 26113, 131686, 46090, 188040, 30031, 72073, 89945, 2538, 23463, 34360, + 138173, 3342, 84724, 64829, 192691, 8206, 251775, 2536, 33329, 64010, 2755, 48205, 112232, 33297, 244729, 27663, + 129905, 107744, 55337, 67101, 35709, 152617, 74645, 44141, 27514, 12925, 107358, 33190, 1841, 66538, 7298, 34436, + 19957, 54584, 3634, 41173, 31411, 2298, 3434, 77461, 127476, 54373, 77688, 7987, 53572, 15128, 19113, 176061, + 17497, 39049, 101234, 59914, 173549, 48281, 54139, 65147, 55063, 16371, 43136, 40263, 175135, 13721, 69771, 59399, + 19841, 1955, 57439, 88361, 69314, 130279, 804, 37567, 5192, 185175, 75166, 10500, 237921, 127018, 7558, 35337, + 117660, 21372, 36787, 27678, 150697, 7, 190870, 106339, 4060, 7260, 122007, 5881, 273045, 63325, 39801, 38618, + 50414, 113953, 105525, 17559, 98940, 56463, 347332, 34915, 65348, 25837, 82591, 5365, 153665, 27182, 7831, 15055, + 164423, 1182, 30831, 177372, 58804, 5448, 49128, 44734, 156695, 4975, 125400, 91561, 48994, 97252, 49285, 17162, + 213928, 127791, 49987, 50768, 86036, 12840, 111058, 253850, 28608, 197563, 19740, 127785, 8355, 34689, 65656, 32199, + 39574, 8110, 23600, 97524, 34540, 38651, 19006, 29152, 16927, 100216, 30893, 172304, 135680, 31450, 91503, 54177, + 18374, 32795, 63764, 459294, 151587, 85350, 39064, 13067, 10830, 3717, 20553, 32482, 53805, 108785, 109353, 20145, + 16878, 76255, 16289, 14152, 16623, 3446, 23337, 31309, 4282, 24663, 64821, 61752, 48030, 64655, 21808, 264145, + 8537, 50728, 25184, 49171, 14986, 13324, 23567, 199062, 46102, 179857, 99718, 369654, 13062, 27072, 2232, 105686, + 72897, 219385, 64202, 22442, 72, 52447, 22847, 94762, 33050, 52976, 8735, 2293, 108227, 50715, 42136, 12707, + 39451, 45981, 114988, 190349, 45935, 22798, 12654, 1, 651, 11355, 22585, 15841, 113320, 18682, 87649, 22561, + 40535, 140869, 61447, 16658, 95176, 80270, 61544, 83797, 57450, 101532, 133714, 89999, 48843, 172813, 18252, 163124, + 5003, 103269, 9853, 67492, 19019, 55271, 3109, 55823, 10407, 119899, 97338, 54114, 211163, 4927, 123086, 69260, + 3848, 55061, 18449, 12690, 1068, 37710, 26424, 11375, 4988, 41383, 92404, 48881, 32091, 48305, 36150, 113778, + 30095, 105405, 16612, 40433, 41692, 73917, 51729, 55139, 15099, 30180, 50, 16916, 43602, 95240, 47258, 86059, + 107434, 94751, 15026, 33649, 50744, 49046, 74109, 13167, 7627, 11804, 18035, 3335, 171349, 35806, 44194, 37671, + 16313, 34545, 198682, 35794, 150832, 210760, 258621, 12579, 352665, 110221, 193929, 21773, 207750, 141990, 78065, 65827, + 33937, 281, 49827, 8372, 38256, 111292, 55786, 57932, 51091, 10740, 12648, 39213, 156000, 72468, 27361, 213358, + 87889, 22207, 42213, 35711, 90663, 88229, 37662, 37545, 84175, 5983, 52865, 9162, 24908, 28484, 109135, 3656, + 114900, 154191, 40016, 143364, 50365, 4998, 47423, 91888, 31494, 33385, 89791, 113590, 83829, 74958, 6063, 23411, + 5398, 3346, 29188, 43992, 169342, 124619, 152146, 38176, 47521, 837, 5847, 40491, 54818, 14886, 64782, 79830, + 18935, 46064, 22834, 11304, 8356, 14908, 14164, 58309, 43094, 59761, 58932, 55478, 41212, 27362, 8157, 45308, + 174536, 290996, 677, 204177, 10082, 87199, 60656, 99512, 92550, 18666, 17670, 8755, 6678, 78663, 12108, 219237, + 60614, 81551, 23867, 117589, 23355, 14754, 99693, 35914, 69721, 75856, 71852, 97445, 14796, 53501, 37755, 5823, + 34149, 11053, 56010, 32326, 128830, 80883, 474, 3312, 58187, 4593, 94897, 82655, 3179, 117179, 34370, 37073, + 208, 174, 40568, 42678, 40325, 118866, 28501, 3518, 28399, 91754, 79629, 270203, 225029, 103041, 171673, 19198, + 401412, 202372, 71959, 27441, 51150, 57934, 46575, 551, 31580, 48734, 52559, 6830, 207268, 88303, 10399, 26375, + 6657, 26942, 1499, 28435, 10993, 84614, 864, 33684, 69818, 63313, 138059, 44306, 64282, 22203, 52406, 127830, + 289845, 11019, 2908, 36009, 23308, 8408, 38414, 42453, 12961, 116672, 9638, 175093, 38447, 99982, 7614, 4603, + 6681, 54049, 103103, 12820, 52944, 2652, 87605, 137098, 31855, 44982, 31388, 16335, 2572, 234999, 76439, 59626, + 47646, 105458, 231, 16630, 120728, 71649, 54479, 42672, 179148, 62338, 5367, 4698, 37240, 85883, 273485, 122580, + 45196, 6452, 17224, 35656, 218274, 532, 77135, 92225, 4816, 24612, 23330, 78494, 3695, 84373, 30447, 293164, + 21961, 19227, 40712, 50432, 50084, 83383, 130654, 3512, 35209, 106119, 26859, 2775, 18073, 188766, 9641, 22040, + 51452, 7828, 120628, 59247, 27004, 7212, 84542, 50515, 6100, 130271, 27415, 45596, 33941, 106546, 4823, 107962, + 1377, 42166, 117980, 25577, 84831, 24787, 184967, 17471, 171214, 62502, 4444, 8334, 85, 27407, 295919, 244072, + 141510, 43179, 145423, 52704, 9078, 33296, 18231, 71008, 99227, 13981, 68573, 4322, 32610, 51176, 165546, 3853, + 6417, 145489, 23086, 27479, 11718, 56566, 19653, 100740, 49868, 121955, 56420, 11535, 65579, 132995, 125548, 43942, + 87902, 58981, 4510, 84294, 73018, 226515, 1295, 68198, 49062, 157567, 27234, 124146, 46280, 100486, 144184, 15600, + 61742, 26572, 61714, 65125, 21512, 7799, 35874, 6311, 40862, 35522, 45414, 16108, 107733, 43364, 9206, 73819, + 15941, 51689, 82329, 40065, 29168, 48562, 85845, 69609, 157765, 60708, 25387, 1180, 144919, 159797, 25726, 214431, + 14487, 5968, 68537, 109664, 5767, 13490, 63443, 104676, 158014, 10404, 26593, 10161, 140070, 96476, 96798, 10196, + 7241, 29156, 51314, 97628, 573, 118109, 8622, 3106, 71584, 57894, 84024, 11036, 16921, 66038, 61545, 106441, + 223566, 16117, 74626, 3336, 40331, 47655, 20982, 117267, 179473, 76397, 121704, 23368, 35081, 186150, 1889, 47653, + 47926, 33122, 15734, 26894, 140885, 14802, 76951, 41988, 41508, 57629, 16634, 12405, 52104, 20107, 218288, 100668, + 59180, 73629, 1683, 30932, 42310, 64739, 20003, 6633, 32811, 26700, 39873, 153638, 29048, 2831, 22955, 8961, + 123517, 244356, 25796, 26746, 102413, 144572, 12002, 20480, 80208, 92037, 145215, 65587, 10104, 70587, 35982, 10208, + 14746, 188951, 116180, 117036, 12649, 257536, 49699, 32220, 153641, 10918, 10962, 51792, 126022, 13715, 104110, 23594, + 37965, 15247, 6442, 44822, 113017, 28398, 13830, 44800, 4171, 120616, 5418, 1810, 83, 42459, 4381, 81522, + 142592, 107242, 4170, 85703, 2809, 7049, 62349, 190193, 6362, 36642, 21195, 33097, 50416, 52066, 84992, 65769, + 71323, 20902, 52748, 114648, 116894, 25884, 34351, 102634, 260776, 19638, 86892, 17434, 16204, 19854, 106540, 27954, + 1524, 13745, 42151, 138947, 5760, 153807, 35075, 95356, 30351, 27161, 68708, 53500, 12658, 22077, 63851, 8487, + 20703, 57740, 44334, 64734, 54403, 39682, 77475, 5602, 36083, 1112, 36181, 71932, 45408, 99180, 206226, 42336, + 74772, 77663, 25805, 117083, 4946, 39476, 36769, 30289, 14485, 5872, 59638, 72213, 50759, 23451, 882, 2453, + 111222, 168615, 130208, 48836, 10890, 90002, 55698, 21422, 2195, 35834, 39131, 16781, 167147, 16091, 54925, 18399, + 92962, 80011, 5820, 4726, 130534, 187899, 869, 40302, 16283, 28616, 86006, 14823, 177256, 25701, 70837, 29786, + 35016, 19926, 80067, 4711, 15472, 93684, 2584, 58032, 210156, 70971, 75498, 15685, 151187, 60994, 38213, 13471, + 73922, 9338, 117718, 24543, 117691, 15713, 45967, 200243, 43250, 36553, 35694, 36433, 52051, 152826, 305512, 217989, + 37392, 40189, 4153, 56219, 24811, 51616, 37703, 87103, 24358, 84298, 167734, 60608, 30830, 95114, 82423, 123075, + 5775, 16326, 137007, 23746, 818, 184283, 59155, 49161, 21969, 92570, 27322, 24660, 1476, 194447, 116982, 30577, + 127322, 117428, 1856, 80745, 151783, 5171, 15901, 75451, 58392, 49455, 93446, 42926, 31021, 17030, 17243, 171279, + 106913, 15354, 115117, 51694, 65215, 88371, 23841, 28644, 89407, 71198, 6973, 57127, 90802, 67682, 21453, 30346, + 28531, 59792, 72619, 106195, 11690, 597, 21636, 30078, 20234, 8145, 91408, 50011, 95249, 25250, 66246, 24442, + 44602, 12103, 41001, 105897, 37256, 44489, 85248, 1331, 18707, 29983, 310182, 6411, 11928, 10116, 19299, 122916, + 5161, 82625, 56098, 136518, 4410, 33338, 119068, 31371, 26571, 52839, 11442, 358, 51903, 115795, 48253, 212226, + 49768, 72313, 32154, 54738, 22008, 16766, 174325, 98378, 25252, 9732, 16533, 147195, 65780, 41940, 24564, 81099, + 209499, 21378, 137617, 184321, 68769, 172072, 71325, 81618, 203726, 24974, 21300, 111798, 13249, 30461, 47901, 78074, + 137363, 96937, 205703, 15259, 48845, 38294, 28061, 109460, 86823, 28722, 44363, 19999, 6658, 142277, 14939, 11150, + 5674, 45392, 60588, 177764, 31881, 6786, 145293, 13598, 1083, 12784, 3617, 14433, 1823, 25033, 79112, 70251, + 108676, 88876, 67887, 11458, 34518, 12199, 148504, 65495, 166752, 78027, 54905, 18762, 13791, 20914, 58692, 1568, + 14287, 15068, 7216, 15244, 91576, 191867, 58273, 3830, 91429, 78507, 84897, 9770, 8665, 7954, 43039, 48860, + 11529, 61697, 166056, 55960, 26401, 61415, 290831, 12539, 16191, 30889, 13589, 1191, 91972, 41144, 4955, 34048, + 30964, 87299, 107280, 64425, 5254, 43169, 46627, 18402, 28486, 30816, 67369, 1564, 54697, 41405, 16000, 32524, + 79613, 30190, 43938, 8057, 66520, 53870, 1494, 247505, 18447, 16053, 29278, 66743, 22870, 25668, 1648, 14080, + 45203, 1341, 40989, 119871, 194466, 122534, 8385, 58819, 22822, 35970, 12729, 29360, 51703, 27032, 51912, 51956, + 12278, 36617, 79242, 39507, 76716, 85023, 73180, 18140, 44595, 125017, 191485, 174629, 73455, 77570, 220522, 125113, + 33546, 90187, 62766, 35279, 12235, 8675, 15151, 50393, 144843, 26013, 205214, 46310, 36154, 69776, 28572, 32563, + 51247, 38454, 4595, 42074, 11116, 86835, 30706, 10273, 33040, 34204, 54246, 91737, 3180, 77652, 106293, 106121, + 225753, 62203, 83244, 49829, 60864, 33244, 3262, 132227, 1972, 167168, 175800, 113557, 28469, 1342, 99125, 98666, + 12891, 8033, 119055, 3277, 28879, 37357, 275688, 62785, 10338, 60445, 97431, 99394, 144157, 1870, 20794, 59985, + 56294, 1569, 12614, 65686, 353058, 24023, 105292, 40234, 38302, 59113, 20587, 39754, 41447, 7733, 28382, 149537, + 87532, 70154, 27770, 8584, 110616, 28877, 50839, 33339, 27065, 8349, 41578, 41373, 168438, 10230, 58202, 18179, + 6557, 87189, 41859, 112308, 1213, 37229, 12748, 127395, 50804, 25519, 6813, 29126, 144643, 51945, 3761, 173270, + 24817, 37177, 11538, 1953, 2390, 71610, 55025, 12286, 136531, 8290, 7081, 13438, 38174, 12201, 368643, 56955, + 247513, 86715, 29189, 151151, 16190, 44518, 9116, 26301, 4059, 29547, 121363, 528, 122791, 104758, 128283, 132963, + 131994, 18283, 17120, 57082, 137430, 286470, 90537, 63450, 39506, 73884, 58318, 16044, 57650, 17259, 42080, 17885, + 16305, 157015, 93813, 43437, 5188, 134150, 32055, 268669, 54309, 84632, 18425, 114608, 106128, 82465, 25150, 81372, + 20628, 50827, 203900, 88756, 88071, 113318, 88552, 32344, 67394, 25784, 120662, 65041, 395446, 1313, 179364, 2878, + 250285, 16496, 42810, 142259, 66176, 14834, 29115, 136061, 91254, 103667, 12871, 26008, 1399, 9634, 6954, 97146, + 114196, 292674, 65716, 14216, 43915, 106501, 379, 35470, 60230, 24709, 71955, 28003, 44853, 42762, 19842, 9247, + 27206, 76172, 35445, 42656, 106353, 30864, 56216, 217302, 43013, 490, 12455, 125743, 18733, 112917, 66668, 5890, + 345105, 38120, 9856, 28648, 226453, 13944, 99130, 54004, 51202, 214051, 47536, 22937, 16607, 40104, 54194, 4979, + 57106, 15086, 23012, 12071, 117175, 174267, 29878, 59251, 35492, 196132, 120077, 81399, 10476, 19539, 129457, 31908, + 89598, 42460, 90787, 28424, 127439, 6776, 101077, 81013, 15187, 1074, 58103, 66003, 39624, 68595, 18810, 173127, + 13688, 6576, 66630, 43484, 61570, 92693, 65418, 85754, 10615, 177935, 31294, 91906, 31111, 386524, 52324, 16388, + 59370, 52508, 156372, 25357, 6238, 72256, 41599, 57828, 175252, 163986, 132645, 50076, 32143, 95350, 15564, 103443, + 224492, 75148, 26023, 120071, 41388, 19532, 110427, 22508, 95408, 89126, 17624, 37562, 34384, 9140, 91145, 109567, + 148238, 18379, 47470, 5638, 78307, 70465, 82451, 53859, 38959, 18925, 14088, 22217, 9340, 26777, 74821, 42124, + 160091, 16523, 3150, 97181, 61443, 8097, 65561, 68601, 15737, 115420, 25095, 57655, 11216, 70875, 87640, 78471, + 41244, 28465, 55017, 134190, 170, 58246, 16739, 39956, 38299, 255505, 2797, 2174, 102443, 13841, 69822, 12621, + 113097, 6991, 123270, 37586, 26382, 47496, 42833, 10023, 14027, 38076, 52804, 80220, 33707, 4788, 3121, 7610, + 3957, 167985, 5094, 37233, 76300, 62786, 189431, 11488, 66160, 1236, 76849, 5333, 19431, 42643, 23661, 46201, + 18900, 8417, 18568, 111327, 6952, 44621, 24495, 38741, 1717, 138255, 22782, 46607, 108656, 236097, 24621, 9067, + 82206, 38888, 253672, 45369, 188021, 74422, 200471, 3792, 257335, 14028, 151249, 5429, 27295, 141619, 22966, 27219, + 43999, 105930, 97394, 24617, 41210, 3333, 88262, 22024, 31777, 58259, 8812, 91559, 46956, 22151, 60598, 161311, + 57457, 123650, 86473, 64439, 12657, 10686, 130688, 112742, 11489, 53274, 26714, 21670, 15697, 30443, 104596, 7868, + 48060, 22775, 3022, 19869, 204748, 16977, 184709, 89313, 53583, 83928, 92875, 99194, 82422, 96190, 2556, 47490, + 284790, 12772, 5841, 48964, 30503, 33825, 99246, 251304, 137341, 36338, 22912, 3614, 8120, 31432, 14001, 2727, + 19615, 36074, 75714, 22938, 220311, 52593, 32987, 17971, 15991, 102877, 210170, 136379, 10217, 43348, 155559, 9056, + 63424, 28650, 29017, 9663, 9808, 49301, 50859, 10641, 67431, 17280, 61331, 20739, 70976, 97391, 58235, 36525, + 98221, 122956, 57506, 98979, 4491, 86694, 28324, 129, 15177, 9809, 3222, 215310, 28535, 4761, 16001, 1184, + 144789, 181348, 54083, 88078, 751, 22452, 65081, 1577, 13230, 27685, 98822, 56681, 2394, 90263, 54478, 144599, + 1504, 78572, 173001, 99606, 33977, 33470, 29437, 39886, 132104, 10699, 34506, 36978, 30316, 13646, 16311, 29262, + 22230, 50283, 49086, 343445, 931, 13052, 125899, 139325, 97193, 24009, 38257, 76027, 185240, 47587, 137522, 115144, + 24826, 38532, 19149, 8495, 22687, 75105, 130036, 15268, 174322, 68514, 245144, 17081, 15307, 34585, 208142, 75209, + 22988, 36011, 65, 2906, 1390, 60888, 44865, 144040, 188745, 118480, 95778, 32437, 180325, 4138, 10609, 92925, + 29580, 8808, 159680, 42631, 59068, 29860, 171355, 10899, 74903, 33949, 320605, 9425, 18994, 26854, 7737, 53509, + 29195, 107306, 35880, 21197, 79, 68771, 286937, 4362, 15436, 42681, 71303, 124778, 7622, 25028, 9618, 122572, + 38462, 11060, 66457, 65269, 11566, 72952, 5073, 71968, 138710, 28743, 12069, 66022, 44828, 82002, 156524, 81292, + 45774, 14165, 218072, 86389, 37768, 116234, 37323, 222673, 99236, 417011, 6380, 170851, 68137, 22809, 50851, 17147, + 84083, 118504, 78497, 64504, 19282, 56977, 84684, 68011, 22698, 100149, 2846, 125107, 17134, 46339, 16369, 72262, + 74807, 15652, 17984, 99115, 126662, 49499, 64245, 224198, 173497, 81277, 63478, 3449, 46248, 2829, 31143, 91485, + 16938, 9355, 21751, 89231, 119735, 2651, 2158, 25221, 3212, 1095, 134321, 26633, 28292, 72271, 10874, 18895, + 213652, 343495, 36158, 6930, 49013, 9714, 53844, 16595, 9975, 99720, 38334, 23140, 32180, 298162, 284394, 20189, + 45660, 51804, 12038, 74719, 86250, 44131, 68813, 48629, 4801, 41574, 219878, 76411, 68788, 91859, 17071, 199893, + 95490, 13890, 126132, 21590, 77482, 5070, 117208, 183553, 113751, 775, 118421, 47980, 11994, 16510, 60560, 22757, + 44624, 41900, 22489, 161977, 94452, 40768, 256639, 97607, 46839, 15049, 48016, 183793, 128497, 40127, 59466, 43034, + 100316, 61744, 20099, 72276, 5798, 4254, 61106, 151277, 58588, 78938, 208785, 23350, 73184, 13401, 114456, 168253, + 202987, 128773, 32481, 9314, 65417, 80566, 15061, 20781, 37790, 80269, 18985, 16154, 88524, 11484, 16349, 5922, + 1606, 101590, 83867, 4032, 43156, 17265, 40946, 123245, 97964, 46724, 2142, 201438, 105717, 55537, 40251, 107387, + 34947, 130879, 26300, 2025, 11203, 27400, 9384, 6700, 100060, 93137, 120697, 32781, 37742, 97514, 147819, 50972, + 130074, 43696, 152282, 11325, 93653, 25846, 60051, 100451, 107799, 99294, 5187, 187837, 94311, 19648, 17481, 47149, + 196106, 2484, 185532, 68892, 41347, 6476, 26576, 262, 8035, 144425, 16194, 7546, 10780, 99032, 192083, 18268, + 16390, 38046, 139599, 36447, 27883, 48800, 8802, 104301, 118236, 16610, 9043, 30215, 167395, 15722, 14540, 10143, + 1979, 18303, 245965, 6606, 25006, 56388, 720, 40122, 19375, 26986, 4175, 5283, 31628, 70617, 156858, 13338, + 18916, 50924, 158448, 13314, 144723, 40846, 148751, 33355, 78502, 66354, 52938, 44935, 114047, 29390, 83010, 31740, + 103107, 187158, 28282, 6840, 86492, 173457, 46403, 22614, 107686, 143217, 20089, 170121, 5844, 9860, 56485, 104630, + 20934, 42133, 9301, 19064, 206963, 93906, 29729, 27462, 23556, 248023, 29615, 24218, 22591, 27525, 19222, 62444, + 16562, 40084, 90324, 40232, 146333, 178921, 45549, 11142, 20167, 301568, 34164, 125423, 10471, 17862, 4749, 774, + 117434, 30213, 12597, 85041, 33085, 58865, 17338, 4578, 2863, 16515, 49743, 2267, 9740, 64838, 32867, 305033, + 36669, 34833, 20474, 42789, 41849, 24106, 210964, 124297, 37271, 24216, 53900, 123495, 22790, 8477, 175065, 22886, + 18209, 95189, 3313, 32543, 28979, 29761, 127609, 71172, 8231, 87016, 63834, 20159, 12952, 70904, 466787, 101605, + 54408, 2160, 17597, 57212, 21731, 165012, 21316, 33552, 25130, 56209, 46615, 46375, 45208, 106318, 31681, 64073, + 55748, 7104, 76381, 85964, 138120, 4075, 21570, 28070, 75826, 73539, 7912, 79024, 414, 177899, 313993, 67507, + 29593, 5743, 4806, 12800, 9925, 25560, 9189, 117626, 292865, 50234, 102480, 16382, 25999, 50641, 18440, 9929, + 683, 55242, 2340, 1064, 123149, 61826, 15245, 38280, 7036, 24794, 44030, 43924, 92159, 34247, 66141, 23809, + 86055, 215911, 128281, 150909, 61827, 53182, 142185, 14010, 103680, 51751, 108481, 22354, 23176, 13327, 14346, 152541, + 54918, 99104, 95228, 63611, 58466, 81038, 32483, 69723, 57578, 44054, 189180, 149427, 13305, 19749, 43628, 89334, + 5709, 43087, 18148, 4104, 86479, 50105, 64469, 20382, 16697, 4708, 14117, 130911, 31064, 73543, 33459, 45627, + 17660, 15860, 57462, 86199, 200919, 78755, 79677, 80038, 10770, 87019, 8576, 17552, 49793, 46030, 21495, 35725, + 33423, 27589, 152364, 6318, 32370, 142933, 34912, 78214, 52047, 54699, 36052, 229203, 16488, 20327, 25789, 14697, + 62555, 29116, 9656, 6836, 6459, 16067, 47438, 81922, 8426, 32236, 21951, 67133, 83493, 104694, 49662, 4774, + 7763, 74850, 270584, 335979, 59725, 82959, 82821, 18110, 82812, 14354, 2193, 9843, 18628, 69780, 24991, 112338, + 67760, 191557, 92348, 79071, 79405, 72842, 11351, 56088, 68557, 139675, 23222, 148134, 9612, 12610, 21344, 25747, + 7673, 584, 17873, 39734, 28102, 18328, 10063, 14720, 56517, 1902, 69798, 38307, 69620, 33351, 1174, 19948, + 171797, 67288, 84834, 16123, 32458, 25946, 172250, 8199, 29541, 28207, 15618, 8731, 15870, 23596, 47369, 57922, + 81109, 26904, 26073, 8326, 32080, 57471, 44892, 162057, 207644, 334076, 10101, 4119, 71495, 49601, 2592, 19742, + 21202, 14849, 98354, 61825, 11039, 158223, 75426, 119901, 91036, 68746, 116495, 8557, 61230, 102302, 14765, 75658, + 2810, 4942, 28526, 36256, 130800, 67752, 202742, 33081, 32260, 193926, 185696, 4064, 4613, 295863, 166466, 13260, + 60590, 1252, 145391, 2657, 37112, 87184, 227365, 8194, 75214, 88155, 115530, 90924, 33979, 90533, 27556, 51339, + 126402, 49225, 196178, 34452, 155062, 4813, 17478, 33954, 30642, 120974, 35852, 38833, 63875, 31380, 62028, 58381, + 12810, 7419, 98274, 1977, 194463, 145760, 23510, 116833, 82799, 19072, 2433, 145655, 47664, 4834, 69147, 46751, + 16725, 33328, 38665, 115531, 36685, 76090, 11537, 18743, 43367, 17948, 23978, 41370, 61099, 40095, 66518, 999, + 449, 217319, 6688, 250897, 172150, 20516, 11330, 20451, 102867, 21452, 159960, 15660, 21691, 82391, 6601, 43312, + 301838, 29124, 21637, 110211, 36745, 105335, 60833, 98115, 7130, 2470, 75962, 2011, 18671, 50489, 79569, 101266, + 57316, 81095, 53258, 13308, 34852, 17013, 84541, 47478, 38034, 23762, 162120, 178016, 54182, 33123, 52028, 72197, + 35578, 4602, 243630, 88186, 65900, 67107, 5029, 138288, 99486, 1235, 6540, 165347, 19771, 47835, 318100, 22891, + 3456, 21803, 91103, 57561, 2658, 54417, 30476, 7012, 16914, 55333, 21913, 180607, 99866, 184639, 7485, 8405, + 28390, 37172, 89244, 53674, 28109, 98360, 69082, 3525, 8262, 79773, 254797, 87253, 21147, 105791, 15807, 58442, + 34353, 98558, 30931, 80675, 20006, 3002, 81642, 11376, 4228, 91457, 8547, 21430, 137085, 33238, 42307, 3087, + 1675, 66687, 47814, 34117, 203023, 131032, 24008, 5970, 283196, 124604, 83088, 60714, 198286, 26339, 5149, 82518, + 214375, 8762, 21409, 25932, 163329, 13237, 37495, 3608, 290603, 72236, 1508, 11575, 152574, 55633, 156361, 32414, + 40471, 48043, 3556, 2415, 83506, 9556, 79122, 233954, 30068, 33325, 6305, 159939, 14730, 53878, 89577, 30054, + 23177, 41063, 32980, 17345, 131539, 217504, 35311, 15300, 34759, 144987, 54877, 46496, 27668, 5784, 24491, 1354, + 32178, 129844, 14953, 7360, 71896, 107476, 206892, 65803, 104799, 60213, 3795, 77961, 116305, 72186, 184835, 52495, + 85430, 98086, 108950, 22959, 119262, 214032, 33931, 102185, 42860, 161725, 32444, 24541, 25160, 41398, 6650, 202950, + 8911, 27523, 50156, 13935, 23428, 255875, 23753, 49759, 49437, 771, 101855, 224178, 105322, 141973, 32780, 5494, + 6519, 83915, 103464, 195927, 16203, 18899, 2849, 150029, 6349, 3289, 4814, 219, 74711, 59509, 333, 40550, + 1230, 49476, 28787, 6325, 38045, 10647, 173625, 26321, 8540, 19101, 23643, 21796, 75165, 98886, 256858, 8390, + 44736, 107620, 67566, 91614, 25909, 54320, 31937, 195737, 51026, 52019, 46128, 10676, 317034, 7784, 41102, 123264, + 4984, 106475, 31610, 19260, 32281, 83653, 4280, 61891, 91312, 19136, 38931, 76940, 27060, 33501, 126832, 48333, + 44431, 81276, 41771, 130533, 17817, 6320, 38313, 928, 45363, 59120, 177473, 41182, 155937, 135020, 126653, 32047, + 239085, 115649, 82912, 3416, 35697, 345331, 53591, 16649, 59784, 39055, 46432, 28477, 91993, 8200, 97534, 6307, + 29531, 9129, 30788, 89098, 126740, 20671, 133582, 65905, 213757, 1632, 18153, 20878, 76560, 55987, 68969, 1600, + 167776, 51365, 34575, 216355, 285273, 37934, 49689, 21386, 24262, 69390, 24454, 75939, 8237, 18742, 88250, 165234, + 65030, 85487, 44653, 10365, 41160, 2784, 164637, 7275, 74437, 817, 5045, 54742, 48804, 217409, 12001, 99489, + 118916, 8909, 10151, 74282, 13159, 165410, 3506, 39017, 37842, 24440, 5032, 93366, 1031, 93948, 42413, 34930, + 75349, 36125, 57529, 29308, 1478, 45294, 1328, 29873, 11655, 72323, 80218, 16686, 108777, 112357, 19468, 161527, + 23435, 67822, 30370, 4433, 277425, 199425, 1173, 8369, 101734, 76516, 110263, 4965, 67469, 27648, 64330, 158915, + 70231, 148349, 33642, 19100, 124711, 6240, 206630, 5766, 43532, 60290, 1618, 11261, 28514, 49764, 75380, 44379, + 65526, 33015, 1566, 161773, 54956, 37344, 69904, 6421, 1000, 17254, 11877, 7155, 21882, 13912, 9792, 134, + 17728, 212180, 90771, 66606, 25302, 43754, 11818, 134151, 40952, 12919, 28325, 57470, 52214, 30361, 5898, 7913, + 149632, 18095, 212017, 195480, 1999, 139, 84069, 3822, 2111, 116190, 22381, 104936, 3259, 19369, 7470, 4564, + 63362, 84396, 244911, 82844, 89961, 73711, 23902, 88689, 220561, 81148, 100516, 124589, 39777, 153793, 37780, 13806, + 26335, 4176, 56333, 280949, 9063, 9260, 69363, 258594, 10572, 107880, 12115, 33299, 12416, 68082, 27837, 184178, + 34551, 83293, 68854, 109274, 34623, 9210, 18491, 59555, 38604, 267, 8192, 6400, 24723, 29696, 82525, 68604, + 5947, 72996, 15729, 703, 15588, 23700, 2015, 100398, 69927, 427, 20207, 148402, 66252, 2099, 146853, 12510, + 119177, 37939, 48402, 172082, 69173, 242876, 15286, 133076, 46629, 9996, 20910, 33571, 28714, 132255, 11444, 47791, + 70715, 103704, 9226, 28482, 212408, 75092, 6197, 29216, 20521, 24, 52569, 5853, 406913, 21243, 31218, 77868, + 74380, 146453, 7607, 72181, 11716, 15373, 26582, 8123, 50659, 30590, 227825, 66454, 50862, 49529, 80294, 15517, + 37009, 35230, 69063, 80260, 88460, 38472, 63246, 37205, 130101, 137671, 14972, 60171, 7210, 90428, 50245, 64301, + 53853, 21012, 116299, 19943, 538, 102919, 143609, 50795, 65120, 122155, 20760, 41285, 151950, 28489, 62634, 48588, + 55806, 151533, 4795, 3053, 163748, 44956, 565, 152058, 52837, 23981, 76468, 97083, 13153, 60576, 2112, 50486, + 21100, 377, 192917, 29902, 16674, 14359, 42767, 170627, 64536, 35897, 66424, 6902, 6091, 127107, 4355, 121366, + 138201, 65773, 66108, 41998, 44837, 63222, 69586, 36291, 58547, 23085, 14181, 135294, 3723, 40961, 35006, 126987, + 163, 9211, 49788, 117861, 2177, 37726, 91665, 22613, 32288, 24902, 24789, 76868, 85454, 74752, 103374, 11683, + 34033, 48129, 1456, 23503, 8497, 70596, 92766, 70637, 14282, 304999, 76392, 9980, 25742, 4216, 140344, 193566, + 10535, 16591, 137916, 20347, 10741, 5439, 17749, 74636, 79559, 244434, 10353, 2254, 117493, 6879, 36582, 273890, + 243787, 15483, 5037, 43308, 49337, 29065, 64416, 85528, 100718, 19024, 222754, 60476, 79495, 44751, 64434, 4020, + 40139, 30091, 121039, 83627, 42956, 12277, 115688, 38864, 7551, 37316, 31576, 348, 55433, 10897, 8383, 89713, + 15421, 4329, 42444, 12217, 31509, 48867, 30445, 38228, 23034, 8090, 37931, 30345, 45081, 21129, 36808, 88429, + 547, 39635, 34098, 148415, 61176, 52774, 24919, 16366, 53434, 13434, 146264, 79719, 328001, 5483, 62687, 73315, + 8470, 79268, 19141, 72096, 36263, 44493, 236350, 267628, 30145, 211091, 25890, 14437, 4519, 17070, 79714, 73443, + 74173, 53239, 98936, 72193, 53935, 17849, 592, 6437, 11845, 802, 96206, 13472, 73774, 36519, 15404, 33551, + 60211, 17322, 196495, 29339, 78025, 276332, 54124, 171051, 3, 52430, 53849, 77154, 4102, 16020, 8709, 109741, + 73782, 54762, 26431, 7665, 109293, 2201, 111613, 5780, 315332, 245103, 65577, 66474, 48412, 162153, 10534, 61430, + 26683, 61829, 32733, 13780, 13714, 81304, 58398, 119619, 6865, 107753, 132039, 172363, 20128, 121666, 235595, 131904, + 18490, 178167, 57539, 35059, 104141, 15949, 75689, 5299, 325, 57947, 48755, 8362, 39470, 20406, 30082, 13818, + 171970, 31118, 19942, 97627, 115860, 172133, 40888, 75047, 15707, 107467, 49758, 2751, 7268, 102546, 139896, 5441, + 73301, 107048, 22686, 65676, 74336, 38585, 54155, 188892, 31370, 24110, 165124, 94512, 2368, 74483, 9470, 16357, + 111827, 4043, 96403, 170548, 107757, 169476, 28693, 28709, 136231, 117890, 11783, 151383, 142844, 158445, 124615, 108842, + 97138, 165759, 118091, 170718, 96638, 151535, 50875, 26742, 84053, 96653, 22587, 72385, 38691, 81135, 2787, 15019, + 23801, 14048, 175978, 40360, 1588, 63401, 3408, 17858, 10573, 129113, 76021, 1698, 122098, 41563, 91014, 29385, + 77039, 205898, 31782, 66049, 240, 30201, 29388, 36852, 186135, 119600, 62862, 71976, 146101, 66513, 162780, 57466, + 11996, 13198, 214801, 18524, 18143, 9123, 160460, 15278, 112963, 50571, 55357, 30128, 46171, 94183, 55715, 34086, + 155836, 66742, 311911, 22315, 348769, 10015, 161530, 38573, 47795, 32056, 23574, 5351, 121213, 7806, 95295, 21922, + 116266, 57531, 199257, 102740, 132515, 64909, 90793, 50599, 3344, 13994, 153769, 111152, 144804, 6819, 219255, 121782, + 31286, 2640, 54412, 62437, 99187, 80216, 98506, 1973, 255838, 88342, 73727, 16363, 129694, 9582, 119806, 25823, + 35894, 95168, 13820, 13961, 48779, 36969, 6826, 36122, 55235, 97975, 132524, 23098, 148582, 4514, 57373, 77831, + 71967, 9045, 37633, 44748, 65282, 84429, 23687, 7236, 15174, 64021, 45128, 22679, 13920, 80494, 17542, 17632, + 47080, 10822, 58383, 72334, 6147, 18237, 59831, 194844, 108242, 19788, 180510, 100731, 157593, 30326, 279827, 55366, + 125025, 15314, 11547, 48137, 7136, 21272, 48386, 58395, 1286, 9368, 30466, 43535, 28957, 219088, 24784, 30339, + 127956, 154838, 51263, 15865, 28402, 41075, 56222, 63661, 98813, 65963, 4997, 12583, 20805, 75481, 56536, 95023, + 7532, 156833, 60839, 127105, 109417, 17040, 15236, 207517, 40610, 22003, 924, 154828, 5041, 3149, 61584, 15751, + 32958, 30934, 164321, 30734, 15142, 101107, 30660, 103621, 18408, 12233, 98156, 98027, 108596, 11650, 55792, 146477, + 33543, 50057, 68000, 98194, 50517, 22325, 104336, 17124, 27748, 70931, 26858, 118550, 80114, 17779, 47640, 40187, + 233434, 205828, 163803, 9522, 91447, 45870, 85576, 87308, 487, 32686, 244627, 45444, 37094, 10371, 30263, 37708, + 100048, 61011, 174186, 98247, 30541, 198823, 425277, 43101, 43477, 177323, 58960, 83354, 10639, 14794, 48614, 76723, + 89862, 7677, 6456, 663, 155868, 17446, 160748, 3648, 37667, 44426, 160030, 75580, 8726, 48941, 203882, 126698, + 60684, 139753, 22714, 49200, 237903, 165483, 83252, 25239, 73408, 26534, 38895, 18906, 99589, 26437, 80391, 31962, + 12190, 496, 115352, 1660, 38739, 25624, 25196, 314328, 97348, 164824, 64001, 40502, 3914, 11141, 3746, 80143, + 13594, 164955, 149665, 13939, 2680, 66054, 20584, 29040, 149016, 20350, 30753, 3677, 13907, 48796, 31858, 191904, + 30171, 5370, 40086, 3400, 28343, 12830, 135213, 25267, 23530, 168908, 4125, 25811, 115225, 31603, 26072, 36166, + 61104, 83325, 117213, 11935, 35821, 85360, 2192, 24751, 147679, 4560, 76850, 115369, 14337, 33806, 25191, 101567, + 4297, 2681, 88848, 31912, 244282, 37953, 201, 50311, 24085, 34402, 47251, 10729, 326976, 52264, 7804, 24341, + 56428, 30572, 45401, 26493, 7851, 42287, 88129, 15527, 21303, 2927, 54360, 26880, 131620, 27105, 560415, 199310, + 71446, 41999, 39217, 105002, 83253, 83065, 24673, 2975, 68692, 62597, 80790, 14068, 111870, 19347, 24015, 20919, + 5224, 78599, 21870, 186584, 15813, 2869, 29987, 2229, 38197, 107855, 17170, 8632, 49026, 43883, 59246, 115190, + 45057, 33719, 56476, 34016, 13660, 4646, 58901, 85953, 3306, 180651, 187566, 98029, 76345, 77891, 987, 5156, + 40671, 55385, 6206, 26225, 27905, 8848, 109863, 213205, 91072, 48382, 18078, 2792, 996, 28414, 213905, 5931, + 68527, 112270, 20314, 69524, 62085, 20144, 88213, 73577, 91351, 41869, 10074, 4599, 122634, 53272, 97538, 11166, + 55109, 35551, 37066, 49101, 168209, 72102, 82064, 21549, 2024, 9446, 465, 39252, 27560, 116621, 45956, 29498, + 112707, 228850, 6346, 6775, 17352, 61942, 42228, 65657, 8306, 101094, 17714, 124595, 167115, 53195, 69704, 69894, + 169921, 90173, 145431, 10207, 166958, 58767, 73284, 73047, 5031, 13824, 73684, 160378, 46020, 98056, 31999, 14582, + 11844, 8113, 397, 4778, 67284, 141101, 53616, 155244, 19130, 25923, 170625, 42980, 76185, 10190, 22410, 161443, + 6920, 90784, 3737, 114515, 32849, 78448, 37681, 43864, 5450, 79795, 35899, 56126, 14610, 23922, 5420, 21396, + 22451, 72777, 14065, 9126, 21002, 52416, 92088, 1118, 17392, 26827, 105671, 77612, 31872, 56056, 132315, 124658, + 66682, 14163, 4889, 103995, 84796, 16665, 66442, 3877, 13709, 21423, 164865, 200571, 40210, 92532, 13583, 53632, + 59898, 24828, 103097, 34087, 12029, 98561, 693, 181750, 8719, 55652, 72459, 64857, 53564, 30839, 32498, 14036, + 21462, 17046, 22523, 14381, 91884, 124941, 10727, 52730, 21647, 30000, 89146, 127984, 88379, 88232, 34226, 35579, + 194428, 38436, 60128, 81429, 12754, 39305, 70531, 1282, 37797, 8724, 135076, 130745, 132826, 35826, 139941, 21253, + 160016, 52919, 35823, 9597, 22567, 65729, 24463, 1528, 54383, 74937, 66907, 66449, 53186, 30489, 4666, 11722, + 77611, 109886, 5707, 100503, 31107, 178087, 63909, 42663, 8339, 41648, 8005, 13790, 4694, 67663, 118773, 48261, + 19185, 50745, 116520, 7658, 90355, 49068, 180, 69370, 23597, 3425, 108805, 81173, 17222, 9136, 45975, 93798, + 140675, 20433, 37301, 145028, 28442, 25919, 19397, 66698, 23668, 7495, 6626, 1272, 55977, 23033, 63440, 185173, + 9128, 176386, 222575, 17347, 61555, 20002, 35331, 148015, 28774, 22224, 32101, 95270, 155229, 119206, 28922, 125370, + 17658, 61847, 307477, 5300, 9390, 116888, 3866, 54029, 24379, 64983, 6059, 113961, 69357, 18619, 44315, 7700, + 15752, 90871, 72844, 35312, 127381, 68250, 248737, 128932, 239631, 59079, 34253, 144975, 62460, 30364, 34513, 135745, + 93181, 50304, 215982, 80359, 55913, 6568, 31706, 56971, 45133, 253285, 65598, 43884, 140155, 103385, 51233, 124409, + 18676, 15537, 161665, 3137, 25249, 53067, 54148, 94047, 33164, 89524, 31906, 32711, 29581, 75647, 98053, 70783, + 82837, 31002, 153684, 118904, 67223, 110578, 329466, 139791, 22362, 2416, 53013, 8222, 29975, 285682, 15525, 40047, + 7317, 13949, 37038, 37506, 52813, 5084, 19535, 108360, 1943, 92833, 2683, 39660, 29613, 47826, 43960, 34705, + 20012, 41780, 29619, 22730, 207130, 29242, 83810, 88401, 49617, 41631, 13484, 27193, 49651, 48220, 16919, 66661, + 5636, 12779, 78449, 38764, 15334, 19403, 26257, 42121, 36313, 84036, 57343, 3445, 29226, 117117, 1835, 68598, + 28084, 20588, 21723, 125697, 95247, 24456, 23959, 82657, 72072, 191527, 20720, 69714, 19000, 159615, 178013, 30853, + 224932, 9741, 2949, 53392, 15811, 65040, 40817, 91887, 114, 34555, 104656, 35360, 32127, 29705, 5951, 48069, + 38097, 8656, 40672, 7670, 37508, 58545, 33267, 101625, 88507, 8002, 107670, 43585, 37225, 107303, 21839, 889, + 27359, 44829, 8538, 29418, 91626, 112730, 12807, 6084, 12193, 229, 49391, 10635, 69279, 134886, 35206, 15904, + 20608, 57877, 82079, 26098, 11055, 91058, 20546, 8467, 88156, 225150, 50824, 258547, 92808, 13614, 10159, 28825, + 2152, 16714, 38468, 40965, 57259, 46500, 133547, 3619, 55275, 36301, 8395, 62581, 72789, 88910, 30763, 61787, + 24475, 34843, 39477, 127755, 104414, 59623, 65515, 98667, 1708, 34899, 29620, 271962, 9882, 16411, 164317, 42920, + 3818, 33307, 30124, 3990, 48661, 154034, 37142, 8914, 66897, 91285, 266, 18859, 1631, 20187, 18104, 18347, + 34806, 41381, 28001, 36157, 227930, 227438, 205318, 151106, 85815, 19169, 188069, 52621, 87753, 7676, 18539, 52732, + 18321, 344197, 32336, 25286, 250664, 72376, 6599, 95500, 72733, 207728, 65258, 4923, 25107, 38173, 18852, 277661, + 206797, 10280, 68706, 18443, 50891, 10099, 26038, 10350, 8082, 230418, 91468, 2822, 196411, 40589, 33553, 83569, + 92596, 11998, 44307, 17424, 96764, 12849, 152582, 141574, 152823, 25400, 29806, 23815, 65514, 99567, 54295, 22450, + 22819, 8254, 3779, 78344, 387277, 116301, 84235, 146179, 62176, 111891, 75668, 115049, 51225, 54668, 119070, 90975, + 40329, 139109, 42305, 58482, 15563, 96039, 231261, 608, 189, 79423, 116781, 399767, 3659, 221564, 150552, 66005, + 73670, 75682, 162148, 83033, 64357, 106094, 73326, 218725, 51793, 156164, 11491, 97189, 275136, 36754, 71371, 10881, + 33482, 5754, 4091, 21520, 86653, 55930, 27813, 23947, 74615, 44269, 3897, 24643, 67058, 22281, 108426, 85853, + 11318, 7545, 54923, 305706, 125720, 61497, 47223, 139282, 15388, 174552, 34639, 16339, 22388, 14264, 74736, 41059, + 8267, 136640, 7760, 45714, 1730, 8753, 1553, 19445, 102663, 112491, 8628, 51162, 170910, 24623, 47654, 50695, + 40784, 36170, 18125, 20583, 7144, 155439, 2288, 9897, 85373, 25015, 92695, 127810, 13040, 72704, 125096, 5883, + 7088, 2125, 51383, 31073, 94309, 21302, 31925, 6461, 583, 47582, 18007, 56348, 44224, 929, 3859, 152766, + 140424, 28867, 217612, 69471, 77439, 20336, 147072, 7793, 18496, 41996, 9932, 226194, 164026, 97199, 21473, 8913, + 36578, 370703, 232366, 40298, 4722, 21848, 30276, 32613, 9151, 13631, 19868, 1123, 5824, 133353, 58605, 148488, + 63365, 98514, 162970, 68898, 26510, 4366, 150983, 23673, 35199, 75476, 53507, 214063, 40500, 35220, 45220, 83826, + 79277, 94220, 130368, 10107, 32495, 103453, 274743, 4302, 44614, 2713, 157, 20394, 35233, 20579, 45213, 30353, + 9566, 163051, 42976, 78372, 203293, 8456, 26004, 25226, 152144, 18362, 25622, 32068, 7097, 9764, 19804, 105586, + 2330, 79661, 38440, 45558, 183480, 5445, 30623, 15936, 98629, 31188, 21742, 11320, 13423, 50238, 196800, 61702, + 330887, 6907, 165378, 29192, 44130, 36247, 125815, 76135, 68600, 3478, 102912, 38948, 30939, 32718, 115597, 14759, + 97829, 178650, 6398, 96989, 31012, 80346, 29170, 167665, 345465, 32639, 105679, 182158, 56747, 62826, 5573, 31888, + 94879, 251468, 82742, 74122, 4939, 105988, 50230, 43427, 160027, 68618, 115561, 16419, 149761, 8457, 34061, 57375, + 99423, 60632, 123694, 18569, 46099, 3392, 96711, 126188, 32251, 264812, 49338, 132633, 15332, 90783, 67149, 64854, + 8761, 62393, 76418, 81682, 96094, 50655, 103271, 39486, 128555, 50988, 205607, 4238, 5763, 202341, 1257, 225585, + 235318, 53879, 36434, 4671, 222223, 22096, 143525, 96909, 55729, 20281, 64025, 133074, 30241, 79441, 19012, 18415, + 55420, 135097, 144146, 48331, 201746, 72776, 317545, 66575, 3987, 123583, 63087, 50450, 81382, 173, 71558, 23663, + 8259, 7334, 32703, 83813, 49325, 14244, 94476, 64667, 23287, 26116, 23310, 52112, 7719, 44701, 18954, 2509, + 24633, 12226, 87574, 88717, 251100, 14491, 4274, 63011, 92311, 24796, 10214, 29179, 18591, 35862, 3752, 140316, + 110533, 16614, 111729, 140400, 64759, 105653, 88014, 99594, 170260, 100542, 4164, 21095, 393860, 1570, 11733, 2292, + 7175, 53291, 7019, 12084, 21144, 53309, 55975, 40606, 132887, 54500, 21593, 18556, 3593, 69, 28773, 8032, + 75346, 68243, 36013, 38871, 101277, 105666, 51660, 13332, 91109, 139335, 73372, 29329, 16387, 148050, 129561, 74071, + 259187, 18882, 26921, 62561, 11627, 44409, 155193, 111945, 57459, 200411, 28094, 21626, 173829, 42067, 142855, 22469, + 44694, 41492, 188424, 1317, 55780, 178410, 31665, 32476, 49797, 50096, 133424, 54045, 89192, 278490, 20313, 169877, + 120443, 901, 6058, 24102, 62622, 149284, 65967, 57346, 3904, 11937, 41372, 17994, 14814, 66911, 235601, 80170, + 23887, 4818, 145255, 128004, 1027, 25782, 724, 207408, 112258, 39194, 29870, 74011, 64955, 219934, 119689, 118077, + 99800, 207128, 83883, 66658, 11132, 62891, 83085, 25216, 66353, 9815, 11198, 16652, 36202, 13812, 128388, 227653, + 48624, 40080, 23190, 29953, 18158, 102077, 62237, 3854, 88481, 9533, 51501, 68427, 96882, 27608, 35856, 58154, + 43059, 186515, 64641, 36774, 11040, 20138, 41376, 37623, 2455, 47929, 25769, 20233, 7077, 19150, 180387, 26425, + 21651, 71540, 74198, 49630, 181159, 39337, 47603, 10453, 99126, 357301, 22413, 127961, 100434, 22332, 3856, 103557, + 61388, 149292, 137919, 5062, 68186, 8654, 8282, 23829, 19161, 9891, 41737, 115440, 110468, 4212, 22815, 35452, + 120052, 148515, 40823, 61325, 8819, 4354, 51789, 12976, 55324, 24368, 77300, 8725, 41494, 7736, 73140, 57163, + 7014, 42056, 13817, 48686, 37689, 9578, 13571, 104584, 3618, 3981, 2615, 22449, 87729, 68084, 20425, 146810, + 92615, 53907, 63016, 17337, 207943, 48593, 10105, 121091, 9823, 47400, 75309, 56784, 128657, 106634, 43561, 70981, + 12587, 51472, 92150, 54178, 15857, 121204, 6055, 42142, 6379, 144024, 8486, 145642, 67628, 94025, 8294, 117913, + 51216, 150314, 153347, 1042, 71775, 2265, 3224, 68227, 157617, 10548, 144881, 33324, 63244, 38654, 28271, 8525, + 1503, 958, 5778, 53723, 3864, 58302, 39847, 53240, 218754, 39512, 6285, 19507, 110864, 63755, 37263, 11172, + 5769, 8998, 16662, 20146, 21492, 50772, 13346, 118643, 7243, 7129, 118273, 175534, 1192, 22122, 76882, 43114, + 87921, 153196, 45106, 138465, 85529, 3075, 30600, 13487, 31512, 11916, 22411, 68282, 18537, 64660, 19916, 149911, + 42698, 4415, 22126, 52393, 35350, 288666, 27537, 97849, 73510, 52941, 15929, 294, 84474, 45720, 39658, 4675, + 34301, 453, 57231, 29931, 8991, 192137, 112683, 33353, 21013, 82429, 32028, 9768, 35034, 2225, 27949, 96606, + 566, 33483, 30858, 67296, 38832, 15545, 58085, 23327, 19838, 45987, 38289, 25368, 35586, 107553, 62141, 38448, + 37216, 107939, 32602, 87203, 39413, 653, 178536, 85622, 55006, 18371, 38085, 25177, 12178, 39794, 54315, 242178, + 59742, 149144, 10766, 127632, 856, 10442, 37459, 12213, 84563, 16802, 59641, 26529, 6900, 63962, 37381, 10736, + 25632, 95267, 29099, 16636, 17437, 23052, 153874, 11429, 49786, 45113, 58163, 65817, 30723, 19440, 8273, 176150, + 13847, 87151, 4967, 13744, 70845, 25959, 93062, 22114, 4044, 73295, 41922, 20005, 7843, 56037, 104704, 125857, + 23944, 178063, 113180, 671, 235976, 152556, 97222, 136235, 55530, 17906, 85124, 11074, 48942, 10651, 199, 27527, + 6518, 91832, 29772, 61127, 20939, 1458, 24121, 44597, 73769, 4219, 49564, 15715, 192653, 166296, 109313, 43836, + 52936, 33537, 1148, 25047, 95207, 270590, 56882, 97809, 23895, 31177, 26762, 3184, 132542, 49663, 59748, 3068, + 142982, 88469, 25870, 43552, 22632, 51664, 23936, 32111, 87452, 107086, 104873, 11521, 48042, 93260, 56685, 20765, + 54018, 59944, 11341, 144539, 178468, 24105, 32314, 35295, 10728, 11236, 29477, 35284, 26230, 226, 165790, 42461, + 23559, 117007, 118411, 290079, 363, 78533, 53121, 31307, 81269, 1905, 16159, 14155, 142012, 33360, 14799, 52790, + 5718, 161126, 136815, 27111, 346258, 23627, 343, 945, 31456, 9607, 277463, 25540, 84333, 314287, 2083, 110065, + 246476, 16799, 92713, 55883, 51018, 19760, 17707, 24992, 66692, 259273, 23704, 9218, 101804, 59562, 16967, 86612, + 120570, 10762, 23969, 43849, 39962, 166523, 272460, 8995, 30373, 16246, 141683, 23812, 70593, 83230, 21943, 323413, + 2864, 2355, 100471, 19429, 60541, 82374, 42741, 35603, 19425, 290629, 43248, 33804, 54209, 7864, 21420, 71753, + 104092, 2325, 5902, 24070, 7201, 62951, 120053, 14462, 31545, 99914, 13564, 113310, 48018, 103024, 129706, 25209, + 25865, 23609, 5945, 17372, 15442, 45351, 49273, 3849, 46257, 44296, 17650, 6, 40443, 52796, 50158, 89987, + 8328, 144410, 81530, 54477, 6451, 36594, 86466, 8893, 111782, 198927, 159705, 4360, 47527, 36893, 117496, 37320, + 97754, 50697, 10204, 162372, 33046, 29631, 95221, 7160, 470, 118627, 17716, 97731, 245116, 237362, 49912, 10078, + 31095, 22842, 98784, 60332, 39, 19235, 8973, 120765, 91934, 111045, 58972, 128887, 87208, 67602, 46022, 29104, + 73470, 5539, 281467, 182667, 36708, 176270, 59432, 31026, 36521, 1009, 33426, 52275, 12801, 73675, 38019, 38883, + 70624, 9854, 44370, 2164, 36272, 3412, 81053, 50116, 8892, 86510, 11513, 257297, 79768, 14274, 40531, 33581, + 12427, 106809, 39327, 15343, 55454, 3699, 118114, 30358, 103756, 3575, 184159, 130664, 5908, 82458, 120759, 58923, + 52390, 273629, 62193, 75485, 62962, 34707, 223853, 87062, 22720, 170728, 85085, 23937, 141138, 3062, 19956, 33198, + 94634, 43295, 14025, 57761, 41689, 20778, 135679, 19997, 128402, 22478, 95668, 1172, 126390, 47383, 9006, 96799, + 6628, 94152, 175749, 12095, 106394, 149169, 112174, 23774, 35527, 95733, 7587, 79649, 134394, 37602, 25349, 151895, + 82727, 41687, 20559, 85846, 254651, 19160, 594, 147220, 194502, 40954, 17422, 66352, 148064, 13518, 9302, 36919, + 89549, 90619, 13212, 61499, 3202, 41426, 127530, 57213, 28359, 266732, 167478, 35161, 957, 34000, 1394, 74891, + 21954, 67533, 109988, 70343, 27906, 20087, 74943, 12177, 49840, 84890, 252007, 38133, 142747, 13270, 9011, 1956, + 8307, 41129, 241047, 11967, 24206, 16598, 65926, 114801, 48978, 39444, 29569, 37783, 1186, 28611, 15878, 63609, + 71728, 79096, 37857, 7905, 133038, 64595, 170871, 4983, 71474, 90251, 41794, 1133, 91306, 2347, 11977, 301891, + 6333, 120429, 31567, 114536, 110959, 92026, 77401, 131107, 74600, 4947, 14258, 12464, 70387, 792, 46742, 49365, + 18983, 55855, 53296, 26567, 62609, 186547, 111481, 9625, 56057, 154515, 28578, 317259, 22970, 130016, 87061, 200872, + 1147, 156105, 94296, 61010, 135850, 23757, 304, 68146, 1321, 101784, 28699, 41754, 28834, 26821, 22763, 34257, + 3578, 58871, 46251, 42855, 59715, 38961, 106435, 68266, 102227, 100604, 165318, 20000, 32827, 79754, 41071, 488644, + 81415, 27823, 114001, 14947, 99952, 20692, 48050, 23304, 55636, 16428, 9523, 58498, 257598, 22601, 34441, 124581, + 390, 131974, 174602, 248497, 22702, 5303, 93016, 7999, 35701, 16482, 87643, 40024, 85872, 13254, 27598, 5591, + 402916, 332853, 161899, 167074, 39216, 53652, 3842, 15164, 189795, 61422, 24281, 156829, 14929, 126509, 20650, 40168, + 19467, 70654, 8316, 5943, 10112, 63120, 121775, 4962, 144422, 66650, 69732, 29954, 61514, 18947, 174426, 19368, + 5279, 6294, 146465, 101384, 63421, 46353, 14826, 52334, 134686, 24672, 129081, 113192, 41436, 46150, 14514, 94104, + 8424, 94187, 87766, 25900, 51925, 128539, 30970, 101548, 10598, 4499, 46545, 283177, 132295, 66488, 16337, 205974, + 124416, 16473, 174653, 64458, 4604, 27366, 56141, 24652, 194739, 122988, 146663, 6230, 210929, 11416, 14833, 244818, + 57866, 80769, 9608, 46573, 31829, 126854, 41657, 44969, 51626, 23726, 29384, 7856, 50007, 53704, 180935, 69829, + 9976, 25004, 73613, 77676, 91878, 46340, 96749, 1491, 61906, 71825, 107515, 42399, 56168, 45795, 84470, 28713, + 81906, 14060, 29478, 12282, 60918, 152243, 2069, 14131, 61859, 62748, 45514, 41973, 40017, 51530, 81012, 35281, + 23059, 3091, 91182, 40986, 16887, 215039, 49110, 48902, 40927, 4664, 22408, 2186, 62064, 38288, 20072, 39739, + 12785, 46679, 137755, 83190, 32893, 3331, 21910, 59337, 32913, 69540, 19401, 3027, 21782, 89291, 9283, 160441, + 93965, 2532, 14763, 2928, 20169, 41863, 117119, 29296, 44387, 196228, 25734, 37480, 79084, 56135, 111008, 23529, + 38463, 12553, 65044, 45442, 11457, 157661, 136804, 196290, 93950, 78976, 90672, 49250, 27127, 21222, 172616, 123647, + 157050, 84415, 40826, 176737, 2697, 48083, 49157, 116229, 337088, 109380, 128213, 191771, 5116, 6808, 21572, 57279, + 54128, 114903, 1829, 16332, 48255, 41551, 56053, 69223, 33279, 27772, 38971, 152878, 8821, 42333, 270100, 30247, + 27352, 31219, 62315, 48036, 25515, 5676, 64424, 6842, 124022, 67108, 75224, 26671, 65710, 5485, 81101, 180131, + 28906, 35401, 41128, 7420, 38557, 103577, 27651, 76992, 33390, 277265, 119976, 16075, 1722, 4016, 8160, 16260, + 104435, 28594, 108521, 42619, 72215, 13679, 41467, 25078, 38551, 17674, 12470, 98147, 12094, 238313, 103012, 94925, + 30978, 17555, 9120, 57009, 25113, 302349, 310035, 87798, 6671, 70215, 168771, 51466, 37355, 11995, 27263, 18145, + 175109, 36992, 46153, 119925, 42862, 41147, 72904, 29988, 98024, 78321, 120886, 98773, 65406, 84549, 46294, 207054, + 221276, 28987, 23494, 87283, 59624, 116795, 91279, 21644, 118012, 30256, 26515, 54289, 64637, 18879, 2366, 48426, + 78760, 113223, 59165, 22607, 86697, 2006, 87700, 5586, 21426, 221804, 43982, 42892, 1639, 18263, 97022, 7861, + 40350, 35831, 74589, 15387, 12584, 16272, 2350, 34840, 67193, 166860, 62, 129059, 84144, 13945, 8872, 16116, + 31396, 12848, 46665, 69587, 7863, 337732, 2021, 215090, 143011, 19774, 108067, 9097, 69629, 62086, 41087, 118595, + 63112, 27350, 127724, 168612, 9454, 94824, 47240, 23536, 28666, 13015, 8075, 83300, 65798, 136675, 46387, 66592, + 46372, 22200, 69177, 100328, 134721, 10046, 40569, 11636, 6314, 82208, 7888, 75928, 51402, 4086, 55946, 3983, + 30837, 6098, 23497, 57256, 151922, 183027, 50022, 109927, 2847, 71736, 48147, 21198, 38676, 17208, 38494, 26901, + 38008, 40243, 23664, 42890, 92823, 28712, 10052, 19450, 136245, 1296, 26037, 115524, 17540, 81959, 87998, 109947, + 5504, 135131, 70356, 151584, 109295, 23619, 23856, 16295, 205242, 19398, 32059, 269066, 37606, 78075, 65249, 115008, + 5211, 52062, 83105, 34423, 214892, 56403, 19696, 105923, 1586, 3982, 120399, 20581, 20670, 68532, 88135, 29281, + 208711, 27405, 153585, 40, 137743, 9722, 8280, 31119, 19328, 72088, 60804, 21766, 40652, 52930, 36086, 14049, + 16995, 33305, 97171, 70003, 20023, 20591, 134059, 31794, 14657, 45601, 41289, 83347, 154919, 205038, 18514, 16315, + 34422, 97287, 94105, 23527, 12996, 13917, 114122, 23417, 13918, 46358, 21310, 7972, 38221, 68113, 107783, 16386, + 47690, 56795, 8499, 18545, 16398, 691, 133344, 34814, 2959, 20119, 119395, 59372, 37680, 79653, 29760, 7411, + 89122, 12450, 10343, 56156, 6721, 9880, 13905, 42800, 198469, 177097, 106548, 82488, 91876, 24609, 10706, 76004, + 172043, 23144, 14890, 62366, 83898, 12968, 38955, 44234, 101992, 9519, 85215, 71444, 26084, 100199, 129339, 314155, + 94570, 5416, 9451, 23094, 3635, 8260, 19505, 102704, 76958, 35234, 26525, 2551, 22853, 44177, 13568, 15372, + 76497, 42940, 8996, 82568, 38266, 73994, 25359, 100653, 176590, 44214, 60988, 126450, 168403, 88749, 20773, 72958, + 44464, 16442, 108241, 43912, 142840, 28715, 26132, 62091, 79180, 335, 13854, 117482, 184594, 27524, 213171, 29588, + 1984, 151855, 9907, 24196, 41806, 10248, 139515, 110653, 83147, 67906, 50005, 126920, 11985, 7146, 70966, 73392, + 6546, 11571, 36898, 51082, 366068, 14298, 110969, 8480, 59732, 41291, 11948, 60798, 24533, 35525, 46894, 151863, + 271505, 8978, 152334, 240030, 8736, 86988, 55120, 56449, 39084, 131143, 52149, 20472, 222992, 92098, 113905, 160942, + 93429, 46821, 134338, 13359, 28962, 133572, 63437, 17720, 58985, 12668, 50951, 30088, 86665, 12598, 38135, 100049, + 8432, 23582, 12520, 74142, 30028, 37676, 72636, 19758, 14548, 75515, 10553, 14518, 32439, 41532, 19021, 32738, + 54424, 77334, 95984, 30113, 165029, 21959, 45058, 51094, 55175, 7821, 3808, 143697, 27458, 92493, 25546, 44547, + 69046, 4809, 22393, 105573, 121277, 83916, 93708, 65917, 46168, 169086, 41887, 133671, 33732, 21296, 85711, 93384, + 20661, 16850, 126144, 79900, 24581, 92813, 68853, 7556, 135574, 40156, 253, 27866, 123110, 108105, 8863, 6885, + 37556, 40974, 5474, 42318, 79260, 53275, 55822, 124845, 72611, 23898, 88911, 72971, 16957, 97387, 53099, 8161, + 12939, 7235, 19325, 37236, 46162, 6870, 103036, 9217, 58238, 52894, 25497, 87733, 44907, 108667, 102200, 20343, + 72936, 85840, 55962, 34676, 253758, 69668, 96389, 111954, 41324, 167841, 8806, 158362, 32518, 41375, 120632, 73135, + 96480, 21314, 4720, 259790, 11949, 5752, 116141, 21106, 124438, 7908, 114832, 63475, 65280, 49770, 31147, 141323, + 43256, 220098, 15804, 42500, 34107, 104757, 75473, 1964, 53533, 71047, 83715, 42845, 43531, 77188, 2321, 57356, + 37037, 106618, 27233, 20062, 28366, 40192, 37908, 345556, 45970, 11045, 55249, 98674, 32741, 77466, 25566, 44398, + 173438, 69888, 65582, 3754, 6121, 34129, 9737, 95439, 194202, 164322, 2102, 4704, 62969, 40994, 31759, 21994, + 26355, 77991, 29831, 64006, 30314, 111389, 59636, 131909, 58370, 93916, 82377, 99217, 28455, 53729, 81544, 46068, + 1848, 132138, 3234, 103227, 50519, 863, 34429, 22675, 82830, 85513, 87414, 12210, 90393, 3294, 143767, 94310, + 21761, 100363, 279561, 80941, 295490, 75238, 72247, 3049, 10936, 2278, 38484, 33144, 256940, 101817, 33856, 75506, + 133568, 39527, 156016, 262504, 44050, 98168, 25682, 41968, 20269, 88567, 37803, 5755, 4089, 36260, 33454, 69130, + 27457, 129154, 52277, 26140, 21610, 15320, 67142, 111206, 219460, 45685, 70880, 105780, 36743, 123247, 83438, 52208, + 14821, 80935, 29659, 180718, 101388, 45817, 3374, 57612, 52005, 6448, 72882, 184891, 13124, 12704, 30703, 25929, + 30979, 69399, 15380, 74068, 140816, 117172, 5753, 5065, 167362, 3697, 123452, 43307, 26054, 54528, 105177, 21154, + 18458, 72597, 72966, 61322, 60789, 5516, 142428, 84303, 34917, 14578, 112502, 12928, 40447, 8085, 94588, 23066, + 26606, 8666, 27561, 98346, 33422, 14547, 68915, 108761, 9066, 161082, 19796, 27553, 3452, 27494, 15534, 23906, + 83614, 19, 132782, 13705, 5761, 29605, 59087, 120796, 20263, 12290, 12851, 50370, 137238, 39286, 94500, 20667, + 25038, 21628, 33990, 8981, 91310, 200127, 63967, 88268, 101, 286303, 114772, 71059, 52322, 129656, 47566, 12562, + 74548, 64285, 49207, 24655, 42572, 28607, 237181, 77595, 38084, 16719, 839, 81694, 214054, 17721, 4580, 98901, + 186568, 121462, 138040, 42832, 31802, 161998, 197148, 18980, 17665, 152652, 24115, 15160, 30620, 99343, 9889, 87020, + 141936, 7344, 42231, 98797, 37730, 129285, 120827, 160915, 14420, 2535, 155000, 34611, 4265, 102563, 57689, 91604, + 187218, 17667, 1262, 39897, 49640, 200660, 37670, 19608, 188208, 32890, 127419, 68124, 51441, 81359, 123584, 58473, + 55388, 45140, 15680, 85159, 96452, 16120, 30294, 94559, 66659, 44469, 59032, 36146, 40869, 33455, 63569, 7922, + 42039, 12261, 115220, 18301, 60967, 146149, 29288, 13403, 221027, 9100, 52523, 139159, 19234, 40843, 206228, 37834, + 178581, 17027, 40896, 45534, 29105, 2544, 91128, 79269, 96050, 47912, 285426, 27282, 9165, 151, 46166, 7045, + 196118, 46867, 66112, 143251, 157335, 55563, 96710, 8207, 3738, 72403, 61089, 157979, 40354, 156883, 40650, 178616, + 117436, 102312, 20179, 41865, 2965, 21966, 28441, 168958, 34136, 38297, 9787, 20997, 59659, 36744, 60366, 114471, + 15570, 25271, 29375, 258002, 50843, 37278, 68479, 43715, 230035, 5024, 2970, 91666, 31444, 117750, 16847, 112904, + 71260, 12363, 55600, 48521, 43886, 46675, 13532, 57721, 18316, 20619, 56356, 4628, 5387, 78044, 11, 33577, + 38500, 104616, 16018, 21088, 168508, 6053, 33669, 9683, 17406, 34961, 60264, 30468, 32174, 24071, 78408, 25436, + 8828, 39141, 39373, 37714, 103373, 109235, 33475, 22349, 143806, 196511, 45872, 33957, 90367, 15839, 11624, 84305, + 3560, 26149, 26619, 50807, 18719, 36474, 43129, 70576, 122310, 12311, 103752, 19102, 16508, 25987, 44388, 63236, + 26719, 131892, 134604, 63602, 2541, 237340, 98802, 45539, 105429, 117394, 38028, 181798, 6645, 86186, 32830, 114213, + 37998, 21792, 4332, 69153, 73190, 173015, 127914, 9801, 10591, 4848, 14686, 23714, 235916, 53231, 142998, 35580, + 49737, 63998, 15639, 52103, 87112, 228066, 40885, 51065, 233941, 23415, 4046, 91775, 53188, 141652, 6728, 119152, + 32193, 121081, 48378, 63940, 79154, 100136, 68457, 56982, 4544, 133512, 181089, 126816, 52905, 78546, 37550, 11149, + 126477, 134049, 124361, 126465, 7580, 1593, 86300, 14758, 63501, 80748, 18813, 163792, 57314, 22986, 6141, 11406, + 3216, 32494, 110332, 28452, 31337, 162136, 21024, 26738, 6541, 7188, 22430, 92369, 103083, 128099, 204997, 51873, + 60846, 69568, 20383, 4773, 49, 48196, 29395, 14748, 9756, 43703, 50109, 163732, 15481, 130284, 88137, 10991, + 1355, 101243, 3715, 54949, 43840, 19376, 118637, 43614, 14319, 50237, 47535, 50675, 13743, 107035, 43658, 76751, + 27486, 7709, 13643, 96042, 10222, 21204, 124418, 113169, 73114, 71157, 117166, 121290, 230718, 203167, 36470, 64894, + 418644, 66714, 116018, 28732, 16706, 52824, 36929, 51413, 6674, 128215, 137726, 19964, 279748, 26329, 87214, 10512, + 23058, 116855, 58012, 13318, 45273, 76517, 33779, 74266, 295831, 1336, 47046, 106101, 86306, 13, 79499, 18377, + 2743, 123188, 92968, 51882, 5535, 6799, 86683, 1562, 88773, 138107, 96667, 11031, 21829, 4831, 30926, 76005, + 35253, 174880, 61764, 219165, 120938, 41317, 25064, 15262, 31153, 216734, 168694, 126329, 3169, 7494, 80595, 54300, + 16839, 27391, 60115, 17288, 42847, 68970, 16373, 61298, 8751, 32529, 67945, 20542, 80974, 71952, 139421, 42317, + 33942, 21817, 38951, 12239, 36867, 67209, 48716, 1479, 35514, 105236, 52386, 100232, 16485, 31451, 46256, 39894, + 26474, 60702, 20094, 10295, 77775, 1748, 117588, 48134, 56877, 51915, 11560, 224153, 5391, 3701, 26137, 58941, + 48346, 80382, 15181, 131665, 3882, 7358, 42702, 14975, 108713, 6343, 80451, 72290, 31403, 4314, 32244, 5000, + 27804, 42280, 29989, 44929, 55248, 6075, 42803, 20470, 26235, 53651, 44765, 109461, 43821, 69384, 24056, 170949, + 136104, 115740, 191734, 36140, 40118, 138988, 7264, 71698, 175507, 12620, 155241, 7468, 28034, 185481, 2642, 23421, + 203908, 130353, 51032, 89780, 18732, 56166, 21485, 47669, 1788, 12936, 94197, 57925, 34030, 152347, 132787, 4308, + 106427, 53840, 90424, 21211, 36958, 111950, 33872, 29233, 54359, 51008, 62593, 42396, 7251, 123282, 198715, 53630, + 44936, 14249, 77826, 258614, 15356, 45209, 51025, 80569, 69139, 10507, 10673, 56339, 455, 43204, 39237, 14263, + 157915, 72576, 100737, 58151, 22173, 51683, 197114, 28812, 140291, 15706, 153760, 89962, 50348, 37718, 108861, 24876, + 43275, 55149, 227847, 109122, 82066, 68840, 83530, 39884, 49621, 18581, 228664, 30516, 54952, 16372, 8186, 29493, + 15216, 129920, 30573, 45435, 36226, 109626, 22456, 7678, 96695, 109013, 34730, 57022, 66855, 100447, 13026, 32965, + 6936, 272617, 48390, 49584, 1987, 61711, 3050, 110021, 8227, 8769, 96593, 27031, 196087, 22068, 19421, 11454, + 4631, 39978, 242671, 88040, 68827, 46976, 54523, 87226, 99004, 37956, 109647, 20830, 47541, 918, 286374, 22872, + 110265, 74675, 4723, 101851, 17953, 216583, 119919, 65609, 147605, 1909, 30855, 55924, 110242, 97095, 129683, 9370, + 58520, 14221, 15934, 60347, 31312, 158125, 25989, 55743, 38724, 14896, 158699, 35678, 329975, 16243, 72941, 112881, + 642, 11436, 38291, 112105, 3155, 67914, 72572, 24476, 34497, 104985, 29314, 84672, 75937, 22802, 6429, 75538, + 6207, 163504, 199842, 23456, 73843, 79289, 235636, 7573, 6120, 124975, 52978, 95704, 17249, 175679, 9569, 104975, + 51429, 329147, 13133, 168135, 117746, 78571, 8080, 58294, 3218, 58849, 10361, 62017, 910, 33143, 555, 31814, + 68961, 145868, 122784, 15453, 319671, 26077, 236592, 74939, 14938, 125306, 95117, 36290, 29555, 75543, 1445, 3405, + 34700, 143718, 17140, 139460, 1649, 866, 29186, 90440, 66673, 82069, 69865, 13592, 72268, 105954, 55710, 1985, + 9655, 105536, 52207, 38806, 76800, 9513, 81900, 12656, 153087, 6755, 43141, 21061, 6941, 16813, 142735, 31152, + 210168, 41188, 100125, 29783, 27130, 37384, 93862, 71525, 35398, 12398, 26982, 303589, 1780, 9910, 34746, 13281, + 73242, 165400, 2032, 112057, 3135, 21087, 114080, 35562, 56689, 79328, 57005, 56383, 19556, 8454, 47363, 3132, + 165307, 54577, 13611, 2516, 8765, 81373, 65366, 57451, 35967, 256887, 71960, 42183, 121458, 34333, 89472, 15018, + 13333, 75535, 131910, 13497, 70453, 25831, 37776, 56546, 17350, 31676, 97161, 28621, 117253, 29257, 16050, 488, + 22265, 48754, 128425, 182002, 13340, 90943, 70744, 33837, 44265, 92131, 45994, 1482, 39869, 27394, 57718, 13956, + 441, 5487, 70995, 55529, 3742, 61617, 118889, 92005, 135025, 86222, 66571, 254704, 23581, 37646, 38231, 4846, + 33309, 92236, 6134, 18657, 16543, 2124, 77619, 45274, 17731, 39418, 106157, 55973, 13291, 9746, 65544, 32616, + 157637, 33791, 108671, 24882, 283005, 65971, 70349, 6146, 21408, 15989, 88659, 365, 101360, 128119, 22909, 2662, + 63887, 10345, 23354, 71146, 52312, 30052, 45711, 99568, 83873, 506963, 2554, 13515, 5338, 289237, 32776, 59272, + 233779, 82419, 26344, 71757, 23759, 15290, 14476, 154914, 186949, 43876, 60677, 44353, 34531, 1802, 77130, 15863, + 177320, 67156, 10629, 89062, 38069, 81953, 13367, 22945, 156465, 63862, 319916, 37995, 91004, 119548, 320016, 44035, + 19353, 17852, 114239, 24818, 59852, 114160, 51645, 12833, 68160, 65930, 36636, 297383, 14891, 2853, 73690, 26169, + 1338, 1151, 23088, 98141, 1072, 29833, 31924, 58527, 29823, 49223, 13440, 94333, 1950, 42392, 24753, 253517, + 28901, 4590, 1846, 35647, 81407, 166926, 16913, 15179, 313445, 157404, 11324, 72420, 73038, 62980, 67242, 98614, + 84807, 1288, 18715, 35345, 162348, 49709, 92384, 35688, 240257, 105106, 97424, 336014, 37162, 31857, 24409, 137966, + 138934, 29778, 20791, 88000, 16111, 4596, 20363, 25650, 58013, 116702, 57820, 5668, 41253, 31169, 251377, 5377, + 102951, 209713, 150400, 82980, 16457, 95615, 28341, 34104, 96056, 14290, 87446, 35563, 19541, 9842, 4673, 34998, + 56402, 37494, 140987, 144287, 67217, 38738, 69684, 90560, 41638, 6371, 67553, 130177, 94381, 18943, 22304, 5556, + 89674, 1540, 104069, 26091, 29481, 79348, 127520, 1738, 37456, 64314, 202862, 31135, 80815, 1319, 24743, 103040, + 151579, 2886, 45671, 10020, 13937, 23292, 25393, 58266, 13683, 1161, 48094, 156120, 132537, 22049, 1682, 76886, + 19699, 10385, 1058, 254528, 134545, 55004, 82397, 41659, 67020, 195747, 38970, 40798, 29816, 202866, 95435, 5414, + 222341, 122682, 143053, 294222, 141235, 18722, 36979, 52903, 427578, 91648, 78453, 23736, 48868, 95317, 62318, 53265, + 129557, 41511, 51444, 17062, 233342, 157429, 4110, 25190, 23077, 32496, 234890, 104393, 87871, 119604, 172405, 31296, + 16213, 41034, 147157, 76, 18728, 337132, 33035, 74049, 16184, 117337, 1430, 50125, 9469, 13116, 96853, 186079, + 37913, 79652, 76574, 12079, 19680, 20090, 74134, 22992, 2798, 111900, 29035, 78700, 171356, 103866, 25861, 76971, + 178328, 160559, 131679, 44747, 13216, 89528, 50885, 79922, 50049, 78775, 61642, 115486, 72690, 15613, 40111, 8974, + 71904, 99272, 76547, 36995, 124644, 58876, 62375, 56306, 55455, 10949, 9333, 20277, 7504, 41873, 102574, 28557, + 29052, 18656, 226780, 29795, 41036, 52032, 84065, 51914, 266546, 6019, 73011, 15118, 19899, 148821, 109409, 68842, + 30391, 102037, 158644, 78906, 188755, 20593, 56915, 26262, 8659, 76359, 39099, 42863, 59469, 24343, 170097, 72940, + 16, 31679, 33449, 44831, 104298, 168570, 329243, 12874, 112943, 10737, 81733, 10145, 53865, 30398, 84862, 90377, + 76203, 66, 37651, 15508, 138226, 36312, 36084, 66979, 68857, 69503, 23486, 27392, 139953, 43251, 74333, 109079, + 14125, 42935, 117495, 77115, 107625, 70706, 2266, 28248, 119795, 6372, 79378, 83196, 173133, 134246, 42289, 4799, + 4398, 34848, 30176, 134351, 50273, 66466, 87051, 132965, 48808, 31554, 48150, 75235, 54390, 30193, 11461, 79397, + 16466, 17661, 7427, 28480, 122086, 37993, 13959, 83801, 31835, 33998, 164771, 60458, 67035, 180999, 5256, 7006, + 50971, 114665, 163017, 23336, 48859, 250788, 4340, 9613, 7508, 81510, 27383, 68480, 46427, 15448, 81334, 97899, + 66477, 71, 139832, 7506, 73021, 97330, 136340, 46842, 84615, 21471, 141895, 32003, 39985, 62480, 13666, 12717, + 83076, 96305, 46422, 172149, 46779, 38567, 66589, 155205, 201569, 190175, 4693, 89306, 53336, 145244, 26386, 78125, + 36443, 60742, 64991, 315, 60865, 22001, 34462, 3145, 168164, 7227, 45365, 52278, 143810, 15529, 219120, 21490, + 51393, 22637, 26823, 15222, 25548, 102002, 40489, 96757, 169307, 3643, 119915, 68728, 32896, 113685, 70503, 18482, + 24485, 209111, 5537, 41079, 38424, 97942, 125499, 59570, 21837, 20492, 31623, 9701, 29087, 11628, 19585, 9000, + 275813, 117347, 75561, 10000, 51674, 25141, 80217, 10734, 6714, 202302, 17083, 85695, 64883, 71665, 13202, 80751, + 46169, 222686, 49498, 67783, 187369, 37577, 13134, 27844, 55186, 70471, 20101, 307605, 76192, 4390, 46641, 16931, + 12852, 7169, 5321, 137488, 12018, 197662, 2595, 12702, 62134, 52236, 43904, 2706, 31067, 311914, 70629, 280345, + 118303, 59493, 9152, 296895, 16542, 4127, 190174, 11204, 12125, 33624, 43704, 629, 10579, 161171, 436098, 110011, + 4928, 20741, 120332, 41283, 26291, 13782, 65933, 147206, 43854, 143015, 24103, 185039, 7091, 135245, 92175, 293076, + 10946, 19925, 19967, 110847, 253716, 42758, 95038, 69599, 109062, 27063, 120815, 57458, 39283, 10218, 39354, 7499, + 17261, 8263, 7839, 189220, 113012, 110601, 48485, 156100, 258512, 41840, 167472, 67791, 47764, 14675, 53087, 6354, + 125126, 12700, 41054, 45096, 32646, 70686, 11736, 1417, 55892, 49536, 45376, 6942, 80279, 12070, 89681, 183322, + 201623, 35389, 58180, 430, 149872, 18459, 444892, 19950, 3192, 82244, 305001, 83495, 385, 1258, 82408, 33652, + 1208, 738, 12995, 21781, 48750, 13634, 68571, 68149, 5376, 30653, 64669, 33991, 58738, 87302, 80018, 88747, + 22335, 35680, 106650, 40779, 5427, 30033, 3552, 51590, 82416, 25102, 25208, 3949, 47811, 74006, 93322, 124119, + 32435, 357395, 49716, 13835, 143086, 4083, 79989, 41030, 38930, 21275, 146867, 20485, 94128, 11151, 10472, 53127, + 59975, 30973, 116792, 75634, 156037, 15565, 112131, 58155, 37977, 33863, 74566, 194491, 38224, 22622, 88291, 51351, + 62485, 19885, 25695, 49858, 7698, 124574, 37501, 200, 50405, 11713, 287549, 195058, 71027, 14971, 39645, 70772, + 16462, 27850, 51933, 19178, 21559, 27321, 3458, 43074, 136153, 7003, 195280, 149565, 34131, 52040, 1210, 6796, + 107506, 11880, 278327, 23579, 162069, 86206, 10271, 126827, 63703, 27398, 13524, 13255, 3101, 29045, 7198, 55423, + 215029, 1232, 15504, 168293, 40407, 14532, 80445, 19258, 4178, 203513, 68565, 70756, 3774, 260344, 5233, 163405, + 9187, 46762, 107090, 26759, 80019, 11197, 524211, 114351, 17880, 91874, 35307, 46472, 97926, 12980, 2932, 75, + 67579, 57528, 43925, 163283, 2600, 68602, 18775, 154886, 18405, 19085, 144161, 117918, 8351, 60026, 40557, 1844, + 47924, 56160, 48862, 13071, 86638, 3171, 163462, 48967, 70820, 50635, 8327, 96197, 92206, 86504, 132, 17742, + 86453, 80271, 35704, 19660, 29610, 70884, 187507, 70566, 42241, 55397, 157816, 116938, 119200, 208499, 318827, 57917, + 3198, 33626, 18608, 33628, 15466, 58518, 23680, 48749, 67813, 203805, 73110, 32434, 57863, 126161, 76577, 74704, + 35454, 272624, 56452, 33611, 4779, 612, 20538, 20813, 99518, 12664, 37685, 51378, 4649, 48965, 52644, 7250, + 104641, 90980, 25121, 20782, 144269, 136467, 25473, 109758, 33730, 23835, 64889, 3994, 38073, 175725, 263011, 73296, + 65864, 7458, 91699, 99785, 6838, 11244, 30971, 22298, 109456, 24378, 14229, 234839, 193298, 16188, 31737, 116657, + 154007, 1122, 41881, 49733, 5623, 164859, 73807, 45069, 45741, 8551, 143581, 9315, 30846, 98697, 126198, 189421, + 182578, 54489, 24321, 45654, 25573, 17216, 24178, 85193, 157224, 15399, 12351, 94329, 1543, 110920, 86691, 20245, + 58575, 21729, 399974, 64597, 138703, 15574, 33184, 95550, 146140, 2393, 2271, 17693, 44971, 124299, 48652, 114592, + 49356, 244271, 56021, 82860, 18275, 26970, 11660, 198792, 59064, 6815, 87808, 78781, 20300, 104409, 662, 71033, + 13122, 35626, 44961, 91041, 11848, 14525, 52226, 42701, 24453, 111637, 27557, 12927, 11973, 27925, 2467, 122935, + 9797, 47887, 24976, 6515, 86843, 117000, 127598, 39829, 2919, 138824, 43874, 110700, 25530, 13248, 168387, 43479, + 49210, 1692, 1259, 64697, 1130, 20465, 27466, 19345, 161220, 120389, 31515, 56190, 76788, 22165, 29616, 5113, + 75373, 17538, 30755, 22978, 85604, 112134, 45015, 68154, 34926, 7355, 114461, 64044, 36014, 70882, 20391, 30584, + 17777, 8803, 13476, 33610, 17255, 352133, 26102, 24765, 51533, 55753, 68095, 21188, 11676, 21823, 21179, 271876, + 92226, 107529, 94889, 47154, 51845, 43801, 5311, 105238, 119859, 268539, 2435, 55644, 21525, 37454, 162919, 79553, + 5936, 143734, 13110, 3235, 18507, 21886, 124645, 8664, 28050, 67683, 58054, 52119, 1140, 3546, 35570, 180315, + 31418, 49700, 27671, 84075, 14857, 30098, 18009, 21868, 34207, 42097, 9293, 74669, 47859, 50876, 49991, 60692, + 10750, 72343, 7644, 83181, 36382, 115481, 14074, 68458, 32079, 110696, 30195, 6157, 106909, 22414, 134401, 11947, + 59426, 71942, 7548, 142461, 87757, 25760, 55425, 47637, 38393, 117046, 33833, 33451, 110042, 21631, 15553, 31475, + 15965, 52160, 30794, 68222, 97104, 44038, 134558, 22658, 33757, 7286, 148203, 73358, 35344, 42812, 2789, 141364, + 97993, 325497, 95230, 62242, 53979, 114390, 187, 3414, 33651, 72017, 42725, 163469, 45407, 53268, 119350, 24322, + 41884, 61527, 104655, 61374, 82515, 10912, 127557, 29939, 173089, 44405, 77727, 37217, 7177, 19015, 73371, 191300, + 58371, 10601, 4287, 145829, 35365, 250779, 11615, 1861, 47543, 67388, 153424, 85556, 51927, 90651, 19359, 9654, + 35587, 131677, 91637, 90460, 10670, 58134, 145964, 112159, 23544, 102870, 17599, 26304, 29306, 17111, 10277, 45092, + 84233, 79517, 44634, 85065, 39976, 55740, 13294, 40340, 76076, 274931, 24696, 94204, 62097, 19765, 27791, 72755, + 9007, 11276, 152590, 52634, 8668, 11381, 87423, 17757, 28119, 349, 237, 60867, 78281, 91158, 140967, 248103, + 120790, 33051, 142673, 247599, 19835, 85755, 184690, 18251, 143020, 164693, 4893, 85858, 54968, 19631, 20889, 110604, + 18670, 132107, 13187, 1827, 64959, 187020, 16093, 2357, 20649, 24949, 120227, 112146, 34469, 22861, 29222, 20839, + 42570, 12164, 72533, 58393, 33001, 67590, 100285, 77190, 136570, 1891, 29881, 176839, 87796, 169800, 46634, 42613, + 120044, 544671, 35573, 33409, 1106, 23688, 8382, 40809, 58700, 21997, 89694, 32633, 63951, 5925, 91071, 83353, + 127623, 193205, 8076, 91094, 12805, 5777, 59517, 20986, 83057, 34629, 28371, 28946, 40212, 16089, 140378, 2115, + 31773, 3807, 48370, 178737, 49850, 322390, 73229, 7228, 7361, 34085, 72856, 162851, 54336, 3090, 10705, 24203, + 347524, 3071, 11926, 15437, 101314, 38218, 37603, 25070, 23751, 18738, 10614, 30446, 19569, 34876, 34037, 143092, + 48791, 17269, 13448, 181374, 29174, 22705, 11280, 8389, 49369, 33246, 4494, 15136, 20467, 189070, 24240, 21646, + 7465, 86521, 109202, 104631, 75842, 73950, 26135, 39426, 38281, 58562, 87792, 10755, 623, 98319, 19283, 178647, + 112457, 28075, 23224, 51865, 60210, 1572, 16872, 3984, 28849, 17199, 19586, 53164, 51003, 578756, 51498, 45446, + 94720, 3831, 11364, 15400, 6426, 42807, 26765, 136732, 90047, 18712, 26660, 98061, 85560, 99889, 37338, 10153, + 43761, 188463, 24546, 9883, 3579, 47095, 149286, 1544, 85105, 109163, 22065, 84228, 34607, 9802, 24403, 6597, + 90410, 107034, 41249, 2151, 118528, 32433, 167290, 143308, 7224, 62473, 32534, 855, 42907, 31366, 15790, 130823, + 111163, 23740, 103312, 73946, 18168, 41718, 10722, 74804, 6960, 77903, 6730, 4836, 161135, 460161, 25329, 3966, + 191298, 108138, 97692, 28539, 5247, 14951, 16072, 148552, 100584, 72497, 44704, 114746, 127552, 2033, 34815, 27555, + 171568, 13044, 57905, 30463, 20121, 12578, 29578, 147967, 91173, 69059, 75171, 15963, 12636, 216233, 12189, 78098, + 54615, 17457, 34910, 14101, 20199, 38879, 33868, 12975, 63730, 19371, 122500, 36320, 98105, 44709, 16796, 8252, + 2396, 7493, 206206, 58138, 40387, 10906, 28152, 8026, 14438, 11987, 27633, 84118, 125012, 155087, 126314, 20627, + 4765, 60466, 170206, 93400, 33235, 15747, 658, 8854, 12865, 30917, 688, 103792, 45299, 136720, 88015, 54331, + 37728, 2913, 65993, 80667, 82098, 15958, 29994, 167188, 77872, 103575, 90590, 244435, 114037, 77901, 91272, 19428, + 59253, 30651, 149287, 11214, 19675, 21663, 134751, 84839, 24838, 61313, 45844, 7512, 398016, 64823, 127529, 3133, + 102561, 20453, 115896, 17344, 11446, 222828, 193, 155155, 17069, 58324, 4480, 25422, 57508, 105295, 23785, 108564, + 178277, 20918, 69131, 161769, 65836, 54488, 201783, 143191, 99941, 18413, 13719, 28184, 26114, 27888, 4392, 129687, + 2585, 3092, 113567, 150793, 271882, 1752, 282, 15224, 136866, 70660, 67393, 235271, 50126, 30236, 5205, 12951, + 11027, 106830, 33950, 26602, 155648, 159630, 116983, 47316, 118367, 77639, 2468, 20768, 14585, 66833, 4411, 197715, + 8910, 308244, 4325, 25115, 123015, 105047, 174692, 661, 335383, 65622, 43950, 89084, 40434, 55523, 40872, 29093, + 41016, 46235, 18304, 57207, 53021, 31025, 145373, 39883, 14439, 64867, 33271, 92303, 87098, 165627, 249075, 23882, + 176860, 43613, 45825, 64126, 201543, 92448, 76394, 85896, 121888, 56679, 6043, 5600, 2358, 43170, 38186, 77345, + 9286, 9851, 24013, 78703, 5739, 81394, 113639, 182825, 22666, 9031, 22509, 9570, 54270, 33648, 34339, 13164, + 37884, 37579, 110690, 71903, 169381, 124661, 154669, 17643, 33984, 69534, 35747, 99083, 93859, 18986, 20872, 30989, + 16124, 4894, 119685, 2601, 89364, 45420, 102352, 14665, 72207, 77064, 26614, 22336, 51639, 228, 56231, 815, + 76366, 85000, 4970, 44952, 99029, 11414, 154634, 81988, 65812, 71056, 307722, 32240, 2198, 67495, 76459, 289714, + 12147, 34660, 56034, 21936, 174891, 33766, 38677, 42238, 194289, 61206, 40811, 81549, 6986, 11184, 50356, 28762, + 30252, 169833, 26033, 37387, 88822, 7300, 54514, 41857, 21284, 89562, 16952, 95611, 11445, 78324, 76361, 17313, + 288337, 35719, 74225, 17706, 160821, 46786, 195486, 98124, 33034, 230403, 46596, 54312, 100869, 187581, 73087, 76045, + 43852, 51201, 111095, 53695, 25761, 171167, 1281, 12511, 52882, 77119, 180240, 70944, 1144, 132888, 99788, 35517, + 103809, 160506, 37582, 28159, 1924, 90499, 9703, 89568, 84458, 91412, 201459, 33796, 86079, 85006, 49619, 62157, + 43411, 14396, 37110, 43017, 13542, 75363, 34855, 3223, 139276, 79591, 32317, 66073, 18141, 8975, 111874, 25536, + 34978, 2876, 88258, 8764, 41298, 4941, 10664, 6849, 7276, 16023, 42365, 44065, 26481, 39848, 38615, 3468, + 173800, 385332, 27782, 6783, 33210, 23625, 31896, 1982, 17951, 11857, 55263, 92496, 142652, 13696, 62877, 77106, + 33616, 34409, 3165, 42139, 33677, 25816, 8589, 110980, 2210, 30731, 11059, 25363, 19941, 193105, 164524, 15578, + 98568, 36064, 29325, 13286, 2486, 12135, 218797, 3219, 192414, 393107, 34699, 89750, 80136, 7124, 7367, 13443, + 12058, 69118, 234202, 17915, 235883, 20792, 100851, 31528, 50963, 3680, 2664, 124375, 249638, 8483, 257047, 41605, + 29572, 29737, 139767, 51651, 27221, 6765, 55803, 23145, 47034, 40480, 52532, 73864, 6124, 42229, 93325, 34530, + 72107, 238280, 199709, 3744, 63346, 16597, 66408, 22715, 97620, 5271, 1410, 22445, 158513, 169512, 31624, 107883, + 299699, 50048, 63128, 87490, 40388, 185087, 19754, 75917, 23235, 138863, 325617, 37883, 37176, 65115, 41352, 25967, + 224244, 118096, 25013, 205505, 198386, 69311, 49810, 112803, 121323, 27224, 31934, 41103, 67992, 90172, 18343, 182947, + 23827, 233481, 44894, 9617, 63170, 38593, 111112, 18189, 17838, 11885, 38329, 7604, 106622, 67890, 139944, 6251, + 158590, 31160, 39376, 75979, 26807, 59454, 75828, 12609, 5345, 62668, 13410, 6377, 23489, 15227, 50336, 23847, + 91891, 46989, 219110, 5016, 55474, 182, 169668, 41243, 74834, 37258, 81806, 25477, 37981, 32374, 29946, 8558, + 13058, 27278, 55639, 110342, 5977, 7496, 7827, 224669, 72552, 8581, 18359, 28445, 34706, 45938, 138729, 19479, + 26828, 4897, 199990, 7309, 145172, 26292, 10057, 2903, 19904, 3127, 7625, 8343, 21367, 5265, 8513, 8299, + 34043, 7029, 6384, 111718, 960, 4780, 109654, 50272, 77092, 23412, 109010, 40059, 91381, 138810, 25275, 30422, + 4733, 94279, 5863, 2603, 47446, 7973, 33416, 25502, 7680, 106096, 17414, 15137, 41697, 38583, 90939, 13115, + 5170, 1287, 11657, 96186, 16960, 66479, 61042, 54454, 14741, 104736, 18646, 28260, 46101, 248526, 78951, 52606, + 13656, 58251, 8482, 69402, 473, 134516, 4405, 18865, 51842, 100181, 26348, 80528, 37433, 55053, 30045, 136822, + 11103, 22444, 11841, 2990, 11551, 36343, 57239, 17946, 121951, 165051, 7702, 15912, 13191, 61072, 26908, 5979, + 97536, 32603, 54072, 112162, 165932, 27730, 13979, 91093, 50397, 48878, 44400, 29260, 51628, 17193, 15977, 23879, + 129028, 208297, 58084, 29487, 9069, 58477, 73687, 7734, 44885, 223955, 46203, 40661, 6590, 253832, 62105, 27627, + 59195, 37610, 112, 160041, 47045, 121276, 9957, 89691, 32940, 13845, 859, 21447, 225472, 109616, 5172, 115309, + 90345, 174021, 7312, 26518, 21833, 129351, 285466, 54661, 13303, 119359, 7473, 179961, 29407, 61141, 37403, 357673, + 96615, 35776, 100714, 58390, 141951, 44340, 133721, 168376, 5198, 37474, 20461, 28860, 6028, 3028, 13118, 40061, + 18395, 65200, 55843, 156099, 7181, 326625, 72811, 24544, 3861, 106507, 15886, 80513, 14966, 54808, 143914, 131660, + 156358, 72569, 331, 115499, 167182, 181285, 3231, 35925, 36529, 34503, 18991, 46621, 55253, 10258, 55965, 813, + 25942, 89419, 48957, 177707, 173153, 46642, 4811, 91950, 30959, 57953, 55844, 6837, 27261, 33866, 171253, 83769, + 50691, 10414, 5492, 45302, 150176, 127189, 25506, 98266, 162201, 46921, 47463, 25896, 38467, 46851, 18084, 3144, + 48462, 72055, 57402, 19107, 80602, 38235, 64308, 11648, 42163, 101559, 80727, 54159, 118482, 153426, 60818, 128542, + 168, 55184, 5394, 2574, 108756, 27110, 245250, 38029, 26011, 2085, 2189, 19738, 17166, 17187, 129874, 85131, + 54149, 86936, 135307, 122042, 456538, 40725, 3718, 195077, 22512, 53925, 52733, 40639, 91374, 71487, 56427, 22962, + 13816, 20316, 44904, 29393, 90358, 36347, 18997, 57794, 131615, 11502, 90717, 6758, 18132, 32540, 226257, 10712, + 226707, 53602, 99511, 19231, 1824, 19111, 49236, 5491, 28139, 42348, 17387, 13741, 26860, 32136, 143030, 11826, + 42253, 125128, 48221, 24174, 93877, 39491, 28952, 24227, 77351, 64398, 63400, 162461, 65575, 4012, 37187, 20132, + 8980, 42178, 52118, 64518, 80574, 106352, 75873, 68981, 22020, 35576, 63767, 70957, 27948, 62633, 6166, 497, + 40422, 23112, 21321, 14642, 91324, 90681, 29471, 53428, 76376, 17160, 5165, 158982, 13528, 21170, 4421, 11861, + 39281, 97681, 28741, 5107, 91685, 14451, 28300, 33929, 82215, 202223, 39186, 1108, 122541, 3164, 84493, 54892, + 144066, 56213, 6189, 105740, 1983, 53506, 28897, 52102, 193851, 154542, 51373, 38315, 17283, 44071, 149080, 48489, + 26320, 80807, 30857, 143431, 2739, 197396, 39482, 10242, 194978, 39273, 69728, 108587, 4790, 80763, 38090, 13241, + 26845, 225930, 45466, 7671, 42627, 235691, 55444, 50456, 61300, 2137, 111458, 41994, 65815, 20573, 171738, 111385, + 174612, 46292, 37295, 150555, 55133, 45791, 85658, 132663, 4200, 13863, 247261, 33106, 191130, 68764, 69933, 342026, + 79771, 57623, 102440, 82923, 158321, 74104, 66775, 232997, 52280, 5348, 14740, 63482, 166796, 21974, 61836, 39710, + 221620, 16509, 20155, 122691, 62461, 15494, 286059, 74491, 11278, 173634, 24814, 36352, 4067, 124651, 6219, 20384, + 88152, 106522, 11199, 27155, 83409, 59291, 62619, 7943, 31717, 82823, 35872, 25490, 121367, 16822, 5527, 43809, + 13522, 275353, 25968, 13784, 47325, 66250, 55180, 23370, 37945, 34951, 32887, 154415, 10406, 26787, 7574, 51785, + 174348, 5257, 63098, 12141, 249321, 18164, 175374, 159625, 154101, 6386, 36436, 10514, 64912, 129913, 42505, 64489, + 29938, 34866, 92162, 115463, 51775, 138015, 32129, 31108, 17220, 19470, 60959, 82863, 15776, 2068, 11894, 44229, + 166138, 59776, 2329, 138779, 78890, 11618, 39616, 3684, 84425, 73187, 5203, 51002, 54121, 48875, 276201, 108655, + 42861, 116287, 106861, 140810, 16368, 27367, 102464, 4845, 24572, 65525, 25498, 65011, 291647, 3490, 34570, 87715, + 10197, 173917, 12769, 8636, 32073, 8577, 38657, 12073, 22651, 98887, 35637, 26878, 11677, 114271, 87008, 92497, + 97509, 14575, 3470, 58305, 26952, 16841, 8381, 10555, 35787, 2648, 41602, 77764, 18424, 35932, 45851, 49096, + 41910, 7650, 71685, 129774, 71614, 52658, 36248, 19880, 94977, 39129, 145464, 57624, 72318, 30245, 113156, 32799, + 41594, 8407, 15488, 66070, 70024, 38697, 26127, 49773, 275419, 9728, 21901, 111141, 37702, 136166, 21682, 76474, + 60199, 7085, 79133, 215800, 7335, 50628, 141287, 17217, 39107, 44612, 205482, 35296, 61315, 75127, 44962, 2175, + 18271, 83503, 115273, 114695, 18394, 122374, 164929, 11745, 33768, 52043, 39554, 3954, 87884, 6547, 14314, 26459, + 104277, 94471, 129578, 91248, 123724, 20555, 12338, 148214, 7277, 42970, 32692, 38110, 56288, 19752, 90889, 130277, + 71981, 95103, 10470, 106893, 189803, 47422, 67706, 38984, 49320, 67270, 32034, 67179, 3352, 105490, 2902, 57799, + 6798, 57302, 88662, 2520, 14240, 632, 64114, 111171, 8954, 67696, 178121, 64478, 69220, 98726, 78181, 52577, + 94433, 48703, 92812, 106819, 57372, 970, 11507, 56315, 28620, 13927, 5879, 50384, 68863, 811, 54518, 38111, + 193727, 4518, 82041, 45997, 85575, 141392, 39464, 38164, 42309, 34939, 27631, 115200, 41667, 5852, 85451, 45254, + 67689, 36959, 69349, 25516, 42081, 284, 1617, 24389, 22543, 92428, 55862, 39478, 44824, 158788, 112673, 24864, + 12719, 95525, 421417, 153017, 28540, 12854, 40525, 3447, 114236, 119912, 41795, 7482, 101553, 14084, 90262, 98146, + 27638, 309738, 63986, 26332, 27296, 73457, 26543, 61153, 4300, 19919, 75492, 157204, 5353, 16531, 61956, 47675, + 4663, 113612, 136374, 222705, 19379, 3505, 93057, 31, 94098, 199552, 229445, 75586, 3758, 9803, 54043, 51022, + 95888, 418251, 47815, 8325, 95144, 54354, 55865, 238684, 80344, 14773, 42431, 26078, 87320, 4173, 49174, 59477, + 28447, 53727, 59450, 37425, 259518, 260604, 13221, 59388, 12718, 19200, 54560, 211, 71391, 111794, 43082, 14317, + 152731, 24043, 16563, 55318, 37063, 33985, 12107, 8451, 24132, 3287, 51633, 24662, 31911, 94583, 27566, 47306, + 104896, 123698, 17450, 4892, 15672, 1239, 135524, 82674, 103782, 128381, 195863, 42040, 1521, 88669, 5368, 61959, + 4945, 14280, 54416, 134709, 72541, 71947, 141565, 31806, 23717, 13486, 49292, 28755, 122632, 37972, 227115, 71973, + 15619, 45930, 73185, 19728, 87175, 41028, 113786, 71313, 206120, 15801, 80915, 37045, 29428, 213276, 42087, 78562, + 189780, 69074, 397153, 114057, 61416, 106834, 67699, 184163, 28350, 15478, 41280, 87632, 44457, 50713, 90885, 28916, + 972, 63102, 58749, 38921, 1175, 182790, 133419, 33965, 47233, 11089, 17346, 24241, 198738, 99658, 3632, 15062, + 95789, 46049, 55098, 80139, 41907, 66419, 62949, 77436, 21953, 25574, 115070, 31261, 97034, 86959, 15541, 120250, + 59341, 34977, 37912, 95547, 22864, 57455, 27137, 114631, 53713, 28129, 16277, 219371, 16873, 48501, 25135, 20596, + 32971, 2044, 70095, 43252, 20693, 70672, 5134, 139706, 20954, 18793, 5240, 51062, 31336, 1055, 9964, 20812, + 21477, 94661, 40609, 21902, 16169, 19574, 74742, 44447, 38370, 72501, 159022, 27749, 16412, 12007, 11867, 64559, + 9019, 60758, 6521, 41890, 3841, 1011, 208127, 23460, 24599, 115489, 30488, 57116, 21938, 126419, 279459, 210650, + 17085, 29349, 117824, 4642, 6484, 24363, 70018, 30366, 81198, 51053, 57403, 18554, 76413, 87591, 130889, 12473, + 5849, 12616, 44081, 17726, 72514, 20574, 39804, 77427, 12320, 153366, 63071, 43010, 65247, 12837, 49822, 119883, + 276175, 48298, 17891, 55934, 37234, 15426, 536, 214834, 59796, 107143, 73492, 82284, 52642, 23860, 59584, 109240, + 16312, 295305, 2881, 141523, 57349, 24996, 10169, 27023, 198507, 100921, 101928, 19612, 94148, 193262, 51722, 22594, + 46134, 59320, 233123, 23163, 18958, 48350, 10418, 11573, 125552, 158579, 54776, 71219, 1747, 9488, 45024, 123446, + 18725, 52331, 24040, 29879, 151873, 17176, 22311, 178292, 14901, 31482, 26423, 45056, 5490, 10022, 15757, 97024, + 68287, 99243, 207125, 128979, 29470, 1325, 74812, 32791, 3689, 45845, 118509, 34820, 64794, 70223, 8344, 91384, + 40814, 104345, 56330, 22095, 26018, 85129, 77063, 49913, 25692, 80443, 48676, 207462, 54450, 117644, 131820, 12098, + 2703, 16863, 18276, 60530, 88278, 81796, 11213, 17129, 124886, 4875, 8932, 23106, 173087, 7396, 71377, 23220, + 174000, 24872, 76210, 196270, 24159, 83016, 95481, 92620, 179477, 142594, 74941, 14268, 24276, 115069, 15141, 25430, + 46004, 119419, 64735, 171433, 201876, 166502, 13507, 2133, 209202, 8831, 250649, 58555, 445, 79606, 10547, 18957, + 52876, 93525, 47741, 109879, 31948, 69285, 97122, 68070, 30206, 36316, 27294, 147592, 157610, 357846, 4949, 3838, + 39180, 165668, 28395, 105564, 18439, 113339, 26143, 6254, 44124, 41027, 149595, 57880, 50469, 74956, 105797, 64751, + 5774, 62996, 55064, 12300, 96278, 74378, 41632, 28378, 222758, 215455, 14905, 29733, 200216, 83974, 14267, 197651, + 50290, 108173, 83523, 72906, 45486, 17894, 248112, 6668, 20435, 12354, 69859, 105672, 46986, 26269, 26119, 21735, + 46276, 81332, 161990, 24229, 140133, 80736, 85948, 28342, 142326, 114859, 5246, 12288, 15569, 321372, 83346, 67317, + 13363, 11347, 62559, 87384, 47522, 66304, 51125, 158071, 92583, 215430, 30981, 130176, 2182, 17025, 35860, 41627, + 7135, 192109, 213, 29142, 16853, 130975, 2389, 127400, 22998, 131988, 9785, 68168, 30272, 21382, 58736, 6997, + 4952, 39834, 32713, 104019, 63263, 581, 147846, 14035, 35623, 7875, 177579, 12052, 39096, 112656, 33118, 37277, + 53789, 60622, 157938, 185910, 44864, 30132, 308910, 81836, 20053, 20029, 111, 252367, 110392, 9585, 162293, 4213, + 124213, 140484, 19392, 33595, 4630, 45380, 23884, 137937, 16087, 21464, 32146, 130095, 28221, 147475, 40847, 37757, + 127787, 95424, 105555, 146520, 25839, 9169, 5255, 99477, 77481, 245575, 97240, 7618, 44693, 52011, 5049, 29327, + 13464, 195851, 8615, 52596, 113146, 3124, 234482, 38343, 6983, 249017, 62799, 87690, 27069, 6892, 7757, 568, + 55717, 67952, 55524, 29469, 50102, 116514, 63808, 119487, 4760, 11374, 79868, 17622, 7107, 13396, 118343, 202733, + 26186, 94968, 133457, 113546, 66507, 11011, 141426, 116015, 59145, 7451, 3054, 4656, 36032, 68955, 55309, 29753, + 104182, 23389, 82478, 44486, 71328, 86912, 16831, 60480, 29425, 22716, 53199, 42308, 64317, 88346, 22804, 101981, + 50781, 6916, 20926, 87069, 47465, 22345, 6416, 67964, 94298, 12161, 198305, 25527, 69706, 1141, 24861, 18820, + 74899, 101908, 136290, 36246, 22754, 43947, 149419, 77020, 120756, 58182, 76675, 53183, 25108, 141513, 334998, 81890, + 93077, 30790, 76148, 97326, 56834, 21494, 3126, 13675, 73286, 10835, 21018, 39793, 39928, 69833, 40373, 1638, + 16218, 27262, 46999, 35926, 41699, 14586, 109707, 10621, 176763, 65754, 4781, 40629, 7555, 38881, 34586, 20380, + 70819, 99768, 116580, 11114, 50083, 71750, 38765, 26763, 26895, 31093, 26106, 99244, 23315, 195234, 103007, 80697, + 26014, 69431, 24523, 14850, 16773, 129449, 83866, 113767, 123079, 183143, 1343, 35751, 41712, 7818, 21857, 75865, + 5719, 13588, 11322, 41995, 31516, 21912, 16746, 20696, 90427, 100022, 97349, 50603, 158540, 42138, 33822, 20310, + 85051, 198477, 100819, 31299, 183128, 37925, 83454, 48059, 40864, 109756, 117963, 246050, 27505, 125055, 6202, 12888, + 55392, 82049, 6852, 20486, 9058, 55998, 15942, 21876, 45224, 30137, 11302, 33518, 96857, 5033, 17578, 243172, + 30901, 1136, 98132, 67204, 136622, 53361, 185908, 164211, 96557, 1199, 46191, 6810, 56304, 16854, 41481, 31638, + 120061, 167078, 70451, 36778, 11501, 72634, 53232, 33096, 151448, 12676, 107140, 3255, 5773, 230373, 199725, 58707, + 89743, 159601, 29117, 51821, 7769, 175079, 179962, 14736, 86069, 12406, 35599, 12585, 2935, 122863, 21218, 92679, + 18471, 74106, 23743, 2268, 41628, 25025, 251009, 101461, 10114, 69681, 874, 844, 33660, 84276, 20996, 3116, + 110170, 3629, 33273, 374091, 49479, 7043, 8134, 1695, 26745, 1439, 1061, 171360, 92846, 117704, 95171, 30559, + 33221, 6627, 172996, 24530, 26731, 509, 15456, 63235, 18795, 30005, 53873, 51891, 87076, 62196, 32574, 96562, + 8550, 98665, 117502, 67674, 2100, 12527, 40235, 66878, 29972, 78874, 26467, 41590, 120289, 181416, 78604, 54157, + 3077, 84697, 134742, 91234, 72490, 15005, 76558, 55084, 33784, 162703, 6048, 46791, 2630, 127835, 19594, 122511, + 208722, 193416, 9502, 8107, 50861, 143793, 44636, 51976, 63483, 12325, 10412, 23264, 79029, 29050, 159857, 149078, + 6419, 154772, 107400, 107603, 39467, 13028, 84919, 63134, 14302, 158425, 87104, 88768, 45286, 22612, 34903, 13577, + 64207, 6221, 59147, 11798, 9686, 121962, 135449, 86848, 67513, 17167, 43511, 68844, 44170, 71147, 44786, 64366, + 1050, 10887, 190612, 21896, 77246, 77296, 70814, 135434, 59266, 18452, 133, 55042, 17055, 1640, 13034, 42496, + 53801, 5748, 52414, 66381, 7150, 144739, 6440, 74993, 11111, 2539, 50363, 23303, 42432, 27028, 66935, 13005, + 4278, 7311, 46716, 3338, 94579, 8115, 26937, 50962, 362117, 30782, 3762, 141892, 36175, 73088, 50180, 37005, + 42902, 253122, 113704, 91922, 41933, 43732, 105477, 3520, 39002, 3843, 42324, 258344, 98489, 29853, 56586, 11607, + 22913, 43149, 12984, 35738, 74161, 6039, 61803, 269, 84773, 58569, 22403, 44259, 57036, 31666, 126796, 12483, + 17556, 38761, 298166, 122446, 162288, 3950, 44945, 1370, 74485, 97973, 26528, 36641, 178760, 75233, 37361, 147382, + 93867, 98504, 161890, 33435, 73635, 18503, 26688, 55952, 128860, 76113, 36649, 15218, 50362, 50874, 136633, 104263, + 261, 187132, 5194, 41473, 67455, 26709, 46683, 61196, 80001, 415, 103032, 77008, 46080, 63776, 21671, 45605, + 35662, 12969, 32724, 41546, 4368, 25676, 78170, 10132, 25247, 21941, 10589, 88199, 19230, 36489, 23652, 71018, + 74393, 15514, 33003, 61628, 22588, 82874, 278, 656, 1822, 7365, 51787, 44718, 27682, 7842, 148545, 22113, + 235324, 53467, 25889, 37986, 13798, 8780, 14653, 79341, 85998, 58114, 38940, 70133, 13194, 10663, 186560, 72895, + 235067, 15731, 34281, 180158, 23514, 60239, 132955, 17621, 71669, 107863, 209492, 4929, 147632, 35364, 73172, 45463, + 23191, 35596, 21865, 59198, 134748, 84141, 128176, 15559, 214683, 7375, 153174, 69569, 105101, 54279, 191537, 11893, + 1518, 28125, 88836, 27303, 25489, 46180, 96736, 5887, 247114, 5137, 287773, 60728, 7380, 108022, 182042, 30064, + 54842, 72963, 28745, 42623, 26922, 16894, 8922, 6003, 3971, 130326, 30795, 15767, 26361, 58938, 27324, 20292, + 20844, 29628, 16534, 159213, 68642, 15346, 219023, 63240, 170517, 8331, 15673, 3213, 77339, 151668, 65928, 33858, + 123255, 106689, 30575, 26185, 8963, 12688, 15792, 24737, 77818, 92544, 7997, 20221, 150998, 55663, 1268, 41573, + 48466, 14085, 128978, 65797, 36806, 28519, 69465, 20974, 2732, 41172, 202748, 116152, 23261, 39001, 2280, 32931, + 11741, 66879, 195696, 31356, 236162, 62810, 25653, 37741, 18243, 31739, 43296, 15723, 126216, 75117, 27208, 74878, + 28690, 17377, 22841, 46221, 50546, 479, 9735, 5075, 16385, 17152, 9080, 33925, 92760, 24705, 35011, 52286, + 197383, 118668, 24200, 32927, 246558, 83210, 49673, 39479, 201295, 11697, 23650, 58791, 88255, 2117, 58010, 136860, + 67588, 5287, 34543, 6591, 71687, 95613, 48832, 64315, 176076, 18307, 105134, 12037, 172653, 140943, 36060, 3370, + 169058, 87901, 2424, 35703, 33906, 68007, 83459, 86267, 63747, 78729, 15829, 39429, 24835, 60607, 1063, 942, + 157621, 15510, 142744, 36875, 43338, 26941, 6283, 201368, 30050, 1294, 14144, 28874, 46152, 163373, 100423, 33959, + 132741, 10200, 30369, 5793, 2770, 40793, 66426, 145294, 51371, 9412, 47667, 53918, 94835, 47111, 93658, 291281, + 6614, 6818, 28373, 98899, 15112, 55868, 85946, 13126, 11749, 15201, 6184, 52292, 56936, 9994, 67564, 15398, + 1250, 16480, 28355, 50093, 19027, 134101, 912, 36390, 399017, 67061, 175796, 31206, 58036, 37028, 36592, 15922, + 100215, 155543, 7324, 4771, 23388, 157277, 186074, 20469, 55815, 15438, 73729, 36924, 308768, 3933, 6366, 20641, + 124152, 60772, 12026, 70045, 94803, 6290, 19858, 1915, 9521, 22497, 33912, 49717, 64186, 47263, 9814, 19866, + 8971, 350258, 314, 10683, 28, 6135, 16425, 48283, 30427, 224788, 96210, 41227, 62163, 9112, 237935, 8329, + 7616, 14660, 20925, 152205, 103838, 6480, 53909, 29003, 35079, 21715, 38510, 2096, 29203, 37569, 47676, 30859, + 131235, 66331, 56052, 67144, 7743, 65717, 38496, 26265, 17389, 72433, 5984, 42527, 10882, 140995, 248537, 4000, + 37420, 43361, 72768, 79706, 61460, 44601, 88348, 120824, 228512, 92578, 101207, 2506, 85363, 72057, 112263, 74889, + 41581, 61184, 59336, 124955, 131077, 388, 24445, 445574, 62822, 10339, 54594, 139384, 119647, 26960, 115230, 377822, + 10130, 53380, 25507, 4582, 54445, 4045, 113722, 79437, 26925, 51571, 10619, 37744, 19968, 21756, 62099, 38841, + 29016, 19474, 28660, 169417, 24446, 77906, 53823, 54729, 74028, 4315, 3444, 12379, 24176, 2062, 118391, 71991, + 61448, 24221, 58190, 114666, 67185, 84137, 1932, 38777, 9254, 63804, 23453, 23502, 8563, 53758, 17591, 83661, + 119129, 33378, 156031, 31341, 9771, 4905, 245, 10643, 99184, 71196, 20709, 250, 37716, 19394, 203310, 82339, + 39514, 27829, 5347, 68674, 10532, 102550, 189900, 41082, 221512, 57643, 21885, 60429, 258753, 28243, 26729, 38284, + 218630, 266776, 74708, 10059, 55980, 59074, 26095, 4002, 23394, 34908, 56295, 38826, 32141, 56657, 44390, 129016, + 61924, 77979, 141893, 16627, 66749, 173128, 78650, 84113, 32411, 36734, 83212, 22287, 3741, 109048, 15156, 33529, + 36475, 217436, 48727, 82121, 26678, 67771, 256285, 2700, 77010, 79442, 5038, 3136, 44946, 56358, 46209, 4267, + 91203, 9096, 96644, 19035, 128749, 10636, 6976, 205036, 116953, 56466, 63959, 18341, 20476, 42517, 7840, 100552, + 49625, 4375, 77579, 19118, 53116, 3012, 35805, 64719, 13735, 124583, 30702, 85109, 102335, 116046, 63278, 101038, + 29376, 131644, 18364, 4281, 51946, 89017, 31230, 164451, 83407, 14320, 34509, 23271, 67892, 72729, 37652, 77746, + 59212, 14913, 6854, 43898, 34685, 72734, 50838, 3371, 21083, 24922, 49503, 29227, 1546, 61493, 17037, 10316, + 112982, 4328, 38907, 93116, 32972, 99365, 223827, 37012, 74397, 3821, 103422, 35362, 1078, 29713, 94154, 55450, + 190545, 68894, 29500, 75558, 16082, 49117, 103414, 107471, 86140, 770, 35589, 44869, 58591, 17981, 10817, 9420, + 89611, 22016, 15994, 34959, 101531, 126914, 193257, 72721, 10061, 73572, 85338, 101867, 105104, 609, 98863, 73482, + 76319, 100600, 207540, 8308, 20035, 8093, 56554, 15585, 17551, 38570, 177750, 85937, 52611, 10767, 28909, 26249, + 169061, 139097, 59137, 254690, 190842, 27037, 47208, 1901, 100780, 278291, 22166, 32105, 23907, 107009, 147748, 23093, + 90413, 43974, 38278, 110542, 115619, 45653, 24331, 51759, 9675, 125197, 28009, 227009, 34710, 181128, 25798, 132667, + 193435, 41954, 44477, 110078, 49443, 28528, 66593, 13781, 129734, 5325, 109119, 17206, 11183, 17837, 41403, 199989, + 258877, 23595, 49436, 2482, 16318, 60636, 117129, 70004, 136182, 100062, 20218, 28137, 126808, 127896, 48962, 38967, + 44635, 13158, 93741, 10921, 27304, 68089, 142263, 18325, 192375, 147811, 36115, 47851, 2599, 12879, 123482, 145544, + 125648, 78600, 106709, 37509, 47051, 31245, 9380, 153218, 12091, 99206, 351089, 1706, 23814, 20083, 2942, 45798, + 721, 22708, 105601, 201509, 58800, 153251, 16149, 130340, 40137, 47023, 45551, 84104, 66726, 85042, 67373, 116656, + 97930, 21507, 18614, 49333, 60877, 118514, 56360, 10125, 74487, 128507, 90887, 17233, 7942, 46505, 12104, 513, + 54326, 57737, 60599, 113700, 9841, 11073, 24431, 42281, 41428, 3734, 51341, 225984, 13762, 7257, 11599, 104571, + 8211, 44012, 104316, 48008, 85383, 17867, 24242, 577, 6950, 151859, 2565, 40033, 99177, 174326, 186646, 2995, + 79806, 4196, 14521, 60729, 201786, 35248, 27115, 28097, 296464, 53923, 41708, 44679, 124087, 83378, 146584, 6497, + 13144, 70640, 20047, 27733, 29741, 53377, 153924, 19142, 41721, 171276, 66163, 88810, 47634, 5092, 38780, 86108, + 55088, 32716, 141186, 15641, 254286, 116055, 26764, 59396, 106408, 75258, 2560, 73860, 17041, 253752, 52211, 39488, + 99064, 95466, 64462, 11423, 12942, 41175, 93052, 29798, 64086, 46186, 33800, 33567, 45233, 66006, 7617, 49299, + 14005, 40955, 150448, 239881, 2612, 82651, 30016, 5178, 55827, 9423, 94272, 251540, 255, 30751, 103573, 11587, + 7984, 28977, 4978, 95968, 13980, 47836, 58308, 50268, 38574, 77347, 20931, 57083, 12776, 22503, 10, 4635, + 46654, 154112, 11869, 151047, 73499, 9650, 31746, 60983, 249951, 39416, 25878, 43811, 2101, 9653, 7416, 8737, + 26676, 92346, 181430, 83072, 25996, 158181, 85015, 37325, 132326, 48445, 2731, 75518, 116415, 209483, 32511, 38210, + 119062, 17333, 2785, 908, 50449, 214116, 161693, 5897, 31033, 187419, 60336, 5447, 23038, 9049, 23426, 57262, + 11589, 1592, 18499, 5286, 179252, 44973, 418, 77691, 20007, 18386, 42112, 52950, 14860, 1598, 187402, 62235, + 129270, 92667, 2326, 100310, 21143, 53140, 34792, 111283, 17796, 65259, 194012, 97011, 144715, 35840, 20371, 15935, + 60106, 189595, 22778, 41157, 70758, 50788, 46106, 29863, 69842, 86840, 30479, 14570, 34674, 67390, 15509, 71299, + 282133, 2275, 35835, 109932, 44014, 100391, 67192, 15948, 16774, 13637, 53829, 16317, 57268, 94004, 20544, 25822, + 38528, 40203, 28555, 97510, 24053, 21113, 6021, 47281, 46373, 2496, 116133, 176010, 201667, 28820, 53091, 166496, + 28327, 26507, 34663, 247773, 471023, 17682, 2427, 24715, 51889, 11389, 166917, 3466, 102667, 63097, 164910, 47310, + 21193, 150917, 3081, 121294, 114909, 56277, 57524, 64525, 84132, 17553, 63486, 76104, 69317, 55368, 502, 4853, + 96723, 70125, 25212, 69051, 67969, 36687, 75249, 1403, 16134, 580, 2956, 41676, 68145, 22459, 93435, 124068, + 15058, 46025, 62695, 17614, 28765, 189125, 1647, 15184, 32035, 23120, 137691, 51605, 2524, 74673, 6620, 207114, + 101089, 36259, 21019, 104217, 98664, 31074, 19082, 94463, 25045, 6564, 91038, 90673, 76571, 79552, 64302, 92382, + 14957, 61083, 144594, 201758, 86040, 109363, 266748, 12661, 118506, 125644, 159814, 57896, 262428, 108888, 87913, 33717, + 154764, 294744, 43549, 58731, 81573, 67852, 24804, 51538, 39681, 122957, 62858, 15248, 283900, 55535, 49196, 35328, + 73287, 114610, 61587, 16985, 127825, 28981, 37479, 9256, 544, 41344, 20620, 91193, 80448, 170849, 59318, 7633, + 52347, 121720, 45439, 11408, 38512, 20264, 4581, 36309, 175971, 26347, 10413, 16235, 15180, 35078, 30388, 152653, + 45467, 29969, 183795, 49439, 33086, 1929, 164867, 88587, 46552, 130665, 18076, 34437, 48894, 15770, 53144, 83762, + 81107, 66843, 19430, 136312, 43213, 23986, 22371, 51721, 36672, 73932, 85044, 11462, 54025, 63006, 70924, 28412, + 76703, 388659, 28510, 37525, 8053, 29403, 351574, 243678, 7608, 105640, 74981, 222745, 13299, 69352, 22764, 32848, + 56619, 140685, 29353, 106, 20752, 4501, 61795, 68153, 238099, 39552, 89245, 17454, 54164, 23662, 42008, 59724, + 105133, 53821, 26404, 115768, 1444, 16209, 287358, 17881, 32942, 78671, 61192, 56974, 953, 17778, 20882, 55194, + 37564, 73360, 211669, 11594, 8000, 109829, 67377, 21481, 66316, 204718, 32898, 37701, 119463, 6868, 32788, 5503, + 106817, 232653, 56662, 123157, 404, 44879, 169840, 19912, 13667, 10522, 13222, 180347, 149108, 31852, 19954, 1455, + 128597, 19388, 66139, 13463, 31267, 28564, 85407, 118622, 10269, 12637, 135119, 151455, 49836, 122605, 44182, 26588, + 106150, 14664, 171949, 1452, 1484, 40891, 43483, 32813, 52330, 160046, 414611, 4668, 76965, 52847, 285294, 29777, + 160486, 19187, 64830, 245534, 171648, 8708, 16151, 96632, 38456, 197248, 3824, 13111, 31263, 5534, 22810, 94095, + 22424, 5060, 6994, 76043, 37738, 54013, 153414, 28274, 66245, 103049, 7220, 15850, 67467, 48469, 60783, 177423, + 143369, 15480, 20191, 1782, 60471, 187319, 90210, 9498, 75610, 1006, 245177, 1892, 20895, 6738, 21020, 52235, + 115528, 104750, 54596, 6369, 86070, 14562, 167100, 84334, 60854, 23828, 51465, 49525, 40796, 89711, 108733, 53141, + 49347, 11699, 22079, 52616, 18989, 60426, 6070, 1322, 15030, 77286, 28845, 5836, 11371, 49753, 49923, 40348, + 37578, 73337, 2788, 68945, 15779, 203365, 40093, 11808, 79867, 81426, 46442, 9689, 10187, 52258, 15730, 33729, + 86462, 49418, 30284, 16818, 46402, 43558, 19285, 95141, 155626, 31136, 296724, 58803, 93200, 46488, 332562, 48870, + 40229, 30569, 5173, 69228, 7090, 28830, 105171, 66711, 57547, 57695, 42695, 76635, 108053, 24676, 92847, 18249, + 99598, 51389, 17912, 84688, 11088, 33411, 178627, 569, 47505, 18773, 121108, 7263, 41218, 129818, 35668, 32165, + 206017, 146881, 10066, 21894, 2173, 12265, 27741, 23761, 20988, 8052, 179620, 44251, 30219, 107113, 49515, 5809, + 22919, 43643, 10121, 20448, 80563, 119663, 169374, 59245, 57566, 90682, 12457, 225388, 42369, 203562, 11662, 128551, + 93141, 84259, 24761, 94597, 41675, 122505, 212284, 48603, 2407, 9599, 7883, 24703, 182519, 107518, 90911, 22385, + 120495, 22791, 32676, 56812, 27154, 24521, 13655, 41800, 16702, 262168, 63509, 14150, 29456, 135382, 45733, 66046, + 14349, 2518, 233250, 50438, 7958, 21556, 8312, 32247, 16688, 7974, 4721, 4342, 117177, 13427, 43940, 123614, + 140375, 20924, 42414, 505, 42467, 36757, 55097, 32118, 261919, 34892, 58385, 134010, 74916, 2566, 138977, 120089, + 153569, 42388, 97409, 75482, 10836, 123, 5341, 33838, 34742, 48578, 76395, 92995, 49526, 37105, 106505, 72144, + 7621, 24215, 152644, 48127, 105997, 73105, 87109, 52037, 12212, 625, 111988, 112734, 2270, 76628, 35699, 44168, + 392377, 67240, 91475, 67254, 7755, 119314, 9723, 6967, 17959, 185692, 25707, 36302, 25086, 109996, 7225, 112068, + 232152, 122120, 101654, 13640, 138791, 16408, 39845, 8399, 33847, 12887, 152461, 34536, 13860, 12517, 180090, 169472, + 35316, 3208, 52910, 286726, 5811, 60049, 6687, 6745, 1344, 108692, 23669, 20503, 71259, 58644, 186034, 23770, + 50452, 17374, 5900, 712, 207539, 154425, 93220, 54448, 92635, 125802, 14285, 77361, 50359, 69288, 133264, 162621, + 5821, 93205, 28457, 129771, 33674, 8402, 51971, 38768, 30255, 195827, 18512, 68308, 2086, 8475, 44179, 212, + 2587, 255482, 11233, 42032, 96264, 234156, 71743, 9619, 17543, 9966, 59340, 53, 42, 51576, 68365, 150251, + 6029, 116729, 63303, 1303, 9580, 56310, 126033, 11299, 43007, 25304, 11348, 2202, 139248, 211176, 10147, 4290, + 82831, 107660, 57933, 177074, 12917, 54254, 36738, 72091, 29607, 42295, 47993, 166376, 25786, 73979, 352922, 17657, + 51467, 73749, 5917, 82140, 42137, 39138, 697, 49880, 85161, 40070, 149172, 172144, 100698, 83192, 48718, 29859, + 31561, 21429, 53401, 29518, 88989, 43651, 46656, 32160, 121990, 32912, 74292, 57977, 278500, 63671, 75205, 23517, + 3602, 60467, 33461, 137178, 109344, 49843, 1353, 103161, 37982, 43271, 19531, 62950, 15279, 34216, 34547, 113009, + 116442, 189404, 140865, 134948, 28936, 38460, 59707, 136053, 30880, 128067, 49530, 48855, 87894, 16331, 15771, 63989, + 58079, 104481, 125524, 14569, 128661, 25492, 365675, 116367, 126731, 94516, 122818, 30710, 67392, 52767, 2196, 47261, + 28051, 49914, 333288, 29945, 146885, 100058, 31013, 158363, 4861, 1817, 42266, 21215, 16216, 4256, 54248, 112813, + 97344, 128078, 30238, 120987, 42827, 6923, 14989, 69805, 147561, 47842, 51853, 2647, 153948, 13103, 39122, 18142, + 22684, 76687, 15882, 92285, 21335, 29519, 3993, 86408, 47685, 39612, 24929, 19453, 1853, 134405, 114177, 25894, + 43349, 26803, 12267, 92165, 15185, 61540, 9990, 69281, 59642, 76734, 309690, 136935, 10229, 92038, 49815, 104501, + 25520, 66774, 32406, 37445, 187921, 81418, 18633, 84262, 108972, 32019, 103853, 41207, 5579, 45804, 210683, 27613, + 98037, 39566, 18876, 154815, 24945, 108917, 31510, 38406, 6697, 20809, 29164, 106328, 19193, 8247, 16805, 3543, + 63734, 213048, 201574, 22433, 137934, 31798, 217223, 2939, 75056, 140267, 99972, 3047, 89740, 22878, 4763, 62402, + 19767, 110374, 49959, 24684, 224268, 106487, 32793, 8178, 56138, 27795, 3080, 77954, 63643, 24857, 121435, 175431, + 151661, 102435, 15023, 177670, 39313, 17174, 24416, 12895, 70618, 46646, 17001, 27902, 84031, 58519, 21749, 50823, + 89723, 59027, 57596, 61596, 84074, 33007, 8029, 24120, 13703, 108284, 63542, 58816, 85626, 83071, 91820, 14146, + 35460, 124390, 61351, 8006, 8867, 11495, 4529, 43870, 64845, 13482, 73015, 24763, 3439, 9485, 79856, 23851, + 57906, 220428, 88667, 80708, 99776, 38036, 39933, 208871, 63968, 30726, 291083, 68, 49270, 106842, 112123, 27384, + 81130, 110097, 118834, 241402, 34356, 13923, 23897, 40492, 16210, 71957, 62441, 58550, 23547, 13636, 20131, 42294, + 36446, 81802, 1100, 142364, 34090, 61710, 9270, 107601, 140028, 39980, 1414, 320109, 72439, 66107, 14862, 134653, + 2221, 1149, 9546, 36018, 22163, 35318, 143604, 19080, 57058, 48579, 2621, 55599, 363492, 110403, 14828, 57857, + 113754, 25759, 29811, 61553, 18913, 107232, 5290, 75792, 95451, 70056, 214553, 3329, 48663, 24095, 11961, 96108, + 54464, 155383, 53360, 112141, 54037, 49177, 57901, 67842, 176097, 123321, 6506, 228274, 68425, 4036, 160696, 23121, + 3023, 30678, 64279, 90792, 34906, 65080, 9259, 58549, 29482, 27140, 216012, 23499, 117389, 49482, 25665, 100543, + 341780, 54232, 60358, 235308, 80431, 37334, 14300, 53910, 58330, 29194, 117489, 59804, 16753, 37401, 37127, 35030, + 92616, 62680, 44495, 8116, 60907, 43835, 168603, 37896, 94846, 842, 40856, 25319, 147486, 395164, 90387, 68791, + 4498, 25599, 15543, 116574, 48646, 254235, 132631, 3917, 7773, 30355, 18277, 60008, 46801, 74243, 4222, 85032, + 7778, 17592, 14912, 22293, 18946, 6094, 46, 29454, 464978, 48886, 97248, 14694, 47558, 169023, 3388, 127473, + 33223, 22400, 144764, 181865, 177444, 13371, 44931, 27593, 7328, 194219, 91202, 3836, 15626, 22427, 52166, 39152, + 63337, 7531, 59378, 193696, 94700, 27634, 40257, 41337, 11743, 257393, 217307, 346548, 9351, 73104, 41502, 1488, + 255024, 105660, 39615, 20814, 39098, 149478, 69081, 19993, 16447, 55270, 37583, 19645, 42647, 14979, 8926, 28968, + 96230, 49277, 22527, 34250, 39769, 81745, 50791, 18698, 58840, 44616, 70138, 6720, 10068, 38140, 5653, 99473, + 63439, 3743, 19237, 163704, 35800, 1626, 33560, 38455, 65843, 158617, 28684, 92983, 58823, 71795, 71233, 1075, + 413844, 42288, 157276, 38514, 9156, 131335, 59762, 40948, 51258, 46584, 9950, 55371, 7434, 2577, 42703, 1693, + 61791, 27603, 63320, 25608, 85018, 30872, 100002, 36167, 6872, 2669, 51250, 778, 3692, 10451, 28383, 163025, + 28096, 44948, 19074, 128798, 7121, 36683, 2203, 17586, 33024, 70070, 348622, 5061, 6009, 23593, 42442, 28013, + 75532, 94062, 64585, 284254, 31997, 89645, 102394, 31393, 192535, 48721, 71088, 128192, 9661, 61738, 34411, 50069, + 3304, 16352, 53075, 45568, 9547, 42732, 1178, 93157, 14753, 88072, 51599, 88701, 31987, 23387, 63847, 44965, + 25314, 47565, 7560, 2438, 55689, 1314, 346, 23289, 15896, 475529, 112925, 131467, 20430, 150168, 2504, 17375, + 39472, 54601, 34817, 12000, 31340, 27414, 5063, 41639, 99744, 6404, 117189, 259172, 25398, 35063, 46527, 96170, + 115569, 8068, 179160, 161042, 54883, 97999, 36646, 8523, 28719, 11447, 6735, 26129, 205423, 83805, 44478, 94354, + 23071, 9474, 27662, 132536, 57855, 155315, 195915, 61922, 64638, 69412, 89700, 153852, 149867, 22483, 25631, 4401, + 25671, 191634, 58296, 7593, 82403, 23703, 17554, 61290, 37616, 211689, 4980, 2922, 20668, 148622, 109058, 2724, + 39989, 54579, 389750, 94744, 77996, 131928, 41416, 77516, 74948, 105981, 7862, 49124, 140555, 58696, 4033, 57560, + 175248, 201147, 43956, 80013, 64810, 82504, 14552, 11127, 36515, 10704, 23006, 45490, 46595, 111926, 16970, 31954, + 4958, 113746, 35379, 27153, 248773, 34760, 166030, 69750, 24045, 70012, 121173, 53304, 28728, 9870, 156097, 134089, + 136673, 71920, 25774, 2488, 168704, 5343, 127631, 74486, 20804, 188876, 26283, 102354, 114833, 476, 53497, 38795, + 100325, 26879, 18226, 1066, 27135, 41772, 14104, 58513, 21205, 5221, 84659, 49948, 96151, 18525, 149506, 51579, + 153134, 107909, 85993, 35590, 45992, 15182, 68394, 22750, 7093, 6602, 26954, 2528, 13992, 8645, 3748, 38754, + 76047, 16039, 28854, 52143, 1980, 22387, 6152, 255879, 19432, 56677, 64082, 99361, 145001, 56506, 42169, 13125, + 75159, 24500, 41901, 21053, 87462, 109469, 103771, 55888, 17710, 31989, 233429, 5318, 1013, 119131, 13220, 94790, + 45556, 27216, 5013, 108338, 34297, 51598, 16968, 224489, 144882, 29596, 70103, 32634, 20648, 23171, 115640, 2381, + 26061, 129018, 59090, 67066, 11319, 1052, 66080, 134106, 129567, 36464, 198632, 6394, 108555, 342064, 340, 57976, + 18872, 21980, 39272, 117475, 464580, 20395, 93823, 156783, 33386, 22005, 34188, 504700, 22717, 50887, 196433, 44491, + 65948, 106413, 3639, 94733, 167189, 37296, 49229, 1697, 5603, 70017, 72359, 61123, 135042, 93369, 6109, 45001, + 79542, 96019, 54203, 50884, 8801, 68912, 114197, 59072, 202632, 47922, 8431, 242124, 18114, 54405, 129410, 6472, + 91882, 124518, 39386, 91470, 5973, 31594, 93512, 401, 5239, 5661, 24933, 37492, 67315, 15503, 24586, 447, + 4431, 98481, 20358, 144946, 60916, 297453, 66825, 30645, 47819, 105167, 552, 87909, 71693, 40566, 5307, 32293, + 32597, 12315, 4634, 118577, 32606, 74622, 13999, 1446, 18183, 5010, 92389, 27675, 45072, 186756, 72549, 62625, + 80329, 3174, 188490, 17768, 76385, 56061, 44774, 4792, 24749, 6756, 29971, 24565, 51305, 2866, 185714, 7372, + 40314, 131257, 46345, 142745, 156514, 10853, 14992, 9306, 14693, 140671, 18567, 166507, 130345, 6503, 52141, 7521, + 13168, 8694, 14811, 40576, 66214, 114434, 97632, 88033, 18029, 21365, 15834, 397881, 12858, 6804, 73691, 171818, + 34801, 11558, 167427, 172844, 27628, 109803, 44373, 61609, 14544, 8723, 7897, 26839, 10823, 38501, 189122, 32876, + 40522, 18836, 231040, 28016, 40185, 9487, 60378, 40240, 33739, 35931, 69716, 16764, 148694, 148116, 26429, 90031, + 23548, 130862, 153367, 10154, 9923, 25899, 86890, 187712, 61012, 106844, 119164, 108121, 28859, 151900, 43746, 70054, + 17933, 46633, 32051, 40306, 19442, 73866, 51802, 202389, 34364, 59031, 39109, 86049, 99849, 27312, 354059, 431, + 164107, 160825, 29370, 26855, 141167, 209995, 47475, 25126, 30629, 112486, 16641, 31932, 21054, 13503, 62291, 8461, + 6744, 25340, 5056, 190589, 36491, 1498, 102273, 136482, 8096, 46702, 98246, 56502, 42474, 9181, 111985, 43767, + 41706, 30774, 3932, 26549, 155060, 66159, 102266, 53051, 30650, 208931, 3598, 31618, 10600, 67535, 135897, 87806, + 163442, 104978, 10409, 139772, 1143, 40979, 7330, 98219, 96655, 131263, 25023, 114039, 61390, 192001, 15973, 35549, + 52359, 902, 12202, 5580, 7559, 52829, 36364, 11107, 51568, 3787, 4394, 31819, 64256, 1505, 29813, 365608, + 203854, 33802, 39839, 47786, 4467, 50956, 226690, 12884, 22453, 47648, 16676, 45252, 14504, 2855, 18627, 541, + 436398, 14538, 2406, 20, 7878, 60282, 10602, 109448, 6980, 70267, 22616, 27176, 8293, 85130, 294480, 30144, + 63610, 187294, 289665, 163077, 293747, 55641, 995, 86282, 16167, 131142, 7732, 139426, 35763, 21669, 81048, 1053, + 19627, 16183, 153848, 41955, 147603, 49219, 127527, 60498, 15419, 62976, 59946, 18598, 18032, 16576, 207, 4670, + 110744, 11552, 9989, 2349, 51346, 15073, 25998, 160678, 33681, 220089, 68035, 65033, 54571, 77929, 12230, 88125, + 40472, 148399, 62247, 44687, 48615, 158618, 103484, 11572, 39073, 41233, 3610, 86331, 21604, 36776, 83989, 518, + 13754, 34617, 179678, 35290, 173027, 43237, 66547, 59016, 92560, 12741, 157332, 29334, 11083, 67849, 24492, 90041, + 47299, 109304, 10326, 20058, 63062, 46195, 31632, 9568, 11813, 949, 131768, 139099, 52007, 9458, 46429, 12293, + 29883, 97116, 3732, 32343, 9734, 20328, 4732, 83588, 139722, 11257, 49471, 2051, 15953, 233007, 15439, 88041, + 1550, 78033, 39910, 56576, 20651, 32790, 66091, 16869, 13616, 226368, 19098, 20124, 49306, 274210, 41089, 39818, + 16113, 202390, 49166, 5280, 90089, 148031, 55043, 2264, 92326, 62595, 168341, 67080, 7584, 39228, 2679, 31454, + 30712, 21771, 49469, 8092, 72424, 14892, 94819, 370101, 164858, 14108, 16628, 34424, 6831, 26672, 13360, 10293, + 152871, 13708, 152221, 56275, 55746, 3003, 189905, 73541, 197721, 19461, 138468, 38166, 34167, 86972, 78519, 126458, + 196442, 22647, 131900, 30322, 6022, 31039, 95120, 35519, 112107, 2704, 104049, 7805, 55215, 99039, 8898, 61822, + 7538, 79147, 8674, 19781, 123381, 122030, 61080, 29510, 4920, 252926, 24948, 29594, 43539, 79504, 36116, 27926, + 77165, 119791, 10396, 47075, 8939, 65089, 91291, 49470, 50392, 130812, 24665, 5396, 34192, 146915, 55, 32388, + 20225, 170176, 24246, 18217, 79762, 97481, 187002, 170504, 22505, 166717, 11581, 22954, 58667, 24092, 24239, 34967, + 40770, 168985, 20697, 10796, 29788, 36609, 33121, 48586, 97180, 70956, 4247, 10919, 82835, 29387, 24795, 134813, + 4568, 41932, 107494, 12409, 8579, 7615, 78083, 27482, 13273, 222151, 109832, 56337, 363569, 100711, 21692, 74289, + 35898, 156666, 112372, 33193, 49983, 165146, 13906, 30221, 436, 23307, 161876, 16834, 36598, 80261, 40181, 489, + 3237, 17307, 33708, 68069, 131691, 47411, 142213, 17996, 62418, 20656, 40859, 30297, 35591, 115572, 96762, 34638, + 8101, 100105, 87872, 93118, 4073, 13106, 53663, 14555, 379438, 12544, 34665, 144134, 65218, 83887, 41458, 1700, + 76072, 7062, 45362, 51519, 33887, 113928, 230002, 145590, 2968, 109731, 69584, 145887, 27573, 34080, 696, 54442, + 212619, 61698, 42014, 1469, 288680, 91524, 69494, 176890, 68278, 36380, 91390, 73061, 72851, 136365, 18061, 126629, + 150504, 108159, 73403, 20532, 217896, 18800, 83394, 3780, 6913, 42351, 72130, 124219, 121339, 338937, 19687, 8446, + 22017, 13873, 48885, 120125, 35340, 27891, 4562, 52291, 51072, 5972, 97159, 14055, 43616, 105781, 67483, 207916, + 75043, 12256, 28487, 7209, 31437, 59474, 13217, 149676, 10833, 46754, 7502, 32640, 81487, 26299, 56642, 3989, + 4364, 2409, 1896, 58704, 22968, 42546, 57069, 47889, 41454, 136134, 46051, 102015, 106687, 15526, 254717, 58, + 85446, 14369, 99446, 71688, 19863, 126847, 291582, 51244, 109625, 70818, 1547, 189380, 149241, 28615, 6289, 179303, + 524, 62440, 6853, 175754, 141850, 162709, 4217, 140213, 214404, 32835, 370939, 250072, 54376, 228761, 71916, 144701, + 657, 89940, 17521, 80160, 237023, 148575, 164257, 272527, 9401, 198903, 24729, 17703, 108137, 43135, 48966, 56162, + 53800, 36151, 13173, 1783, 32474, 18864, 70754, 46888, 49712, 30038, 58553, 64793, 53334, 174049, 42965, 84561, + 126876, 70090, 16520, 63753, 27337, 69921, 58122, 69010, 45552, 33142, 1092, 120910, 177696, 3676, 16059, 23396, + 8269, 22160, 9571, 34657, 15036, 46764, 37354, 25445, 12097, 63888, 48103, 145, 42240, 80858, 105547, 28234, + 2328, 51188, 12063, 12469, 125374, 98182, 171585, 129756, 119295, 23533, 25395, 181401, 99715, 107908, 42579, 37609, + 2500, 59133, 67194, 46635, 19624, 31959, 24153, 277972, 39441, 105587, 56371, 24069, 27220, 18122, 50693, 3846, + 102691, 55065, 140440, 293, 60957, 118436, 1340, 17314, 94543, 71522, 9010, 49481, 39101, 30757, 52442, 3349, + 18566, 55681, 6148, 49861, 67362, 29473, 16424, 51773, 13975, 16105, 153263, 53902, 78230, 197042, 15803, 187130, + 25017, 6214, 105388, 38599, 34017, 9107, 660, 114778, 239007, 212872, 16230, 195154, 90027, 38987, 248, 60897, + 39351, 34856, 31011, 21775, 41681, 1559, 85670, 6103, 35354, 83280, 187563, 5745, 43822, 13397, 20816, 140079, + 1043, 6348, 13019, 188905, 916, 83185, 13921, 197369, 58587, 308353, 44852, 37817, 141983, 32764, 68581, 40892, + 94818, 6526, 46289, 37353, 38799, 65245, 127045, 12280, 75459, 107508, 56307, 93576, 41114, 92631, 22742, 68224, + 67432, 122795, 2131, 30261, 16195, 71686, 80872, 19067, 36606, 55415, 51055, 65943, 59568, 48358, 40947, 230410, + 22272, 116297, 133612, 74166, 126769, 58783, 115647, 39171, 31424, 59980, 6420, 75687, 68659, 22219, 19662, 51609, + 12287, 7887, 94526, 61885, 134302, 46006, 92537, 80123, 257977, 126663, 55154, 71071, 5756, 38621, 29511, 61768, + 207285, 85526, 35878, 1517, 95637, 40711, 214057, 75041, 47248, 72951, 22699, 85378, 117689, 4729, 158936, 22518, + 19583, 25056, 17451, 43230, 77451, 141822, 2028, 7801, 22373, 4034, 75301, 60991, 12200, 59589, 123234, 17449, + 54993, 3264, 16430, 33128, 117118, 56124, 178609, 12642, 34244, 236200, 43665, 19313, 29386, 45091, 42098, 10042, + 34562, 71330, 29635, 50068, 53819, 124237, 44714, 32804, 71267, 130300, 48998, 56578, 64172, 172768, 50075, 17351, + 77665, 85602, 1594, 81728, 49368, 46606, 19775, 75183, 7716, 32889, 26648, 13436, 59301, 29561, 77044, 108652, + 25749, 26512, 343982, 16328, 45426, 53772, 84254, 67097, 194789, 61224, 17035, 160685, 17297, 202215, 135406, 118341, + 2650, 2712, 165122, 39668, 1766, 97847, 41583, 64750, 32501, 260547, 28864, 64103, 45198, 19516, 1158, 166912, + 20403, 34027, 10963, 16141, 20984, 163663, 185362, 27299, 6600, 243594, 45496, 154199, 14171, 53891, 52940, 101642, + 94604, 7963, 104592, 152606, 19037, 11118, 25808, 54515, 5402, 42084, 147184, 18390, 29896, 164225, 162873, 40466, + 9938, 54801, 70146, 66759, 59935, 43540, 58676, 69171, 109708, 38543, 32207, 46591, 88081, 20140, 41767, 101298, + 145182, 39899, 12204, 21085, 44844, 32313, 226062, 13138, 39167, 7649, 21294, 19544, 352626, 42947, 112978, 162137, + 164173, 121993, 17813, 6102, 35374, 5269, 42206, 30800, 45982, 22982, 36251, 17144, 6122, 8671, 8084, 272404, + 154, 122768, 12006, 76527, 73419, 69325, 105807, 9495, 220487, 29197, 89056, 160446, 53834, 197550, 37292, 117751, + 53601, 24091, 108269, 72650, 17992, 118251, 13578, 64227, 8609, 97876, 56750, 36113, 229321, 150223, 85160, 26383, + 5610, 88738, 33839, 35306, 68098, 12374, 121473, 27197, 66815, 63716, 10127, 10388, 71012, 155117, 10660, 38130, + 95069, 200906, 56997, 10546, 140968, 26164, 58789, 80414, 27396, 29337, 17319, 78747, 8957, 43718, 57739, 8704, + 134489, 9251, 14262, 40583, 24656, 39133, 5306, 43837, 86659, 164677, 194782, 27468, 56598, 41406, 95731, 17647, + 134852, 11972, 71605, 77846, 17316, 34195, 24465, 42471, 123838, 4286, 11465, 5223, 255436, 106016, 15363, 133653, + 6613, 57615, 21482, 5929, 41610, 5528, 159163, 20266, 138033, 2783, 48074, 249145, 81452, 57741, 38155, 31191, + 32023, 131830, 8712, 116513, 32396, 160702, 187621, 166002, 123687, 12, 62689, 145928, 63398, 18560, 86346, 150231, + 8693, 5478, 54663, 56869, 29712, 20471, 322015, 164692, 30407, 52016, 160121, 22929, 19296, 52881, 60340, 71650, + 121188, 31059, 10424, 72973, 3551, 30412, 44737, 172383, 36099, 243424, 5274, 49999, 20032, 79415, 43567, 95143, + 111948, 20318, 17729, 101737, 56624, 96891, 161576, 14956, 16547, 135980, 59262, 77152, 27453, 6123, 35571, 43380, + 35916, 62277, 21785, 53693, 15378, 108237, 63, 2276, 52039, 70272, 78694, 41537, 56849, 116796, 14411, 20761, + 13489, 233058, 9422, 23296, 22214, 27805, 167552, 26532, 73177, 43781, 1976, 47479, 53097, 70358, 25233, 10202, + 277349, 32720, 23465, 45782, 2157, 75011, 99414, 46797, 14029, 331188, 26634, 25912, 187886, 51411, 142415, 54672, + 10260, 67364, 68176, 84898, 141743, 32203, 8882, 16414, 246460, 67826, 1065, 38386, 91880, 168610, 5162, 41010, + 50869, 14162, 7962, 335266, 3788, 18011, 86185, 14140, 49486, 66814, 124474, 12893, 133566, 255655, 79151, 46849, + 54950, 40987, 113502, 4653, 33120, 1563, 160382, 117713, 129337, 309186, 18171, 10889, 53768, 44858, 38544, 36763, + 18333, 15858, 58971, 6477, 9525, 8535, 14726, 14096, 26902, 170756, 28405, 233366, 312251, 51708, 14127, 19199, + 10297, 110312, 48460, 646, 9020, 40769, 83604, 51716, 70759, 2649, 59125, 55621, 16647, 2952, 10961, 74126, + 112432, 43916, 267460, 5120, 59260, 28040, 31308, 16545, 84609, 47186, 40537, 205682, 9818, 19650, 93983, 42181, + 82766, 50191, 13339, 114720, 73569, 23501, 5541, 66254, 468, 17966, 5125, 81538, 46001, 88315, 134477, 4042, + 75780, 17161, 37372, 9273, 55028, 52868, 48506, 197660, 52106, 1678, 131509, 88997, 11498, 229161, 99808, 17550, + 43645, 124582, 219145, 8184, 108069, 70061, 175724, 99312, 17150, 2838, 7073, 156152, 17753, 49092, 16803, 1821, + 29417, 92090, 23379, 66219, 16705, 25405, 141529, 27280, 31799, 6767, 12496, 46640, 9606, 10300, 33865, 90498, + 289, 141972, 28645, 1755, 122254, 36574, 145200, 57778, 115975, 15433, 1941, 4099, 8620, 50560, 123303, 55676, + 6133, 5443, 25678, 28512, 255357, 14348, 122676, 93720, 56908, 9978, 32758, 60073, 14456, 30325, 74179, 182377, + 133464, 124701, 18020, 32177, 43554, 808, 19883, 16600, 79224, 7238, 18109, 28556, 11247, 50684, 94823, 7729, + 29630, 27895, 43494, 66615, 160, 75616, 204393, 4150, 12756, 120948, 108425, 9998, 25464, 61334, 213823, 15423, + 65960, 63934, 87262, 84230, 350428, 96963, 99319, 27630, 62521, 82558, 7456, 70035, 321796, 22677, 117013, 180582, + 100359, 79812, 34557, 287830, 67358, 14176, 80683, 114848, 35169, 90997, 1447, 22600, 46172, 146596, 10923, 103084, + 113128, 53346, 226456, 59683, 48988, 21632, 90741, 80771, 88868, 89090, 59673, 44207, 31094, 81602, 72782, 32997, + 33266, 124468, 127301, 33848, 6847, 2940, 167663, 1154, 60887, 4791, 68165, 51588, 98188, 27452, 53523, 3630, + 49659, 31844, 716, 23618, 69117, 101601, 4697, 29366, 92977, 133129, 100459, 35256, 220228, 220740, 11194, 50122, + 13947, 1305, 2379, 119210, 80181, 112061, 18955, 53969, 35103, 28242, 18281, 26482, 62170, 23125, 22627, 17903, + 97351, 70139, 14931, 69751, 13475, 194213, 6823, 66651, 2440, 3123, 124201, 127058, 199768, 273513, 29218, 168746, + 19498, 30628, 254726, 18151, 36597, 16458, 114447, 3813, 46971, 184066, 132731, 85793, 25234, 113561, 20977, 87033, + 67806, 81570, 82077, 83128, 62881, 16590, 59929, 31721, 84717, 54839, 152353, 27946, 73648, 1152, 51494, 25166, + 181966, 18536, 35859, 21096, 10488, 5434, 87296, 116782, 94149, 20100, 42748, 119284, 21550, 80954, 161142, 3281, + 26655, 56068, 31234, 68973, 63436, 197146, 77802, 53836, 48375, 31390, 138097, 215755, 14405, 14690, 48482, 192674, + 165650, 4356, 6779, 90318, 9621, 53563, 21892, 11380, 24439, 27988, 65408, 32100, 28043, 30121, 124, 52304, + 42735, 36882, 47875, 40915, 4490, 1857, 64523, 63890, 29963, 3265, 24732, 6558, 56674, 255187, 78937, 55716, + 45373, 202097, 105143, 40496, 1934, 50343, 10400, 93193, 262446, 123174, 33291, 88639, 50855, 19733, 11387, 78609, + 67098, 33565, 79076, 71724, 26898, 68956, 47175, 78105, 5261, 194162, 6861, 11334, 52696, 3195, 1099, 854, + 40644, 42446, 51986, 165826, 33900, 14512, 8567, 107082, 9440, 96468, 48368, 15017, 180286, 38407, 11266, 27073, + 87162, 25059, 1767, 90124, 22940, 50038, 4456, 79274, 19704, 269589, 3740, 24611, 26936, 118228, 122759, 44861, + 69769, 8268, 21928, 1448, 10254, 25662, 37572, 15808, 101759, 47818, 56338, 32066, 27406, 61598, 102489, 68037, + 12243, 45731, 6222, 13525, 48000, 97528, 22882, 28821, 73926, 12033, 35515, 19990, 113215, 45359, 13095, 69110, + 54935, 144153, 32952, 39972, 5726, 20322, 27148, 119607, 192787, 10814, 127655, 29129, 4312, 11899, 293735, 47721, + 106216, 47945, 13663, 4293, 9366, 4600, 36217, 51600, 11550, 30486, 35147, 4378, 52949, 225366, 876, 56535, + 23457, 10620, 14352, 63024, 212271, 53386, 55283, 2154, 277152, 6832, 58247, 34965, 133895, 60302, 8020, 17598, + 108374, 41827, 77422, 41356, 6191, 78382, 44389, 79737, 96477, 57997, 36253, 168231, 29980, 58643, 13506, 77777, + 218916, 163459, 37836, 70135, 58024, 40795, 89998, 95793, 54696, 46896, 3850, 14959, 40853, 50010, 53886, 103929, + 91124, 21842, 109259, 112031, 65894, 24294, 11400, 75618, 91170, 52085, 77528, 106068, 65908, 36186, 196059, 70011, + 252552, 674, 93814, 79169, 6793, 31343, 87518, 50063, 29212, 56507, 62602, 24490, 15389, 130371, 20806, 17839, + 44516, 4956, 102925, 118742, 122515, 17602, 47643, 17175, 52617, 34827, 384, 128737, 35058, 16456, 4055, 91444, + 9017, 27903, 32324, 74054, 103536, 349949, 23135, 91177, 39510, 20237, 139249, 107742, 49136, 161940, 10176, 4296, + 19242, 19236, 38664, 13941, 130652, 63883, 181786, 74033, 662077, 40517, 51656, 4092, 74699, 174254, 30240, 249851, + 47024, 124719, 88983, 17979, 31422, 88107, 12752, 18046, 8517, 112048, 15131, 61643, 73351, 4553, 10608, 181387, + 24399, 17507, 26238, 34094, 13867, 45419, 28560, 23320, 128360, 95692, 140246, 250559, 4810, 17968, 25372, 235183, + 4434, 11316, 6759, 113457, 61779, 50021, 20556, 133305, 111983, 259709, 231509, 141441, 61036, 58891, 28950, 14898, + 17798, 35773, 7261, 450465, 110240, 66004, 161650, 164984, 59722, 17874, 41866, 39325, 102960, 36234, 10606, 25254, + 39688, 16397, 879, 188946, 10001, 46267, 109745, 88992, 23803, 12899, 109186, 223568, 23039, 16254, 20592, 126376, + 176498, 68200, 93812, 5609, 56659, 71490, 16814, 75820, 44814, 26002, 31909, 11613, 134295, 51635, 17304, 5479, + 17188, 72639, 166564, 60617, 77577, 9173, 51736, 125261, 74466, 141449, 33396, 52135, 226175, 206041, 16540, 2241, + 102472, 15065, 11417, 44369, 154333, 39439, 21371, 35696, 63900, 86098, 215585, 10637, 111747, 26520, 35829, 5072, + 18062, 38762, 86113, 33683, 41171, 51676, 206735, 11386, 79669, 104994, 174586, 84969, 32773, 6701, 65682, 16472, + 408933, 62302, 88447, 143840, 42562, 29889, 168822, 199833, 28931, 31217, 94805, 6702, 30907, 53329, 73464, 80367, + 107388, 92999, 83741, 56375, 43487, 94239, 54863, 13740, 2946, 15038, 117251, 65511, 240310, 36372, 2795, 110090, + 23938, 154352, 180646, 13562, 24354, 38003, 14983, 27192, 319, 49724, 68544, 92943, 184983, 39339, 36199, 161825, + 7927, 16738, 7599, 1393, 6488, 53031, 27832, 35812, 1422, 77769, 52152, 9393, 10790, 70529, 103117, 58677, + 68809, 142754, 214789, 212425, 68209, 24340, 33236, 124155, 64775, 336, 120720, 43770, 4361, 63444, 9512, 52337, + 202, 37869, 58071, 28602, 17123, 124940, 64579, 79394, 59634, 16838, 71347, 33171, 51200, 72048, 194123, 84312, + 44391, 184338, 30592, 49986, 18188, 72135, 53498, 57477, 17843, 74498, 12560, 37524, 2619, 153428, 26875, 24918, + 74278, 49884, 44432, 39983, 3230, 39257, 81646, 26616, 9540, 23710, 69802, 52778, 47187, 280, 20102, 190963, + 21702, 33112, 201384, 189730, 36274, 151103, 62470, 79614, 56894, 160976, 37846, 3819, 43907, 28142, 33980, 44483, + 16310, 43780, 91255, 6410, 34790, 53414, 55594, 62493, 16866, 126630, 78730, 70800, 6150, 2638, 96447, 42805, + 5561, 80903, 142508, 69107, 13587, 90093, 68310, 13770, 107545, 142426, 6310, 11281, 108873, 30379, 19476, 19039, + 126867, 47619, 44321, 1557, 86986, 12174, 285300, 692, 28640, 174731, 39442, 33395, 33427, 183086, 62041, 33967, + 19017, 71946, 141533, 41962, 5762, 27368, 19966, 260045, 80637, 136704, 106076, 25336, 17430, 7907, 59393, 184, + 46903, 143058, 51209, 156531, 2047, 28617, 58028, 3727, 131055, 2181, 190078, 104219, 25958, 39516, 25800, 76861, + 13558, 64738, 31952, 28604, 5444, 142725, 28795, 87891, 47152, 833, 20563, 69475, 13900, 17091, 271888, 185043, + 44563, 4833, 59908, 40623, 122857, 138131, 6213, 136826, 45348, 94359, 56641, 22196, 70863, 57354, 56451, 72278, + 39593, 53647, 46234, 29708, 54332, 63721, 17639, 16420, 38068, 45645, 18971, 54437, 33637, 39722, 36484, 68634, + 318, 5298, 22418, 20417, 40310, 88, 18126, 44073, 143467, 137263, 71354, 82354, 18502, 189426, 11156, 72484, + 24520, 27572, 28397, 1057, 11377, 43227, 61610, 141001, 62013, 3621, 2123, 25838, 28942, 106389, 138280, 139177, + 27246, 10742, 1290, 24912, 28269, 40413, 12701, 122933, 83545, 3131, 5191, 33319, 17999, 208755, 75460, 44990, + 59015, 14954, 33696, 180654, 90707, 63223, 87538, 51, 30065, 70087, 47405, 49098, 15161, 490862, 57902, 10363, + 34720, 124013, 2826, 107593, 1263, 16539, 8297, 53595, 37008, 190173, 24783, 28381, 2012, 14817, 222228, 23569, + 6060, 37405, 4132, 23773, 98575, 114011, 35293, 24317, 92933, 80389, 14038, 96901, 5721, 58820, 71786, 38355, + 299, 2937, 128393, 129071, 199555, 22135, 61163, 3457, 24578, 103336, 75552, 8037, 29223, 24032, 36855, 65087, + 2985, 11252, 15167, 48922, 743, 16251, 113770, 51774, 115825, 202685, 4095, 133501, 109523, 3240, 22784, 51862, + 136657, 17899, 114978, 57429, 47454, 8657, 11392, 32391, 26378, 35272, 1426, 34467, 53586, 83481, 40561, 57729, + 3733, 111799, 328168, 6514, 174945, 20097, 14557, 18636, 93340, 171450, 639, 117760, 244456, 15998, 75359, 111774, + 5693, 73895, 98142, 34182, 37386, 132752, 48186, 121074, 28782, 11866, 26615, 23940, 89767, 129357, 80551, 82029, + 27545, 83711, 126798, 801, 23573, 21400, 128295, 14924, 18798, 114163, 50035, 114816, 136425, 471234, 15959, 173936, + 34320, 17327, 80636, 27686, 84778, 119579, 98823, 73515, 20041, 82828, 124250, 4650, 48453, 64519, 115563, 26853, + 38215, 37801, 92219, 69955, 7477, 145790, 19159, 94085, 71958, 65302, 12375, 44454, 40621, 106911, 19581, 3379, + 8773, 16999, 182583, 5202, 5874, 127304, 16993, 14116, 187927, 3375, 20370, 44171, 105965, 18978, 61953, 17115, + 51100, 102276, 75811, 7602, 43533, 31235, 7956, 72681, 18083, 5986, 190352, 3671, 8443, 19561, 18603, 95186, + 10180, 31524, 10515, 35607, 43597, 12356, 10299, 174108, 2003, 31154, 62144, 6234, 183999, 16214, 205583, 69997, + 69689, 1386, 87561, 18340, 12216, 23427, 2010, 44232, 129696, 140942, 7349, 4623, 146188, 5101, 86380, 150439, + 62389, 21860, 117536, 12248, 34044, 63481, 85500, 98463, 68410, 7339, 87770, 71963, 12765, 3686, 14919, 2974, + 43273, 7350, 39745, 6266, 26949, 192687, 75021, 968, 266807, 27515, 15493, 5904, 3345, 21226, 90343, 14616, + 34477, 13783, 5111, 69002, 79197, 20455, 25812, 125162, 5688, 23290, 86326, 151802, 47539, 53270, 120925, 57870, + 213110, 15305, 23776, 142238, 21634, 69658, 179702, 13601, 22257, 9455, 35397, 86555, 50092, 17185, 21662, 47115, + 32222, 159490, 66608, 20354, 42346, 75706, 11938, 55979, 39530, 138927, 7527, 13431, 63668, 92125, 206545, 83160, + 98, 105744, 113739, 10666, 134978, 88373, 50980, 17237, 74022, 5974, 44855, 31946, 5152, 17761, 22091, 89954, + 59088, 181724, 89377, 71648, 174145, 6081, 202459, 12825, 37220, 45669, 60029, 47529, 9934, 69759, 92928, 1003, + 9545, 40944, 40882, 123191, 118937, 215977, 4632, 152290, 5724, 38351, 20824, 19010, 87240, 135102, 56782, 135053, + 19875, 30902, 38714, 93406, 15784, 18212, 103460, 25829, 40143, 17780, 5626, 20039, 23263, 66779, 128772, 41751, + 87513, 216438, 5230, 73516, 181654, 37997, 80801, 90214, 285152, 76150, 31873, 8348, 37881, 138317, 50195, 1565, + 263241, 15964, 118491, 28092, 4966, 6035, 45147, 26418, 43934, 84355, 16241, 7487, 10433, 247295, 3172, 8129, + 186657, 57, 71773, 143295, 6470, 101381, 39489, 160086, 74416, 43233, 52957, 51944, 225854, 53358, 11933, 29452, + 25908, 40737, 49314, 60112, 142677, 7636, 42896, 27738, 246262, 17093, 14777, 56250, 32280, 129157, 16346, 76797, + 6192, 34415, 425, 120600, 75890, 191879, 176315, 63506, 45546, 161456, 5005, 46773, 143264, 38320, 150132, 134225, + 135305, 182762, 55889, 102851, 29742, 44842, 129661, 64244, 47013, 53257, 4250, 50419, 77787, 123983, 24915, 12948, + 11732, 36176, 80467, 160621, 126658, 56748, 175875, 78143, 8763, 54016, 205303, 6236, 37950, 84876, 66862, 80427, + 21806, 125486, 21484, 35813, 57557, 14539, 213401, 86192, 113464, 36625, 64405, 27231, 89465, 4451, 75847, 20978, + 108995, 205734, 68217, 94454, 164574, 18012, 255036, 16771, 23894, 158505, 7114, 43317, 22996, 11028, 52204, 124949, + 23169, 226500, 10370, 46407, 15369, 14412, 60558, 218161, 23117, 18847, 313212, 60955, 17642, 82698, 38578, 289214, + 130607, 42162, 81718, 82632, 40503, 951, 48442, 14289, 36239, 91499, 48742, 125633, 280990, 7266, 26286, 77911, + 44666, 7534, 217478, 178981, 9981, 2833, 22818, 156155, 40427, 12913, 72539, 44825, 147487, 28272, 67343, 16061, + 26869, 28878, 13104, 26717, 168452, 222284, 63772, 8001, 32886, 55288, 25367, 12083, 32991, 27965, 29014, 23535, + 46798, 8822, 7448, 101081, 240839, 93683, 48095, 16054, 15111, 14427, 104643, 135450, 70502, 37385, 89619, 135605, + 65697, 66256, 31643, 242955, 88548, 21883, 9676, 103291, 44145, 3863, 31735, 8400, 28701, 1387, 89573, 11921, + 48767, 27191, 47327, 74488, 31139, 34928, 58382, 10630, 206777, 28582, 17378, 118639, 35659, 45393, 41374, 26204, + 181164, 243974, 22596, 109998, 166262, 140883, 75323, 38999, 14554, 45944, 89326, 18593, 171445, 14273, 83848, 7094, + 31786, 136223, 135153, 75926, 66523, 5050, 82214, 24940, 76607, 13068, 103875, 30264, 17956, 28575, 70190, 14699, + 6507, 6918, 148803, 40975, 31279, 13140, 17326, 280841, 90476, 164678, 26191, 29026, 116611, 14717, 6030, 73654, + 167918, 94589, 13531, 31467, 6560, 37936, 764, 2646, 1243, 47040, 46211, 49422, 115324, 23197, 48193, 11038, + 80128, 4014, 18828, 39730, 41867, 964, 138962, 14313, 55897, 4976, 27379, 30682, 187323, 81139, 45324, 19782, + 37069, 15003, 3973, 32623, 32596, 5813, 218135, 46814, 189444, 1329, 15593, 67740, 145931, 8233, 95368, 52092, + 13390, 126973, 24773, 78080, 105530, 127257, 27684, 75829, 65709, 23804, 30679, 23341, 26805, 39433, 72773, 79105, + 6999, 9337, 78288, 91647, 55714, 45624, 31732, 25179, 41300, 62926, 8984, 56532, 22915, 82260, 13175, 111014, + 68951, 8391, 237398, 27237, 22138, 159504, 224263, 75273, 21120, 32545, 81951, 75664, 22264, 44392, 981, 6782, + 10058, 4181, 2250, 85033, 19945, 215931, 9376, 41673, 33635, 15417, 217394, 101669, 56123, 23340, 51752, 11920, + 99085, 5011, 143610, 229235, 10032, 59585, 16698, 27704, 5818, 10883, 13785, 186415, 6016, 52857, 9702, 70336, + 46649, 206034, 15092, 14481, 57476, 8081, 27610, 12151, 35264, 32218, 24641, 138702, 94413, 16922, 15037, 25736, + 112522, 11746, 14172, 11310, 262288, 112160, 142819, 50926, 93686, 24209, 43747, 11953, 83038, 1813, 102643, 324202, + 14341, 3919, 29176, 21127, 23204, 81844, 69984, 61119, 28807, 12474, 58355, 40271, 66084, 21889, 11758, 31845, + 77987, 65881, 45978, 68177, 6101, 28932, 58051, 649, 126673, 52123, 157370, 15105, 7133, 62360, 40724, 9837, + 38126, 27864, 30072, 264757, 5923, 6078, 20776, 4896, 122091, 30718, 48046, 119459, 170240, 303310, 26816, 100117, + 97772, 9974, 81454, 42024, 46874, 11564, 45132, 109732, 215746, 2127, 10903, 7713, 43948, 4937, 28852, 25103, + 41622, 38117, 17887, 60135, 3272, 72498, 31571, 43132, 55596, 108898, 45911, 110563, 8332, 37358, 183144, 1744, + 146411, 106155, 85432, 89589, 251315, 29773, 4572, 57991, 13533, 23984, 36596, 74746, 8561, 47865, 143388, 13408, + 81521, 143096, 93820, 10893, 115449, 113660, 48899, 7902, 48616, 6164, 68386, 80304, 175175, 147319, 43500, 47779, + 2063, 16353, 18616, 12432, 186556, 23124, 95665, 69513, 3036, 14556, 14786, 10437, 134537, 36883, 56269, 63535, + 75772, 100719, 86026, 42447, 29728, 3767, 25145, 40239, 82360, 26124, 91863, 12060, 22973, 30854, 96321, 53650, + 186559, 22801, 8489, 72885, 86348, 51954, 28230, 88192, 89100, 269995, 13885, 51315, 38388, 73083, 25625, 53485, + 82297, 39389, 100926, 72363, 45610, 10521, 13154, 68652, 2613, 44579, 170934, 38080, 87082, 32745, 40511, 28882, + 9986, 23752, 68927, 62035, 177812, 181149, 29031, 11611, 57884, 182442, 8046, 104980, 23591, 100153, 104125, 9117, + 47485, 23873, 2671, 349983, 42543, 328134, 85104, 58966, 33582, 332001, 133483, 9354, 44713, 26316, 6446, 63766, + 74439, 40756, 76029, 97107, 257444, 43586, 84500, 59959, 252451, 55620, 150696, 63676, 31825, 65735, 146929, 23371, + 35631, 35977, 145121, 51984, 38540, 33976, 24513, 207079, 33066, 10465, 7127, 153150, 5147, 36952, 154507, 3865, + 13973, 14200, 52272, 11308, 4343, 15766, 13965, 24679, 51830, 184838, 3348, 86524, 70378, 36337, 84987, 49030, + 22827, 32995, 19326, 2046, 26448, 253830, 60248, 12393, 95560, 44044, 28370, 1662, 36896, 50220, 48315, 80320, + 241741, 43652, 242555, 131179, 48067, 39495, 113599, 13797, 203953, 20287, 78696, 3410, 298860, 46405, 39410, 64369, + 61620, 171971, 71030, 204186, 20450, 29322, 37991, 260572, 3220, 386508, 87523, 9404, 67272, 73458, 10375, 45255, + 6586, 2590, 34096, 4160, 107662, 57683, 97396, 79188, 100160, 35851, 78921, 149875, 108684, 200141, 33908, 53318, + 6929, 19857, 56702, 3398, 57226, 58810, 9304, 20429, 4762, 64257, 64571, 51955, 7457, 60202, 39068, 65191, + 1320, 89495, 11353, 17456, 40404, 104230, 19164, 17854, 77204, 58530, 172392, 75503, 99309, 15916, 157308, 83740, + 62750, 50622, 1879, 15474, 208653, 18824, 11343, 41248, 59977, 127748, 31363, 172064, 44000, 65018, 12188, 41891, + 74315, 17651, 19590, 90710, 34332, 9615, 58267, 127126, 5819, 63902, 44975, 20415, 172217, 26030, 99297, 158027, + 64904, 15382, 45953, 118417, 114077, 18724, 56092, 87313, 18147, 79997, 136198, 62361, 84012, 22885, 9665, 4621, + 1791, 3009, 54017, 91348, 98456, 56262, 72712, 106254, 90930, 42901, 80747, 25508, 21446, 133798, 113357, 6097, + 116669, 1181, 110413, 11032, 103938, 49121, 260341, 161282, 7422, 24145, 56140, 35654, 85140, 174230, 9633, 104905, + 59713, 728, 60193, 191876, 5768, 22655, 5145, 41262, 326211, 147566, 80079, 41245, 16239, 59176, 15547, 123829, + 75411, 13376, 315047, 105840, 13229, 35046, 43694, 56413, 29398, 90069, 53794, 84673, 10758, 107725, 5524, 23780, + 236107, 388309, 62023, 165588, 1539, 46003, 176003, 163955, 112472, 361654, 29424, 49364, 95979, 3700, 306600, 117453, + 152154, 17800, 82564, 14444, 151294, 22058, 29517, 47312, 306, 266768, 196797, 94605, 21196, 107639, 225607, 18057, + 38146, 50176, 69453, 50095, 10700, 216046, 17364, 47494, 6891, 29894, 48715, 14004, 84282, 21694, 7598, 82070, + 109646, 6365, 16302, 27108, 56492, 142883, 77880, 27851, 40539, 187868, 189893, 289432, 6589, 19096, 22176, 166724, + 119491, 38469, 38709, 163079, 51354, 26677, 199471, 115939, 30685, 126480, 79686, 66788, 140209, 95841, 256423, 20274, + 136906, 108937, 4472, 99520, 29622, 157862, 29670, 35606, 73617, 56291, 14416, 1391, 49553, 41902, 66050, 23269, + 70525, 139634, 148637, 11479, 51671, 3128, 65679, 40966, 166869, 116434, 159850, 7654, 139616, 20315, 65982, 116183, + 74395, 50212, 88368, 27581, 37439, 11453, 97247, 212239, 49595, 3922, 25404, 51622, 45678, 120847, 23534, 2190, + 11959, 15866, 21030, 7156, 33211, 32273, 16756, 51864, 86560, 62359, 37272, 150553, 52434, 48096, 52877, 35909, + 9282, 150331, 56064, 3339, 62690, 77469, 38848, 312832, 112155, 50347, 133337, 6119, 130810, 19939, 40188, 198954, + 5243, 178898, 39868, 142856, 108261, 286939, 44549, 159984, 99970, 197697, 81046, 134326, 265613, 8809, 13626, 21584, + 72551, 29643, 102979, 213474, 80049, 198207, 20362, 229516, 6391, 82595, 72275, 12563, 33365, 2420, 161399, 254521, + 90721, 10070, 61781, 32490, 66737, 212773, 229338, 7775, 69872, 54551, 80069, 13914, 87011, 91386, 134664, 33101, + 1860, 15322, 69366, 97910, 9032, 31405, 11616, 221, 112544, 23414, 109925, 66229, 60905, 34215, 18312, 31402, + 37371, 77552, 57720, 2026, 89015, 4380, 50369, 20157, 140351, 42001, 57692, 30433, 19076, 51739, 23715, 62058, + 850, 121732, 145992, 46915, 373531, 25804, 8590, 87747, 2802, 16807, 15221, 116280, 36725, 12360, 34724, 117090, + 218795, 142043, 148440, 65614, 72062, 18466, 55923, 22439, 28990, 58866, 64866, 114538, 16550, 89174, 112318, 27549, + 24614, 155152, 5486, 45048, 7815, 58664, 6423, 11415, 6187, 21207, 67086, 238124, 26336, 2489, 21350, 54052, + 33373, 60539, 51387, 100319, 32162, 11584, 95109, 44016, 42791, 31049, 47206, 52852, 73555, 110693, 7535, 38410, + 32062, 15667, 9670, 65566, 23386, 531, 44985, 2760, 10244, 123017, 50775, 39638, 56392, 170971, 54953, 18366, + 49442, 134359, 57768, 10659, 27076, 77194, 62382, 113419, 136262, 150169, 22322, 207134, 12412, 139797, 55514, 2505, + 14883, 65500, 22972, 15267, 1134, 64278, 37799, 235955, 33675, 43711, 22813, 276041, 97153, 48116, 34495, 6178, + 199281, 32510, 95181, 5794, 15608, 76263, 19924, 230629, 100152, 10562, 76444, 119798, 74072, 219457, 36986, 12066, + 47942, 54591, 35202, 23051, 254301, 155103, 68248, 13470, 36451, 42899, 93606, 121040, 16026, 27968, 10851, 17794, + 10687, 100974, 49021, 10866, 65067, 10018, 39088, 10965, 56708, 897, 11410, 7452, 254030, 47692, 32629, 18771, + 30290, 48037, 43471, 14347, 50490, 66808, 37049, 49968, 13864, 83559, 25801, 3591, 57941, 75692, 173303, 61385, + 259331, 1969, 57685, 2094, 35588, 6233, 27697, 16717, 23485, 26772, 4734, 15135, 43486, 85019, 26988, 179071, + 24869, 25026, 9295, 27083, 21620, 11383, 45847, 134822, 92971, 19856, 42005, 31000, 22072, 2896, 21798, 125082, + 88645, 561, 47297, 28868, 1048, 75739, 25425, 197147, 182050, 124782, 126886, 12162, 13343, 152665, 53046, 7557, + 32452, 9893, 110355, 9538, 14825, 62686, 7879, 104424, 19509, 31568, 4996, 5559, 3325, 22164, 66618, 2476, + 216938, 38862, 52182, 79198, 45740, 52776, 32070, 132672, 99716, 19543, 5515, 40777, 189082, 6051, 3103, 146615, + 53740, 256827, 80531, 104166, 78245, 34550, 28933, 112044, 25609, 72638, 36640, 25629, 24311, 56326, 11524, 83163, + 176777, 23393, 82414, 6106, 47340, 19377, 61707, 10698, 308354, 82475, 8066, 15310, 40669, 62347, 33738, 15955, + 66085, 140789, 4852, 37500, 14102, 5845, 9813, 54656, 125339, 67825, 97677, 67735, 9225, 11506, 173536, 159289, + 128709, 12613, 20379, 46259, 97207, 42699, 91068, 45947, 1271, 211146, 104284, 55003, 200933, 14250, 55082, 49995, + 78439, 185897, 62876, 11600, 113451, 32229, 199030, 36486, 88975, 65343, 140167, 135960, 18324, 638, 86929, 96115, + 46521, 34134, 437, 7115, 11819, 80629, 96102, 12424, 18570, 81183, 15089, 30525, 141756, 201210, 66036, 47056, + 72512, 98759, 18003, 68671, 170020, 14775, 7872, 86707, 52754, 279230, 82966, 13276, 63550, 101747, 103537, 30259, + 118515, 110652, 15079, 51435, 103073, 104977, 76964, 5981, 93330, 91388, 21050, 56718, 32736, 2464, 36579, 80299, + 50499, 49852, 67313, 130037, 14722, 2418, 7783, 76521, 31600, 78508, 133834, 49167, 68452, 47680, 2363, 25459, + 398867, 67795, 165159, 68999, 29316, 33111, 23239, 12957, 172786, 66330, 3816, 4414, 18417, 12030, 30134, 7919, + 104924, 9960, 36133, 26144, 2606, 105224, 32252, 42036, 5670, 72687, 493, 78524, 84818, 34715, 26322, 28439, + 16288, 21908, 74255, 9962, 67106, 147542, 139191, 43764, 59580, 72920, 393509, 63136, 82929, 53980, 78657, 4543, + 607401, 11665, 318088, 11366, 291, 7537, 212378, 77254, 85829, 59252, 37336, 13232, 359, 43117, 65592, 71269, + 15897, 112396, 53939, 40125, 35830, 56176, 59326, 11017, 50696, 114234, 276483, 22837, 65630, 17802, 22227, 18232, + 52672, 51170, 100713, 92360, 22115, 91842, 43063, 195957, 356968, 3794, 166425, 56044, 29895, 163395, 11168, 56699, + 40837, 67702, 27339, 20360, 231192, 89936, 103744, 1998, 34024, 32020, 3803, 117654, 38957, 94943, 70290, 85606, + 26722, 43088, 170484, 36210, 406, 282841, 54770, 175134, 23335, 44094, 73528, 47037, 124952, 31360, 23208, 78534, + 72068, 123285, 11398, 40458, 68804, 30009, 6939, 3499, 13268, 40221, 12223, 61566, 147101, 333845, 73905, 2372, + 164740, 293468, 55614, 327574, 276569, 59394, 21940, 154180, 162596, 28918, 37039, 166169, 66943, 84556, 40144, 10616, + 11569, 25337, 104847, 48420, 26654, 76526, 228642, 20116, 66358, 44381, 25600, 2578, 4777, 70479, 5757, 64766, + 23229, 11688, 27998, 24560, 102127, 6006, 130766, 11689, 5848, 24290, 203474, 51926, 978, 76149, 170663, 68953, + 2921, 5461, 117041, 24360, 59666, 1098, 64926, 198078, 5371, 1164, 166512, 13456, 28212, 22987, 95713, 13302, + 90108, 31433, 120078, 63947, 42938, 68482, 38260, 42265, 39320, 109797, 110494, 79743, 2499, 2553, 58577, 180281, + 4271, 259624, 94417, 68375, 108792, 50431, 9717, 29255, 33510, 160264, 7272, 343301, 125072, 154624, 6168, 27338, + 71653, 51148, 140929, 51394, 65239, 109678, 179395, 7761, 38250, 81439, 23490, 79048, 66357, 53948, 107018, 28855, + 38577, 94122, 43589, 44430, 13964, 103761, 2708, 12411, 86251, 119198, 17302, 51623, 35708, 305, 95393, 8798, + 50755, 41461, 203637, 19736, 36010, 8599, 54546, 13603, 29448, 118755, 50260, 10357, 12209, 86678, 39594, 88467, + 3844, 173096, 17788, 39975, 38222, 14809, 54370, 53581, 206337, 67848, 23694, 2309, 100876, 41983, 276960, 18075, + 67827, 14170, 117970, 89349, 137088, 75893, 70548, 20757, 14167, 10804, 5959, 67463, 252225, 44451, 87528, 36335, + 84163, 175996, 66912, 69227, 195270, 25238, 167523, 96366, 1306, 7967, 27706, 52700, 5703, 285, 51677, 60197, + 54198, 170697, 20548, 18244, 779, 4822, 39984, 71212, 46802, 72502, 31290, 74896, 22028, 154697, 58236, 131173, + 51124, 252252, 64234, 48608, 86759, 36236, 13170, 143379, 70560, 101041, 195793, 70671, 113164, 99377, 70248, 34118, + 35685, 116394, 50149, 302730, 162145, 121592, 530, 30881, 45471, 162432, 6235, 49645, 34561, 40287, 58509, 43757, + 422, 70918, 113036, 190344, 2611, 233661, 162936, 32114, 6464, 94933, 54217, 64327, 47486, 871, 90931, 33404, + 19223, 20183, 3928, 34508, 38246, 36359, 11459, 66339, 9191, 90968, 122115, 45027, 18331, 84569, 82055, 106565, + 89942, 52285, 40019, 20438, 243642, 100401, 166242, 127119, 212364, 42312, 34711, 1671, 15893, 23179, 5020, 74061, + 17518, 110465, 11940, 3873, 22617, 123195, 18144, 100726, 6409, 91356, 45936, 73471, 30046, 108852, 212969, 66765, + 126182, 98830, 107226, 23993, 59716, 48049, 45651, 82888, 36560, 16256, 52004, 17296, 104428, 12933, 38645, 135609, + 18846, 26099, 40801, 56830, 26592, 992, 156526, 79480, 19458, 91618, 39463, 7988, 50793, 54675, 156601, 19881, + 147333, 1159, 50024, 77736, 30826, 64647, 13710, 115978, 1388, 51510, 5276, 207487, 27647, 59310, 5123, 271841, + 10922, 2382, 11425, 17267, 14495, 244507, 2126, 492, 33545, 12138, 8818, 184454, 19269, 134769, 8528, 57017, + 135828, 73552, 22221, 65808, 39727, 367870, 203492, 24483, 41601, 196988, 198, 55446, 46931, 68675, 244761, 5411, + 233379, 19207, 36423, 316277, 49169, 745, 204311, 317017, 131130, 150130, 101903, 260111, 182112, 30434, 25375, 59274, + 16276, 109977, 54255, 20999, 82381, 135770, 2885, 31724, 118209, 21645, 119343, 36886, 142445, 81249, 42421, 43503, + 128310, 66260, 92555, 94890, 19672, 1769, 178045, 35419, 28740, 2136, 226543, 24030, 82907, 124857, 54353, 157870, + 33436, 38109, 85642, 96673, 3118, 112407, 1944, 31498, 102206, 135319, 205619, 160787, 28723, 91910, 50034, 79540, + 24819, 28372, 80113, 173951, 41937, 15370, 19059, 55603, 38854, 100638, 70561, 519, 5157, 19218, 16617, 91793, + 3881, 75012, 176191, 145596, 111491, 20452, 154738, 27981, 1142, 2054, 22256, 54130, 9776, 19737, 32399, 69945, + 421673, 103058, 91031, 7281, 152241, 74595, 46116, 86993, 29309, 22846, 33982, 54529, 14961, 41775, 23014, 131668, + 87854, 171036, 94711, 50319, 6054, 72531, 3482, 3581, 15424, 83151, 45387, 66155, 3796, 118067, 32026, 181774, + 82656, 49811, 12569, 44671, 54996, 83240, 157346, 143069, 2108, 19813, 11164, 42601, 55367, 1359, 101577, 27699, + 239450, 9023, 33206, 152235, 154525, 73472, 7296, 55929, 9643, 80206, 87554, 68722, 118103, 89632, 161537, 59640, + 106041, 77231, 63719, 12373, 64601, 98305, 1056, 46674, 68549, 18960, 17748, 19013, 48707, 296146, 134285, 64092, + 30266, 15379, 85084, 87899, 25772, 62788, 25525, 31250, 18740, 80665, 23101, 34025, 9462, 7075, 49746, 39284, + 229669, 57834, 2626, 248569, 91798, 873, 22206, 84442, 112152, 160148, 59240, 6711, 191327, 15256, 141511, 171566, + 14493, 68797, 15010, 17086, 72828, 164513, 36088, 32054, 8175, 11054, 81290, 64307, 66636, 51647, 21137, 68255, + 236474, 72999, 12123, 66901, 25817, 58290, 23813, 41818, 87351, 51685, 349139, 15386, 129027, 92193, 14750, 7028, + 76653, 56861, 59524, 43395, 20422, 123741, 40958, 19478, 22983, 87931, 5921, 15341, 71240, 18213, 18961, 25648, + 27846, 61261, 75568, 216919, 44661, 12442, 49311, 68342, 12399, 74324, 7455, 42754, 46158, 66251, 405, 72411, + 77704, 58295, 15625, 4552, 53101, 50537, 30941, 37141, 35032, 18292, 98289, 17870, 11072, 115848, 60108, 70972, + 17300, 13269, 63524, 140693, 109294, 93883, 56701, 69184, 33638, 4485, 36667, 26721, 24408, 5954, 28290, 80247, + 1895, 82128, 40307, 96015, 11241, 5825, 45230, 255638, 760, 31698, 12512, 26145, 17584, 92444, 8948, 17954, + 82479, 9085, 5850, 120208, 125877, 9751, 11265, 22102, 63150, 153550, 69826, 75885, 141075, 131001, 14419, 128804, + 34259, 129918, 115229, 23808, 23274, 3580, 82265, 18942, 81698, 8545, 39913, 79933, 15732, 6741, 38339, 39271, + 43577, 31006, 30604, 53478, 48340, 102062, 39630, 12695, 91584, 222, 20589, 89230, 14688, 30824, 97582, 47266, + 16379, 99608, 42679, 70464, 24481, 4475, 80121, 49522, 150280, 121584, 178585, 20071, 96420, 5695, 31648, 64033, + 262050, 20662, 107571, 34749, 48635, 192388, 60052, 163993, 43727, 40545, 72642, 99324, 61819, 17935, 20846, 61496, + 56268, 69226, 133071, 52853, 72003, 57628, 110499, 29460, 88178, 40245, 24970, 58958, 17281, 21360, 121825, 31853, + 79912, 81792, 201844, 95444, 13218, 256154, 26236, 61260, 122519, 90685, 37984, 5119, 125295, 126359, 310134, 54407, + 166396, 6520, 28971, 31149, 11811, 266489, 27120, 1794, 2171, 23105, 744, 2814, 118930, 46693, 140092, 4993, + 67746, 27308, 66270, 97039, 17636, 6061, 69135, 4202, 178278, 7472, 32642, 40673, 174656, 26758, 204108, 44815, + 95661, 95589, 192828, 73663, 173039, 77882, 43232, 71654, 83845, 55846, 26313, 21216, 79689, 31469, 85659, 11793, + 17473, 17000, 64471, 78858, 98555, 104223, 20905, 121028, 127696, 15679, 22246, 93167, 203415, 40670, 1525, 47197, + 54730, 29955, 27650, 142614, 22925, 38365, 107626, 61283, 232239, 25514, 194946, 12768, 9309, 63949, 114873, 57567, + 12136, 30868, 3548, 537341, 175026, 133711, 27455, 27667, 20740, 32351, 1997, 26211, 180188, 35259, 10358, 54362, + 10747, 42370, 12304, 6425, 39816, 22704, 99010, 215128, 314017, 17879, 58536, 20732, 266131, 43327, 1650, 27592, + 10040, 89403, 28410, 125002, 175732, 21475, 13832, 98954, 112550, 155503, 53781, 62057, 220651, 63490, 218647, 26496, + 31974, 28320, 13557, 72935, 37393, 40244, 102949, 25746, 888, 15552, 12165, 23782, 23008, 37306, 182690, 178294, + 86799, 19876, 69717, 10583, 4303, 116880, 7218, 92683, 64905, 100026, 340736, 142052, 148467, 8925, 2702, 63925, + 75337, 81983, 220124, 89751, 251, 226035, 14097, 1808, 3284, 142418, 16036, 72819, 370102, 13289, 144922, 3996, + 50264, 199033, 45199, 139880, 9835, 4702, 60405, 74816, 5438, 7368, 27687, 162954, 23655, 159039, 21280, 61851, + 4481, 92865, 109762, 3285, 29851, 3021, 104939, 2905, 329, 63385, 22681, 52094, 12855, 38488, 18381, 19211, + 7162, 61266, 8835, 22825, 64931, 45593, 66502, 25309, 78141, 46199, 59413, 50610, 12804, 59952, 186517, 61018, + 42372, 46728, 18388, 90815, 296771, 59091, 46636, 192289, 83547, 3423, 29852, 2745, 18624, 16583, 357641, 32404, + 34874, 30511, 86377, 868, 86271, 59760, 81404, 39749, 3360, 74207, 15394, 156217, 48665, 41137, 72366, 52831, + 77735, 59042, 22515, 6142, 88767, 22116, 68286, 40920, 11463, 78197, 68958, 24062, 63527, 100286, 139882, 65777, + 28889, 12481, 28953, 8266, 22258, 3319, 99181, 17609, 29140, 179534, 30832, 42841, 194315, 120705, 27548, 161124, + 113924, 42548, 41864, 56260, 25499, 42783, 177062, 105955, 6406, 14311, 23992, 86657, 31334, 225197, 24185, 39921, + 1845, 104026, 301294, 95718, 4802, 8899, 157667, 77564, 49184, 6115, 80340, 47518, 43455, 6339, 54561, 39882, + 35469, 115497, 123233, 68548, 127594, 20262, 97680, 60841, 92970, 5781, 28954, 4558, 61038, 45382, 35089, 49876, + 115005, 15489, 27010, 91676, 38840, 12352, 20606, 19800, 87761, 12264, 9268, 146639, 106838, 47766, 91230, 8234, + 8811, 48534, 107720, 27259, 20572, 34400, 108143, 52933, 55637, 28872, 61739, 77203, 11162, 21038, 66975, 30423, + 96721, 31993, 45541, 7376, 132425, 71889, 178420, 446221, 108925, 260438, 102283, 4056, 2948, 77259, 83943, 38199, + 125457, 36830, 123208, 391, 36356, 138390, 99456, 92051, 3502, 239674, 36201, 114068, 75270, 3160, 39536, 218269, + 27622, 12173, 56780, 8501, 127192, 66434, 47097, 13635, 2561, 98519, 73258, 96646, 123095, 5710, 42788, 66384, + 49394, 12035, 7389, 23253, 61155, 251141, 4195, 439, 16897, 56354, 25580, 66462, 110064, 188570, 17260, 12827, + 9699, 13844, 208611, 7653, 89448, 41275, 5078, 37917, 53356, 45195, 15877, 74097, 19628, 231041, 21225, 15175, + 220310, 3514, 79626, 97496, 21622, 20434, 48926, 95346, 83036, 47481, 10584, 14331, 9885, 4023, 29396, 21139, + 112214, 87100, 83793, 9796, 6087, 423, 60612, 11748, 26713, 29951, 132442, 40260, 17901, 55713, 5620, 88019, + 161912, 177970, 3729, 49808, 91492, 35869, 138357, 40508, 3440, 61216, 56765, 68562, 68594, 2747, 88777, 43463, + 9266, 44125, 1567, 2354, 92238, 29774, 47207, 47789, 8087, 20375, 191924, 3415, 6866, 22316, 82861, 233038, + 150194, 13698, 143688, 29411, 72175, 16465, 14358, 220015, 80701, 53366, 59020, 22661, 13459, 20745, 8739, 76074, + 31836, 46743, 45518, 51271, 43243, 19787, 114669, 18136, 239700, 15692, 105609, 60536, 95846, 27460, 7762, 225232, + 44749, 11206, 14819, 1690, 50647, 170657, 224611, 139596, 21945, 134017, 15972, 174955, 230538, 2804, 25876, 121127, + 120612, 18921, 14091, 435, 132371, 178953, 144326, 158152, 244604, 220898, 21478, 121856, 5193, 4031, 105823, 11008, + 105637, 134379, 253591, 97747, 34661, 247232, 20987, 6949, 41341, 106816, 110210, 45958, 68775, 150399, 11104, 93886, + 85393, 28015, 147749, 112829, 1874, 19994, 21402, 16367, 8771, 33037, 11041, 96701, 33718, 36354, 26705, 23369, + 49672, 29673, 72422, 32419, 77403, 36496, 28454, 23255, 595452, 242129, 61562, 58092, 99507, 41978, 40275, 32822, + 6490, 1688, 175006, 8864, 58895, 13716, 45499, 120546, 128742, 24764, 141091, 121483, 7704, 83412, 14149, 58968, + 39239, 165272, 32855, 72184, 73217, 52628, 13081, 73279, 43816, 9383, 216195, 56823, 62824, 48448, 191659, 3540, + 37804, 223316, 171995, 17606, 199976, 21733, 141024, 23939, 22361, 42786, 77686, 3523, 80005, 1542, 22284, 32365, + 87514, 43833, 4665, 93155, 94832, 32683, 134693, 9494, 14089, 54921, 16128, 131782, 4574, 168587, 76247, 7989, + 139975, 821, 8368, 108503, 59142, 158797, 137, 205170, 75523, 18074, 13682, 91077, 100268, 65492, 54879, 15629, + 43906, 38056, 45569, 40180, 53442, 24989, 20763, 24867, 15152, 30094, 129619, 140074, 2547, 23241, 27435, 7171, + 186002, 4003, 5665, 192737, 17011, 57494, 230276, 241405, 19513, 27773, 95035, 92634, 204282, 5213, 32107, 87507, + 3343, 10550, 3806, 71001, 60568, 10837, 23329, 144168, 128318, 1900, 47551, 4240, 119250, 50444, 64351, 85851, + 4298, 169567, 1401, 13814, 51871, 3524, 75657, 25885, 41336, 136110, 12759, 77034, 71759, 22871, 604, 13904, + 21921, 84968, 84920, 208954, 45074, 13960, 4204, 102255, 98169, 58850, 58448, 58879, 145889, 22357, 8919, 58428, + 99427, 13803, 157733, 68068, 11350, 61811, 360594, 118202, 1237, 824, 163104, 118356, 5520, 769, 31581, 20685, + 28799, 181670, 40637, 38360, 7803, 8532, 69133, 37235, 53702, 86519, 85294, 62552, 21026, 8827, 142049, 30386, + 136352, 11344, 158995, 19682, 38293, 242831, 103750, 55804, 128690, 108982, 27181, 18409, 12158, 167408, 120214, 132169, + 90132, 134213, 7909, 28749, 44600, 10115, 55121, 16581, 10184, 82321, 25270, 21542, 26957, 2707, 106897, 145041, + 39459, 145473, 48977, 26927, 126025, 157588, 249490, 64382, 78904, 11519, 1284, 9871, 82999, 78364, 173378, 109477, + 59373, 50500, 2168, 30838, 39301, 154212, 66143, 91333, 150198, 28707, 45440, 20859, 120529, 33550, 21869, 80014, + 153042, 19905, 153475, 81658, 20177, 158807, 120156, 38566, 50089, 6373, 63762, 19510, 14764, 26971, 108976, 72526, + 271571, 84066, 18309, 66438, 30530, 98093, 65740, 53411, 123161, 23236, 24050, 64130, 38975, 177329, 37078, 133183, + 101562, 89382, 51844, 19732, 22941, 26188, 51520, 22735, 5648, 43118, 130081, 12788, 124654, 200339, 25097, 48211, + 109243, 196680, 216387, 69966, 69817, 55482, 6031, 5293, 71675, 18384, 137078, 73066, 49162, 68808, 11413, 25901, + 106884, 643, 4412, 18355, 21241, 36413, 7382, 16629, 107795, 6893, 5332, 242, 30258, 49533, 74544, 39490, + 16572, 4199, 12724, 122748, 188262, 108611, 126989, 88570, 141456, 72114, 87870, 20276, 7688, 37800, 22712, 59241, + 60718, 170557, 299711, 3515, 8271, 16537, 107094, 81327, 11044, 299399, 71715, 154123, 32440, 16413, 169052, 42581, + 104608, 33812, 5696, 16661, 103419, 161, 39832, 179084, 236109, 71375, 67676, 75508, 93156, 21777, 80970, 58192, + 43293, 31757, 51423, 41531, 128929, 182898, 12880, 113231, 42107, 61632, 45914, 4884, 67180, 4744, 128700, 2781, + 25201, 36266, 194380, 87971, 115254, 341, 41014, 57871, 185488, 92043, 17835, 89050, 130954, 19517, 84683, 21380, + 72813, 45915, 93851, 203411, 167547, 176973, 63085, 59916, 20537, 17002, 36711, 31276, 39969, 36726, 65357, 13243, + 38432, 15644, 94063, 10719, 22582, 47135, 16038, 5381, 184022, 23165, 76012, 35198, 1139, 18638, 45545, 84452, + 27199, 192134, 119684, 123811, 5655, 13706, 141932, 24822, 17767, 37181, 5142, 34476, 97412, 225589, 175180, 68777, + 122606, 11285, 10611, 55686, 209377, 100096, 22340, 26689, 27070, 51760, 149649, 30372, 35871, 50512, 21058, 17439, + 326617, 170142, 107982, 135181, 188954, 85308, 56136, 9593, 42680, 26872, 58659, 5746, 73512, 25617, 2549, 48114, + 80911, 1733, 156604, 26196, 22629, 16115, 47515, 69763, 3011, 81888, 4772, 72580, 95021, 23422, 61841, 69210, + 315242, 20699, 13055, 19951, 157737, 52563, 31431, 59838, 383, 35462, 55449, 68880, 41821, 63984, 213573, 50441, + 41808, 53480, 40494, 130778, 19335, 64598, 138641, 25152, 27950, 8191, 57199, 35528, 15674, 204275, 70906, 3181, + 25677, 26876, 2717, 132658, 110950, 49839, 49173, 20862, 35375, 20135, 50308, 213100, 76835, 103314, 64615, 7399, + 59108, 22329, 92119, 34649, 57370, 20920, 11016, 129444, 35262, 68761, 92220, 17938, 16569, 14039, 59057, 72434, + 160415, 16248, 7148, 40010, 37706, 58080, 149680, 137070, 78086, 105307, 67671, 478, 32041, 27870, 179796, 13035, + 49691, 26716, 81195, 147295, 137143, 13139, 168200, 45495, 9782, 24335, 30927, 557, 172080, 226060, 57625, 14169, + 50148, 53124, 40398, 22321, 77917, 74830, 6334, 70846, 6323, 77024, 9517, 93307, 10110, 13831, 4136, 54992, + 69172, 15584, 33047, 77148, 17711, 31085, 33621, 126215, 21795, 114268, 35065, 145060, 59511, 11859, 154026, 131303, + 76184, 102024, 58089, 66420, 135114, 32471, 26586, 9983, 31046, 232116, 194394, 99288, 132319, 610, 10459, 98229, + 59105, 34807, 29993, 22965, 157578, 4107, 28141, 140655, 20549, 7101, 7846, 55412, 80778, 17135, 7430, 73220, + 57649, 27939, 10941, 92844, 158421, 173174, 64726, 12726, 65143, 202755, 176021, 57189, 4575, 7195, 177904, 25156, + 72235, 146111, 11686, 22007, 21899, 135284, 138978, 752, 10797, 65724, 5168, 151662, 92745, 109290, 75372, 160210, + 34035, 17369, 97529, 60335, 106079, 2306, 2423, 4131, 80159, 158934, 136359, 59711, 4508, 40343, 250673, 65860, + 78304, 17795, 104032, 148124, 25350, 58256, 33525, 20642, 75457, 81761, 183350, 24569, 46458, 63924, 58666, 8047, + 32937, 81997, 33987, 7245, 25623, 17931, 5112, 122123, 47, 80630, 79317, 15250, 8531, 7845, 42854, 87493, + 104751, 31479, 59823, 168974, 84953, 28434, 95840, 86398, 8138, 40995, 4860, 26024, 36508, 101200, 49636, 8174, + 187199, 50053, 89152, 20854, 66310, 61067, 8004, 30413, 115274, 278866, 106773, 120445, 13253, 40328, 1516, 70360, + 32461, 1703, 301530, 572, 38536, 75536, 423620, 18713, 1916, 3143, 70650, 60724, 42007, 14851, 262515, 136679, + 187160, 70985, 131034, 54573, 35055, 14435, 225137, 23005, 26325, 174156, 20786, 195824, 84394, 19162, 85376, 70194, + 35963, 49566, 21279, 91399, 94216, 64873, 68891, 55512, 45590, 3382, 26979, 72069, 97782, 126859, 187860, 246200 +] diff --git a/packages/kad-dht/src/routing-table/index.ts b/packages/kad-dht/src/routing-table/index.ts new file mode 100644 index 0000000000..a3f2afb83a --- /dev/null +++ b/packages/kad-dht/src/routing-table/index.ts @@ -0,0 +1,333 @@ +import { EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { PeerSet } from '@libp2p/peer-collections' +import Queue from 'p-queue' +import * as utils from '../utils.js' +import { KBucket, type PingEventDetails } from './k-bucket.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' + +export const KAD_CLOSE_TAG_NAME = 'kad-close' +export const KAD_CLOSE_TAG_VALUE = 50 +export const KBUCKET_SIZE = 20 +export const PING_TIMEOUT = 10000 +export const PING_CONCURRENCY = 10 + +export interface RoutingTableInit { + lan: boolean + protocol: string + kBucketSize?: number + pingTimeout?: number + pingConcurrency?: number + tagName?: string + tagValue?: number +} + +export interface RoutingTableComponents { + peerId: PeerId + peerStore: PeerStore + connectionManager: ConnectionManager + metrics?: Metrics +} + +export interface RoutingTableEvents { + 'peer:add': CustomEvent + 'peer:remove': CustomEvent +} + +/** + * A wrapper around `k-bucket`, to provide easy store and + * retrieval for peers. + */ +export class RoutingTable extends EventEmitter implements Startable { + public kBucketSize: number + public kb?: KBucket + public pingQueue: Queue + + private readonly log: Logger + private readonly components: RoutingTableComponents + private readonly lan: boolean + private readonly pingTimeout: number + private readonly pingConcurrency: number + private running: boolean + private readonly protocol: string + private readonly tagName: string + private readonly tagValue: number + private metrics?: { + routingTableSize: Metric + pingQueueSize: Metric + pingRunning: Metric + } + + constructor (components: RoutingTableComponents, init: RoutingTableInit) { + super() + + const { kBucketSize, pingTimeout, lan, pingConcurrency, protocol, tagName, tagValue } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:routing-table`) + this.kBucketSize = kBucketSize ?? KBUCKET_SIZE + this.pingTimeout = pingTimeout ?? PING_TIMEOUT + this.pingConcurrency = pingConcurrency ?? PING_CONCURRENCY + this.lan = lan + this.running = false + this.protocol = protocol + this.tagName = tagName ?? KAD_CLOSE_TAG_NAME + this.tagValue = tagValue ?? KAD_CLOSE_TAG_VALUE + + const updatePingQueueSizeMetric = (): void => { + this.metrics?.pingQueueSize.update(this.pingQueue.size) + this.metrics?.pingRunning.update(this.pingQueue.pending) + } + + this.pingQueue = new Queue({ concurrency: this.pingConcurrency }) + this.pingQueue.addListener('add', updatePingQueueSizeMetric) + this.pingQueue.addListener('next', updatePingQueueSizeMetric) + + this._onPing = this._onPing.bind(this) + } + + isStarted (): boolean { + return this.running + } + + async start (): Promise { + this.running = true + + if (this.components.metrics != null) { + this.metrics = { + routingTableSize: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_routing_table_size`), + pingQueueSize: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_ping_queue_size`), + pingRunning: this.components.metrics.registerMetric(`libp2p_kad_dht_${this.lan ? 'lan' : 'wan'}_ping_running`) + } + } + + const kBuck = new KBucket({ + localNodeId: await utils.convertPeerId(this.components.peerId), + numberOfNodesPerKBucket: this.kBucketSize, + numberOfNodesToPing: 1 + }) + this.kb = kBuck + + // test whether to evict peers + kBuck.addEventListener('ping', this._onPing) + + // tag kad-close peers + this._tagPeers(kBuck) + } + + async stop (): Promise { + this.running = false + this.pingQueue.clear() + this.kb = undefined + } + + /** + * Keep track of our k-closest peers and tag them in the peer store as such + * - this will lower the chances that connections to them get closed when + * we reach connection limits + */ + _tagPeers (kBuck: KBucket): void { + let kClosest = new PeerSet() + + const updatePeerTags = utils.debounce(() => { + const newClosest = new PeerSet( + kBuck.closest(kBuck.localNodeId, KBUCKET_SIZE).map(contact => contact.peer) + ) + const addedPeers = newClosest.difference(kClosest) + const removedPeers = kClosest.difference(newClosest) + + Promise.resolve() + .then(async () => { + for (const peer of addedPeers) { + await this.components.peerStore.merge(peer, { + tags: { + [this.tagName]: { + value: this.tagValue + } + } + }) + } + + for (const peer of removedPeers) { + await this.components.peerStore.merge(peer, { + tags: { + [this.tagName]: undefined + } + }) + } + }) + .catch(err => { + this.log.error('Could not update peer tags', err) + }) + + kClosest = newClosest + }) + + kBuck.addEventListener('added', (evt) => { + updatePeerTags() + + this.safeDispatchEvent('peer:add', { detail: evt.detail.peer }) + }) + kBuck.addEventListener('removed', (evt) => { + updatePeerTags() + + this.safeDispatchEvent('peer:remove', { detail: evt.detail.peer }) + }) + } + + /** + * Called on the `ping` event from `k-bucket` when a bucket is full + * and cannot split. + * + * `oldContacts.length` is defined by the `numberOfNodesToPing` param + * passed to the `k-bucket` constructor. + * + * `oldContacts` will not be empty and is the list of contacts that + * have not been contacted for the longest. + */ + _onPing (evt: CustomEvent): void { + const { + oldContacts, + newContact + } = evt.detail + + // add to a queue so multiple ping requests do not overlap and we don't + // flood the network with ping requests if lots of newContact requests + // are received + this.pingQueue.add(async () => { + if (!this.running) { + return + } + + let responded = 0 + + try { + await Promise.all( + oldContacts.map(async oldContact => { + try { + const options = { + signal: AbortSignal.timeout(this.pingTimeout) + } + + this.log('pinging old contact %p', oldContact.peer) + const connection = await this.components.connectionManager.openConnection(oldContact.peer, options) + const stream = await connection.newStream(this.protocol, options) + stream.close() + responded++ + } catch (err: any) { + if (this.running && this.kb != null) { + // only evict peers if we are still running, otherwise we evict when dialing is + // cancelled due to shutdown in progress + this.log.error('could not ping peer %p', oldContact.peer, err) + this.log('evicting old contact after ping failed %p', oldContact.peer) + this.kb.remove(oldContact.id) + } + } finally { + this.metrics?.routingTableSize.update(this.size) + } + }) + ) + + if (this.running && responded < oldContacts.length && this.kb != null) { + this.log('adding new contact %p', newContact.peer) + this.kb.add(newContact) + } + } catch (err: any) { + this.log.error('could not process k-bucket ping event', err) + } + }) + .catch(err => { + this.log.error('could not process k-bucket ping event', err) + }) + } + + // -- Public Interface + + /** + * Amount of currently stored peers + */ + get size (): number { + if (this.kb == null) { + return 0 + } + + return this.kb.count() + } + + /** + * Find a specific peer by id + */ + async find (peer: PeerId): Promise { + const key = await utils.convertPeerId(peer) + const closest = this.closestPeer(key) + + if (closest != null && peer.equals(closest)) { + return closest + } + + return undefined + } + + /** + * Retrieve the closest peers to the given key + */ + closestPeer (key: Uint8Array): PeerId | undefined { + const res = this.closestPeers(key, 1) + + if (res.length > 0) { + return res[0] + } + + return undefined + } + + /** + * Retrieve the `count`-closest peers to the given key + */ + closestPeers (key: Uint8Array, count = this.kBucketSize): PeerId[] { + if (this.kb == null) { + return [] + } + + const closest = this.kb.closest(key, count) + + return closest.map(p => p.peer) + } + + /** + * Add or update the routing table with the given peer + */ + async add (peer: PeerId): Promise { + if (this.kb == null) { + throw new Error('RoutingTable is not started') + } + + const id = await utils.convertPeerId(peer) + + this.kb.add({ id, peer }) + + this.log('added %p with kad id %b', peer, id) + + this.metrics?.routingTableSize.update(this.size) + } + + /** + * Remove a given peer from the table + */ + async remove (peer: PeerId): Promise { + if (this.kb == null) { + throw new Error('RoutingTable is not started') + } + + const id = await utils.convertPeerId(peer) + + this.kb.remove(id) + + this.metrics?.routingTableSize.update(this.size) + } +} diff --git a/packages/kad-dht/src/routing-table/k-bucket.ts b/packages/kad-dht/src/routing-table/k-bucket.ts new file mode 100644 index 0000000000..e49f1d0af2 --- /dev/null +++ b/packages/kad-dht/src/routing-table/k-bucket.ts @@ -0,0 +1,523 @@ +/* +index.js - Kademlia DHT K-bucket implementation as a binary tree. + +The MIT License (MIT) + +Copyright (c) 2013-2021 Tristan Slominski + +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. +*/ + +import { EventEmitter } from '@libp2p/interfaces/events' +import type { PeerId } from '@libp2p/interface-peer-id' + +function arrayEquals (array1: Uint8Array, array2: Uint8Array): boolean { + if (array1 === array2) { + return true + } + if (array1.length !== array2.length) { + return false + } + for (let i = 0, length = array1.length; i < length; ++i) { + if (array1[i] !== array2[i]) { + return false + } + } + return true +} + +function createNode (): Bucket { + // @ts-expect-error loose types + return { contacts: [], dontSplit: false, left: null, right: null } +} + +function ensureInt8 (name: string, val?: Uint8Array): void { + if (!(val instanceof Uint8Array)) { + throw new TypeError(name + ' is not a Uint8Array') + } +} + +export interface PingEventDetails { + oldContacts: Contact[] + newContact: Contact +} + +export interface UpdatedEventDetails { + incumbent: Contact + selection: Contact +} + +export interface KBucketEvents { + 'ping': CustomEvent + 'added': CustomEvent + 'removed': CustomEvent + 'updated': CustomEvent +} + +export interface KBucketOptions { + /** + * A Uint8Array representing the local node id + */ + localNodeId: Uint8Array + + /** + * The number of nodes that a k-bucket can contain before being full or split. + */ + numberOfNodesPerKBucket?: number + + /** + * The number of nodes to ping when a bucket that should not be split becomes + * full. KBucket will emit a `ping` event that contains `numberOfNodesToPing` + * nodes that have not been contacted the longest. + */ + numberOfNodesToPing?: number + + /** + * An optional `distance` function that gets two `id` Uint8Arrays and return + * distance (as number) between them. + */ + distance?: (a: Uint8Array, b: Uint8Array) => number + + /** + * An optional `arbiter` function that given two `contact` objects with the + * same `id` returns the desired object to be used for updating the k-bucket. + * For more details, see [arbiter function](#arbiter-function). + */ + arbiter?: (incumbent: Contact, candidate: Contact) => Contact +} + +export interface Contact { + id: Uint8Array + peer: PeerId + vectorClock?: number +} + +export interface Bucket { + id: Uint8Array + contacts: Contact[] + dontSplit: boolean + left: Bucket + right: Bucket +} + +/** + * Implementation of a Kademlia DHT k-bucket used for storing + * contact (peer node) information. + * + * @extends EventEmitter + */ +export class KBucket extends EventEmitter { + public localNodeId: Uint8Array + public root: Bucket + private readonly numberOfNodesPerKBucket: number + private readonly numberOfNodesToPing: number + private readonly distance: (a: Uint8Array, b: Uint8Array) => number + private readonly arbiter: (incumbent: Contact, candidate: Contact) => Contact + + constructor (options: KBucketOptions) { + super() + + this.localNodeId = options.localNodeId + this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket ?? 20 + this.numberOfNodesToPing = options.numberOfNodesToPing ?? 3 + this.distance = options.distance ?? KBucket.distance + // use an arbiter from options or vectorClock arbiter by default + this.arbiter = options.arbiter ?? KBucket.arbiter + + ensureInt8('option.localNodeId as parameter 1', this.localNodeId) + + this.root = createNode() + } + + /** + * Default arbiter function for contacts with the same id. Uses + * contact.vectorClock to select which contact to update the k-bucket with. + * Contact with larger vectorClock field will be selected. If vectorClock is + * the same, candidate will be selected. + * + * @param {object} incumbent - Contact currently stored in the k-bucket. + * @param {object} candidate - Contact being added to the k-bucket. + * @returns {object} Contact to updated the k-bucket with. + */ + static arbiter (incumbent: Contact, candidate: Contact): Contact { + return (incumbent.vectorClock ?? 0) > (candidate.vectorClock ?? 0) ? incumbent : candidate + } + + /** + * Default distance function. Finds the XOR + * distance between firstId and secondId. + * + * @param {Uint8Array} firstId - Uint8Array containing first id. + * @param {Uint8Array} secondId - Uint8Array containing second id. + * @returns {number} Integer The XOR distance between firstId and secondId. + */ + static distance (firstId: Uint8Array, secondId: Uint8Array): number { + let distance = 0 + let i = 0 + const min = Math.min(firstId.length, secondId.length) + const max = Math.max(firstId.length, secondId.length) + for (; i < min; ++i) { + distance = distance * 256 + (firstId[i] ^ secondId[i]) + } + for (; i < max; ++i) distance = distance * 256 + 255 + return distance + } + + /** + * Adds a contact to the k-bucket. + * + * @param {object} contact - the contact object to add + */ + add (contact: Contact): KBucket { + ensureInt8('contact.id', contact?.id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + // this is not a leaf node but an inner node with 'low' and 'high' + // branches; we will check the appropriate bit of the identifier and + // delegate to the appropriate node for further processing + node = this._determineNode(node, contact.id, bitIndex++) + } + + // check if the contact already exists + const index = this._indexOf(node, contact.id) + if (index >= 0) { + this._update(node, index, contact) + return this + } + + if (node.contacts.length < this.numberOfNodesPerKBucket) { + node.contacts.push(contact) + this.safeDispatchEvent('added', { detail: contact }) + return this + } + + // the bucket is full + if (node.dontSplit) { + // we are not allowed to split the bucket + // we need to ping the first this.numberOfNodesToPing + // in order to determine if they are alive + // only if one of the pinged nodes does not respond, can the new contact + // be added (this prevents DoS flodding with new invalid contacts) + this.safeDispatchEvent('ping', { + detail: { + oldContacts: node.contacts.slice(0, this.numberOfNodesToPing), + newContact: contact + } + }) + return this + } + + this._split(node, bitIndex) + return this.add(contact) + } + + /** + * Get the n closest contacts to the provided node id. "Closest" here means: + * closest according to the XOR metric of the contact node id. + * + * @param {Uint8Array} id - Contact node id + * @param {number} n - Integer (Default: Infinity) The maximum number of closest contacts to return + * @returns {Array} Array Maximum of n closest contacts to the node id + */ + closest (id: Uint8Array, n = Infinity): Contact[] { + ensureInt8('id', id) + + if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { + throw new TypeError('n is not positive number') + } + + let contacts: Contact[] = [] + + for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + const detNode = this._determineNode(node, id, bitIndex++) + nodes.push(node.left === detNode ? node.right : node.left) + nodes.push(detNode) + } else { + contacts = contacts.concat(node.contacts) + } + } + + return contacts + .map(a => ({ + distance: this.distance(a.id, id), + contact: a + })) + .sort((a, b) => a.distance - b.distance) + .slice(0, n) + .map(a => a.contact) + } + + /** + * Counts the total number of contacts in the tree. + * + * @returns {number} The number of contacts held in the tree + */ + count (): number { + // return this.toArray().length + let count = 0 + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + count += node.contacts.length + } + } + + return count + } + + /** + * Determines whether the id at the bitIndex is 0 or 1. + * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {Uint8Array} id - Id to compare localNodeId with. + * @param {number} bitIndex - Integer (Default: 0) The bit index to which bit to check in the id Uint8Array. + * @returns {object} left leaf if id at bitIndex is 0, right leaf otherwise. + */ + _determineNode (node: any, id: Uint8Array, bitIndex: number): Bucket { + // **NOTE** remember that id is a Uint8Array and has granularity of + // bytes (8 bits), whereas the bitIndex is the _bit_ index (not byte) + + // id's that are too short are put in low bucket (1 byte = 8 bits) + // (bitIndex >> 3) finds how many bytes the bitIndex describes + // bitIndex % 8 checks if we have extra bits beyond byte multiples + // if number of bytes is <= no. of bytes described by bitIndex and there + // are extra bits to consider, this means id has less bits than what + // bitIndex describes, id therefore is too short, and will be put in low + // bucket + const bytesDescribedByBitIndex = bitIndex >> 3 + const bitIndexWithinByte = bitIndex % 8 + if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { + return node.left + } + + const byteUnderConsideration = id[bytesDescribedByBitIndex] + + // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits + // where 255 is 11111111 and 0 is 00000000 + // in order to find out whether the bit at bitIndexWithinByte is set + // we construct (1 << (7 - bitIndexWithinByte)) which will consist + // of all bits being 0, with only one bit set to 1 + // for example, if bitIndexWithinByte is 3, we will construct 00010000 by + // (1 << (7 - 3)) -> (1 << 4) -> 16 + if ((byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) !== 0) { + return node.right + } + + return node.left + } + + /** + * Get a contact by its exact ID. + * If this is a leaf, loop through the bucket contents and return the correct + * contact if we have it or null if not. If this is an inner node, determine + * which branch of the tree to traverse and repeat. + * + * @param {Uint8Array} id - The ID of the contact to fetch. + * @returns {object | null} The contact if available, otherwise null + */ + get (id: Uint8Array): Contact | undefined { + ensureInt8('id', id) + + let bitIndex = 0 + + let node: Bucket = this.root + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + // index of uses contact id for matching + const index = this._indexOf(node, id) + return index >= 0 ? node.contacts[index] : undefined + } + + /** + * Returns the index of the contact with provided + * id if it exists, returns -1 otherwise. + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {Uint8Array} id - Contact node id. + * @returns {number} Integer Index of contact with provided id if it exists, -1 otherwise. + */ + _indexOf (node: Bucket, id: Uint8Array): number { + for (let i = 0; i < node.contacts.length; ++i) { + if (arrayEquals(node.contacts[i].id, id)) return i + } + + return -1 + } + + /** + * Removes contact with the provided id. + * + * @param {Uint8Array} id - The ID of the contact to remove + * @returns {object} The k-bucket itself + */ + remove (id: Uint8Array): KBucket { + ensureInt8('the id as parameter 1', id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + const index = this._indexOf(node, id) + if (index >= 0) { + const contact = node.contacts.splice(index, 1)[0] + this.safeDispatchEvent('removed', { + detail: contact + }) + } + + return this + } + + /** + * Splits the node, redistributes contacts to the new nodes, and marks the + * node that was split as an inner node of the binary tree of nodes by + * setting this.root.contacts = null + * + * @param {object} node - node for splitting + * @param {number} bitIndex - the bitIndex to which byte to check in the Uint8Array for navigating the binary tree + */ + _split (node: Bucket, bitIndex: number): void { + node.left = createNode() + node.right = createNode() + + // redistribute existing contacts amongst the two newly created nodes + for (const contact of node.contacts) { + this._determineNode(node, contact.id, bitIndex).contacts.push(contact) + } + + // @ts-expect-error loose types + node.contacts = null // mark as inner tree node + + // don't split the "far away" node + // we check where the local node would end up and mark the other one as + // "dontSplit" (i.e. "far away") + const detNode = this._determineNode(node, this.localNodeId, bitIndex) + const otherNode = node.left === detNode ? node.right : node.left + otherNode.dontSplit = true + } + + /** + * Returns all the contacts contained in the tree as an array. + * If this is a leaf, return a copy of the bucket. If this is not a leaf, + * return the union of the low and high branches (themselves also as arrays). + * + * @returns {Array} All of the contacts in the tree, as an array + */ + toArray (): Contact[] { + let result: Contact[] = [] + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + result = result.concat(node.contacts) + } + } + return result + } + + /** + * Similar to `toArray()` but instead of buffering everything up into an + * array before returning it, yields contacts as they are encountered while + * walking the tree. + * + * @returns {Iterable} All of the contacts in the tree, as an iterable + */ + * toIterable (): Iterable { + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + + if (node == null) { + continue + } + + if (node.contacts === null) { + nodes.push(node.right, node.left) + } else { + yield * node.contacts + } + } + } + + /** + * Updates the contact selected by the arbiter. + * If the selection is our old contact and the candidate is some new contact + * then the new contact is abandoned (not added). + * If the selection is our old contact and the candidate is our old contact + * then we are refreshing the contact and it is marked as most recently + * contacted (by being moved to the right/end of the bucket array). + * If the selection is our new contact, the old contact is removed and the new + * contact is marked as most recently contacted. + * + * @param {object} node - internal object that has 2 leafs: left and right + * @param {number} index - the index in the bucket where contact exists (index has already been computed in a previous calculation) + * @param {object} contact - The contact object to update + */ + _update (node: Bucket, index: number, contact: Contact): void { + // sanity check + if (!arrayEquals(node.contacts[index].id, contact.id)) { + throw new Error('wrong index for _update') + } + + const incumbent = node.contacts[index] + const selection = this.arbiter(incumbent, contact) + // if the selection is our old contact and the candidate is some new + // contact, then there is nothing to do + if (selection === incumbent && incumbent !== contact) return + + node.contacts.splice(index, 1) // remove old contact + node.contacts.push(selection) // add more recent contact version + this.safeDispatchEvent('updated', { + detail: { + incumbent, selection + } + }) + } +} diff --git a/packages/kad-dht/src/routing-table/refresh.ts b/packages/kad-dht/src/routing-table/refresh.ts new file mode 100644 index 0000000000..5c3d198c1c --- /dev/null +++ b/packages/kad-dht/src/routing-table/refresh.ts @@ -0,0 +1,257 @@ +import { randomBytes } from '@libp2p/crypto' +import { logger } from '@libp2p/logger' +import { peerIdFromBytes } from '@libp2p/peer-id' +import length from 'it-length' +import { sha256 } from 'multiformats/hashes/sha2' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import { TABLE_REFRESH_INTERVAL, TABLE_REFRESH_QUERY_TIMEOUT } from '../constants.js' +import GENERATED_PREFIXES from './generated-prefix-list.js' +import type { RoutingTable } from './index.js' +import type { PeerRouting } from '../peer-routing/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Logger } from '@libp2p/logger' + +/** + * Cannot generate random KadIds longer than this + 1 + */ +const MAX_COMMON_PREFIX_LENGTH = 15 + +export interface RoutingTableRefreshInit { + peerRouting: PeerRouting + routingTable: RoutingTable + lan: boolean + refreshInterval?: number + refreshQueryTimeout?: number +} + +/** + * A wrapper around `k-bucket`, to provide easy store and + * retrieval for peers. + */ +export class RoutingTableRefresh { + private readonly log: Logger + private readonly peerRouting: PeerRouting + private readonly routingTable: RoutingTable + private readonly refreshInterval: number + private readonly refreshQueryTimeout: number + private readonly commonPrefixLengthRefreshedAt: Date[] + private refreshTimeoutId?: NodeJS.Timer + + constructor (init: RoutingTableRefreshInit) { + const { peerRouting, routingTable, refreshInterval, refreshQueryTimeout, lan } = init + this.log = logger(`libp2p:kad-dht:${lan ? 'lan' : 'wan'}:routing-table:refresh`) + this.peerRouting = peerRouting + this.routingTable = routingTable + this.refreshInterval = refreshInterval ?? TABLE_REFRESH_INTERVAL + this.refreshQueryTimeout = refreshQueryTimeout ?? TABLE_REFRESH_QUERY_TIMEOUT + this.commonPrefixLengthRefreshedAt = [] + + this.refreshTable = this.refreshTable.bind(this) + } + + async start (): Promise { + this.log(`refreshing routing table every ${this.refreshInterval}ms`) + this.refreshTable(true) + } + + async stop (): Promise { + if (this.refreshTimeoutId != null) { + clearTimeout(this.refreshTimeoutId) + } + } + + /** + * To speed lookups, we seed the table with random PeerIds. This means + * when we are asked to locate a peer on the network, we can find a KadId + * that is close to the requested peer ID and query that, then network + * peers will tell us who they know who is close to the fake ID + */ + refreshTable (force: boolean = false): void { + this.log('refreshing routing table') + + const prefixLength = this._maxCommonPrefix() + const refreshCpls = this._getTrackedCommonPrefixLengthsForRefresh(prefixLength) + + this.log(`max common prefix length ${prefixLength}`) + this.log(`tracked CPLs [ ${refreshCpls.map(date => date.toISOString()).join(', ')} ]`) + + /** + * If we see a gap at a common prefix length in the Routing table, we ONLY refresh up until + * the maximum cpl we have in the Routing Table OR (2 * (Cpl+ 1) with the gap), whichever + * is smaller. + * + * This is to prevent refreshes for Cpls that have no peers in the network but happen to be + * before a very high max Cpl for which we do have peers in the network. + * + * The number of 2 * (Cpl + 1) can be proved and a proof would have been written here if + * the programmer had paid more attention in the Math classes at university. + * + * So, please be patient and a doc explaining it will be published soon. + * + * https://github.com/libp2p/go-libp2p-kad-dht/commit/2851c88acb0a3f86bcfe3cfd0f4604a03db801d8#diff-ad45f4ba97ffbc4083c2eb87a4420c1157057b233f048030d67c6b551855ccf6R219 + */ + Promise.all( + refreshCpls.map(async (lastRefresh, index) => { + try { + await this._refreshCommonPrefixLength(index, lastRefresh, force) + + if (this._numPeersForCpl(prefixLength) === 0) { + const lastCpl = Math.min(2 * (index + 1), refreshCpls.length - 1) + + for (let n = index + 1; n < lastCpl + 1; n++) { + try { + await this._refreshCommonPrefixLength(n, lastRefresh, force) + } catch (err: any) { + this.log.error(err) + } + } + } + } catch (err: any) { + this.log.error(err) + } + }) + ).catch(err => { + this.log.error(err) + }).then(() => { + this.refreshTimeoutId = setTimeout(this.refreshTable, this.refreshInterval) + + if (this.refreshTimeoutId.unref != null) { + this.refreshTimeoutId.unref() + } + }).catch(err => { + this.log.error(err) + }) + } + + async _refreshCommonPrefixLength (cpl: number, lastRefresh: Date, force: boolean): Promise { + if (!force && lastRefresh.getTime() > (Date.now() - this.refreshInterval)) { + this.log('not running refresh for cpl %s as time since last refresh not above interval', cpl) + return + } + + // gen a key for the query to refresh the cpl + const peerId = await this._generateRandomPeerId(cpl) + + this.log('starting refreshing cpl %s with key %p (routing table size was %s)', cpl, peerId, this.routingTable.size) + + const peers = await length(this.peerRouting.getClosestPeers(peerId.toBytes(), { signal: AbortSignal.timeout(this.refreshQueryTimeout) })) + + this.log(`found ${peers} peers that were close to imaginary peer %p`, peerId) + this.log('finished refreshing cpl %s with key %p (routing table size is now %s)', cpl, peerId, this.routingTable.size) + } + + _getTrackedCommonPrefixLengthsForRefresh (maxCommonPrefix: number): Date[] { + if (maxCommonPrefix > MAX_COMMON_PREFIX_LENGTH) { + maxCommonPrefix = MAX_COMMON_PREFIX_LENGTH + } + + const dates = [] + + for (let i = 0; i <= maxCommonPrefix; i++) { + // defaults to the zero value if we haven't refreshed it yet. + dates[i] = this.commonPrefixLengthRefreshedAt[i] ?? new Date() + } + + return dates + } + + async _generateRandomPeerId (targetCommonPrefixLength: number): Promise { + if (this.routingTable.kb == null) { + throw new Error('Routing table not started') + } + + const randomData = randomBytes(2) + const randomUint16 = (randomData[1] << 8) + randomData[0] + + const key = await this._makePeerId(this.routingTable.kb.localNodeId, randomUint16, targetCommonPrefixLength) + + return peerIdFromBytes(key) + } + + async _makePeerId (localKadId: Uint8Array, randomPrefix: number, targetCommonPrefixLength: number): Promise { + if (targetCommonPrefixLength > MAX_COMMON_PREFIX_LENGTH) { + throw new Error(`Cannot generate peer ID for common prefix length greater than ${MAX_COMMON_PREFIX_LENGTH}`) + } + + const view = new DataView(localKadId.buffer, localKadId.byteOffset, localKadId.byteLength) + const localPrefix = view.getUint16(0, false) + + // For host with ID `L`, an ID `K` belongs to a bucket with ID `B` ONLY IF CommonPrefixLen(L,K) is EXACTLY B. + // Hence, to achieve a targetPrefix `T`, we must toggle the (T+1)th bit in L & then copy (T+1) bits from L + // to our randomly generated prefix. + const toggledLocalPrefix = localPrefix ^ (0x8000 >> targetCommonPrefixLength) + + // Combine the toggled local prefix and the random bits at the correct offset + // such that ONLY the first `targetCommonPrefixLength` bits match the local ID. + const mask = 65535 << (16 - (targetCommonPrefixLength + 1)) + const targetPrefix = (toggledLocalPrefix & mask) | (randomPrefix & ~mask) + + // Convert to a known peer ID. + const keyPrefix = GENERATED_PREFIXES[targetPrefix] + + const keyBuffer = new ArrayBuffer(34) + const keyView = new DataView(keyBuffer, 0, keyBuffer.byteLength) + keyView.setUint8(0, sha256.code) + keyView.setUint8(1, 32) + keyView.setUint32(2, keyPrefix, false) + + return new Uint8Array(keyView.buffer, keyView.byteOffset, keyView.byteLength) + } + + /** + * returns the maximum common prefix length between any peer in the table + * and the current peer + */ + _maxCommonPrefix (): number { + // xor our KadId with every KadId in the k-bucket tree, + // return the longest id prefix that is the same + let prefixLength = 0 + + for (const length of this._prefixLengths()) { + if (length > prefixLength) { + prefixLength = length + } + } + + return prefixLength + } + + /** + * Returns the number of peers in the table with a given prefix length + */ + _numPeersForCpl (prefixLength: number): number { + let count = 0 + + for (const length of this._prefixLengths()) { + if (length === prefixLength) { + count++ + } + } + + return count + } + + /** + * Yields the common prefix length of every peer in the table + */ + * _prefixLengths (): Generator { + if (this.routingTable.kb == null) { + return + } + + for (const { id } of this.routingTable.kb.toIterable()) { + const distance = uint8ArrayXor(this.routingTable.kb.localNodeId, id) + let leadingZeros = 0 + + for (const byte of distance) { + if (byte === 0) { + leadingZeros++ + } else { + break + } + } + + yield leadingZeros + } + } +} diff --git a/packages/kad-dht/src/rpc/handlers/add-provider.ts b/packages/kad-dht/src/rpc/handlers/add-provider.ts new file mode 100644 index 0000000000..f01c5f21da --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/add-provider.ts @@ -0,0 +1,63 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { CID } from 'multiformats/cid' +import type { Message } from '../../message/index.js' +import type { Providers } from '../../providers' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const log = logger('libp2p:kad-dht:rpc:handlers:add-provider') + +export interface AddProviderHandlerInit { + providers: Providers +} + +export class AddProviderHandler implements DHTMessageHandler { + private readonly providers: Providers + + constructor (init: AddProviderHandlerInit) { + const { providers } = init + this.providers = providers + } + + async handle (peerId: PeerId, msg: Message): Promise { + log('start') + + if (msg.key == null || msg.key.length === 0) { + throw new CodeError('Missing key', 'ERR_MISSING_KEY') + } + + let cid: CID + try { + // this is actually just the multihash, not the whole CID + cid = CID.decode(msg.key) + } catch (err: any) { + throw new CodeError('Invalid CID', 'ERR_INVALID_CID') + } + + if (msg.providerPeers == null || msg.providerPeers.length === 0) { + log.error('no providers found in message') + } + + await Promise.all( + msg.providerPeers.map(async (pi) => { + // Ignore providers not from the originator + if (!pi.id.equals(peerId)) { + log('invalid provider peer %p from %p', pi.id, peerId) + return + } + + if (pi.multiaddrs.length < 1) { + log('no valid addresses for provider %p. Ignore', peerId) + return + } + + log('received provider %p for %s (addrs %s)', peerId, cid, pi.multiaddrs.map((m) => m.toString())) + + await this.providers.addProvider(cid, pi.id) + }) + ) + + return undefined + } +} diff --git a/packages/kad-dht/src/rpc/handlers/find-node.ts b/packages/kad-dht/src/rpc/handlers/find-node.ts new file mode 100644 index 0000000000..1bf7b6ce47 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/find-node.ts @@ -0,0 +1,72 @@ +import { logger } from '@libp2p/logger' +import { protocols } from '@multiformats/multiaddr' +import { equals as uint8ArrayEquals } from 'uint8arrays' +import { Message } from '../../message/index.js' +import { + removePrivateAddresses, + removePublicAddresses +} from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +const log = logger('libp2p:kad-dht:rpc:handlers:find-node') + +export interface FindNodeHandlerInit { + peerRouting: PeerRouting + lan: boolean +} + +export interface FindNodeHandlerComponents { + peerId: PeerId + addressManager: AddressManager +} + +export class FindNodeHandler implements DHTMessageHandler { + private readonly peerRouting: PeerRouting + private readonly lan: boolean + private readonly components: FindNodeHandlerComponents + + constructor (components: FindNodeHandlerComponents, init: FindNodeHandlerInit) { + const { peerRouting, lan } = init + + this.components = components + this.peerRouting = peerRouting + this.lan = Boolean(lan) + } + + /** + * Process `FindNode` DHT messages + */ + async handle (peerId: PeerId, msg: Message): Promise { + log('incoming request from %p for peers closer to %b', peerId, msg.key) + + let closer: PeerInfo[] = [] + + if (uint8ArrayEquals(this.components.peerId.toBytes(), msg.key)) { + closer = [{ + id: this.components.peerId, + multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)), + protocols: [] + }] + } else { + closer = await this.peerRouting.getCloserPeersOffline(msg.key, peerId) + } + + closer = closer + .map(this.lan ? removePublicAddresses : removePrivateAddresses) + .filter(({ multiaddrs }) => multiaddrs.length) + + const response = new Message(msg.type, new Uint8Array(0), msg.clusterLevel) + + if (closer.length > 0) { + response.closerPeers = closer + } else { + log('could not find any peers closer to %b than %p', msg.key, peerId) + } + + return response + } +} diff --git a/packages/kad-dht/src/rpc/handlers/get-providers.ts b/packages/kad-dht/src/rpc/handlers/get-providers.ts new file mode 100644 index 0000000000..c213591b61 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/get-providers.ts @@ -0,0 +1,105 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { CID } from 'multiformats/cid' +import { Message } from '../../message/index.js' +import { + removePrivateAddresses, + removePublicAddresses +} from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { Providers } from '../../providers.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:kad-dht:rpc:handlers:get-providers') + +export interface GetProvidersHandlerInit { + peerRouting: PeerRouting + providers: Providers + lan: boolean +} + +export interface GetProvidersHandlerComponents { + peerStore: PeerStore +} + +export class GetProvidersHandler implements DHTMessageHandler { + private readonly components: GetProvidersHandlerComponents + private readonly peerRouting: PeerRouting + private readonly providers: Providers + private readonly lan: boolean + + constructor (components: GetProvidersHandlerComponents, init: GetProvidersHandlerInit) { + const { peerRouting, providers, lan } = init + + this.components = components + this.peerRouting = peerRouting + this.providers = providers + this.lan = Boolean(lan) + } + + async handle (peerId: PeerId, msg: Message): Promise { + let cid + try { + cid = CID.decode(msg.key) + } catch (err: any) { + throw new CodeError('Invalid CID', 'ERR_INVALID_CID') + } + + log('%p asking for providers for %s', peerId, cid) + + const [peers, closer] = await Promise.all([ + this.providers.getProviders(cid), + this.peerRouting.getCloserPeersOffline(msg.key, peerId) + ]) + + const providerPeers = await this._getPeers(peers) + const closerPeers = await this._getPeers(closer.map(({ id }) => id)) + const response = new Message(msg.type, msg.key, msg.clusterLevel) + + if (providerPeers.length > 0) { + response.providerPeers = providerPeers + } + + if (closerPeers.length > 0) { + response.closerPeers = closerPeers + } + + log('got %s providers %s closerPeers', providerPeers.length, closerPeers.length) + return response + } + + async _getAddresses (peerId: PeerId): Promise { + return [] + } + + async _getPeers (peerIds: PeerId[]): Promise { + const output: PeerInfo[] = [] + const addrFilter = this.lan ? removePublicAddresses : removePrivateAddresses + + for (const peerId of peerIds) { + try { + const peer = await this.components.peerStore.get(peerId) + + const peerAfterFilter = addrFilter({ + id: peerId, + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), + protocols: peer.protocols + }) + + if (peerAfterFilter.multiaddrs.length > 0) { + output.push(peerAfterFilter) + } + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + } + + return output + } +} diff --git a/packages/kad-dht/src/rpc/handlers/get-value.ts b/packages/kad-dht/src/rpc/handlers/get-value.ts new file mode 100644 index 0000000000..d5a7c0a311 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/get-value.ts @@ -0,0 +1,131 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { Libp2pRecord } from '@libp2p/record' +import { + MAX_RECORD_AGE +} from '../../constants.js' +import { Message, MESSAGE_TYPE } from '../../message/index.js' +import { bufferToRecordKey, isPublicKeyKey, fromPublicKeyKey } from '../../utils.js' +import type { PeerRouting } from '../../peer-routing/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:kad-dht:rpc:handlers:get-value') + +export interface GetValueHandlerInit { + peerRouting: PeerRouting +} + +export interface GetValueHandlerComponents { + peerStore: PeerStore + datastore: Datastore +} + +export class GetValueHandler implements DHTMessageHandler { + private readonly components: GetValueHandlerComponents + private readonly peerRouting: PeerRouting + + constructor (components: GetValueHandlerComponents, init: GetValueHandlerInit) { + const { peerRouting } = init + + this.components = components + this.peerRouting = peerRouting + } + + async handle (peerId: PeerId, msg: Message): Promise { + const key = msg.key + + log('%p asked for key %b', peerId, key) + + if (key == null || key.length === 0) { + throw new CodeError('Invalid key', 'ERR_INVALID_KEY') + } + + const response = new Message(MESSAGE_TYPE.GET_VALUE, key, msg.clusterLevel) + + if (isPublicKeyKey(key)) { + log('is public key') + const idFromKey = fromPublicKeyKey(key) + let pubKey: Uint8Array | undefined + + try { + const peer = await this.components.peerStore.get(idFromKey) + + if (peer.id.publicKey == null) { + throw new CodeError('No public key found in key book', 'ERR_NOT_FOUND') + } + + pubKey = peer.id.publicKey + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + if (pubKey != null) { + log('returning found public key') + response.record = new Libp2pRecord(key, pubKey, new Date()) + return response + } + } + + const [record, closer] = await Promise.all([ + this._checkLocalDatastore(key), + this.peerRouting.getCloserPeersOffline(msg.key, peerId) + ]) + + if (record != null) { + log('had record for %b in local datastore', key) + response.record = record + } + + if (closer.length > 0) { + log('had %s closer peers in routing table', closer.length) + response.closerPeers = closer + } + + return response + } + + /** + * Try to fetch a given record by from the local datastore. + * Returns the record if it is still valid, meaning + * - it was either authored by this node, or + * - it was received less than `MAX_RECORD_AGE` ago. + */ + async _checkLocalDatastore (key: Uint8Array): Promise { + log('checkLocalDatastore looking for %b', key) + const dsKey = bufferToRecordKey(key) + + // Fetch value from ds + let rawRecord + try { + rawRecord = await this.components.datastore.get(dsKey) + } catch (err: any) { + if (err.code === 'ERR_NOT_FOUND') { + return undefined + } + throw err + } + + // Create record from the returned bytes + const record = Libp2pRecord.deserialize(rawRecord) + + if (record == null) { + throw new CodeError('Invalid record', 'ERR_INVALID_RECORD') + } + + // Check validity: compare time received with max record age + if (record.timeReceived == null || + Date.now() - record.timeReceived.getTime() > MAX_RECORD_AGE) { + // If record is bad delete it and return + await this.components.datastore.delete(dsKey) + return undefined + } + + // Record is valid + return record + } +} diff --git a/packages/kad-dht/src/rpc/handlers/ping.ts b/packages/kad-dht/src/rpc/handlers/ping.ts new file mode 100644 index 0000000000..09d569c06e --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/ping.ts @@ -0,0 +1,13 @@ +import { logger } from '@libp2p/logger' +import type { Message } from '../../message/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const log = logger('libp2p:kad-dht:rpc:handlers:ping') + +export class PingHandler implements DHTMessageHandler { + async handle (peerId: PeerId, msg: Message): Promise { + log('ping from %p', peerId) + return msg + } +} diff --git a/packages/kad-dht/src/rpc/handlers/put-value.ts b/packages/kad-dht/src/rpc/handlers/put-value.ts new file mode 100644 index 0000000000..20a4084948 --- /dev/null +++ b/packages/kad-dht/src/rpc/handlers/put-value.ts @@ -0,0 +1,58 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { type Logger, logger } from '@libp2p/logger' +import { verifyRecord } from '@libp2p/record/validators' +import { bufferToRecordKey } from '../../utils.js' +import type { Validators } from '../../index.js' +import type { Message } from '../../message/index.js' +import type { DHTMessageHandler } from '../index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +export interface PutValueHandlerInit { + validators: Validators +} + +export interface PutValueHandlerComponents { + datastore: Datastore +} + +export class PutValueHandler implements DHTMessageHandler { + private readonly log: Logger + private readonly components: PutValueHandlerComponents + private readonly validators: Validators + + constructor (components: PutValueHandlerComponents, init: PutValueHandlerInit) { + const { validators } = init + + this.components = components + this.log = logger('libp2p:kad-dht:rpc:handlers:put-value') + this.validators = validators + } + + async handle (peerId: PeerId, msg: Message): Promise { + const key = msg.key + this.log('%p asked us to store value for key %b', peerId, key) + + const record = msg.record + + if (record == null) { + const errMsg = `Empty record from: ${peerId.toString()}` + + this.log.error(errMsg) + throw new CodeError(errMsg, 'ERR_EMPTY_RECORD') + } + + try { + await verifyRecord(this.validators, record) + + record.timeReceived = new Date() + const recordKey = bufferToRecordKey(record.key) + await this.components.datastore.put(recordKey, record.serialize().subarray()) + this.log('put record for %b into datastore under key %k', key, recordKey) + } catch (err: any) { + this.log('did not put record for key %b into datastore %o', key, err) + } + + return msg + } +} diff --git a/packages/kad-dht/src/rpc/index.ts b/packages/kad-dht/src/rpc/index.ts new file mode 100644 index 0000000000..fc435fe062 --- /dev/null +++ b/packages/kad-dht/src/rpc/index.ts @@ -0,0 +1,115 @@ +import { type Logger, logger } from '@libp2p/logger' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Message, MESSAGE_TYPE } from '../message/index.js' +import { AddProviderHandler } from './handlers/add-provider.js' +import { FindNodeHandler, type FindNodeHandlerComponents } from './handlers/find-node.js' +import { GetProvidersHandler, type GetProvidersHandlerComponents } from './handlers/get-providers.js' +import { GetValueHandler, type GetValueHandlerComponents } from './handlers/get-value.js' +import { PingHandler } from './handlers/ping.js' +import { PutValueHandler, type PutValueHandlerComponents } from './handlers/put-value.js' +import type { Validators } from '../index.js' +import type { PeerRouting } from '../peer-routing' +import type { Providers } from '../providers' +import type { RoutingTable } from '../routing-table' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { IncomingStreamData } from '@libp2p/interface-registrar' + +export interface DHTMessageHandler { + handle: (peerId: PeerId, msg: Message) => Promise +} + +export interface RPCInit { + routingTable: RoutingTable + providers: Providers + peerRouting: PeerRouting + validators: Validators + lan: boolean +} + +export interface RPCComponents extends GetValueHandlerComponents, PutValueHandlerComponents, FindNodeHandlerComponents, GetProvidersHandlerComponents { + +} + +export class RPC { + private readonly handlers: Record + private readonly routingTable: RoutingTable + private readonly log: Logger + + constructor (components: RPCComponents, init: RPCInit) { + const { providers, peerRouting, validators, lan } = init + + this.log = logger('libp2p:kad-dht:rpc') + this.routingTable = init.routingTable + this.handlers = { + [MESSAGE_TYPE.GET_VALUE]: new GetValueHandler(components, { peerRouting }), + [MESSAGE_TYPE.PUT_VALUE]: new PutValueHandler(components, { validators }), + [MESSAGE_TYPE.FIND_NODE]: new FindNodeHandler(components, { peerRouting, lan }), + [MESSAGE_TYPE.ADD_PROVIDER]: new AddProviderHandler({ providers }), + [MESSAGE_TYPE.GET_PROVIDERS]: new GetProvidersHandler(components, { peerRouting, providers, lan }), + [MESSAGE_TYPE.PING]: new PingHandler() + } + } + + /** + * Process incoming DHT messages + */ + async handleMessage (peerId: PeerId, msg: Message): Promise { + try { + await this.routingTable.add(peerId) + } catch (err: any) { + this.log.error('Failed to update the kbucket store', err) + } + + // get handler & execute it + const handler = this.handlers[msg.type] + + if (handler == null) { + this.log.error(`no handler found for message type: ${msg.type}`) + return + } + + return handler.handle(peerId, msg) + } + + /** + * Handle incoming streams on the dht protocol + */ + onIncomingStream (data: IncomingStreamData): void { + Promise.resolve().then(async () => { + const { stream, connection } = data + const peerId = connection.remotePeer + + try { + await this.routingTable.add(peerId) + } catch (err: any) { + this.log.error(err) + } + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + await pipe( + stream, + (source) => lp.decode(source), + async function * (source) { + for await (const msg of source) { + // handle the message + const desMessage = Message.deserialize(msg) + self.log('incoming %s from %p', desMessage.type, peerId) + const res = await self.handleMessage(peerId, desMessage) + + // Not all handlers will return a response + if (res != null) { + yield res.serialize() + } + } + }, + (source) => lp.encode(source), + stream + ) + }) + .catch(err => { + this.log.error(err) + }) + } +} diff --git a/packages/kad-dht/src/topology-listener.ts b/packages/kad-dht/src/topology-listener.ts new file mode 100644 index 0000000000..162e2c782c --- /dev/null +++ b/packages/kad-dht/src/topology-listener.ts @@ -0,0 +1,77 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { createTopology } from '@libp2p/topology' +import type { KadDHTComponents } from '.' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Startable } from '@libp2p/interfaces/startable' +import type { Logger } from '@libp2p/logger' + +export interface TopologyListenerInit { + protocol: string + lan: boolean +} + +export interface TopologyListenerEvents { + 'peer': CustomEvent +} + +/** + * Receives notifications of new peers joining the network that support the DHT protocol + */ +export class TopologyListener extends EventEmitter implements Startable { + private readonly log: Logger + private readonly components: KadDHTComponents + private readonly protocol: string + private running: boolean + private registrarId?: string + + constructor (components: KadDHTComponents, init: TopologyListenerInit) { + super() + + const { protocol, lan } = init + + this.components = components + this.log = logger(`libp2p:kad-dht:topology-listener:${lan ? 'lan' : 'wan'}`) + this.running = false + this.protocol = protocol + } + + isStarted (): boolean { + return this.running + } + + /** + * Start the network + */ + async start (): Promise { + if (this.running) { + return + } + + this.running = true + + // register protocol with topology + const topology = createTopology({ + onConnect: (peerId) => { + this.log('observed peer %p with protocol %s', peerId, this.protocol) + this.dispatchEvent(new CustomEvent('peer', { + detail: peerId + })) + } + }) + this.registrarId = await this.components.registrar.register(this.protocol, topology) + } + + /** + * Stop all network activity + */ + async stop (): Promise { + this.running = false + + // unregister protocol and handlers + if (this.registrarId != null) { + this.components.registrar.unregister(this.registrarId) + this.registrarId = undefined + } + } +} diff --git a/packages/kad-dht/src/utils.ts b/packages/kad-dht/src/utils.ts new file mode 100644 index 0000000000..dccf4c5b08 --- /dev/null +++ b/packages/kad-dht/src/utils.ts @@ -0,0 +1,151 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { Libp2pRecord } from '@libp2p/record' +import { Key } from 'interface-datastore/key' +import { sha256 } from 'multiformats/hashes/sha2' +import isPrivateIp from 'private-ip' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { RECORD_KEY_PREFIX } from './constants.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +// const IPNS_PREFIX = uint8ArrayFromString('/ipns/') +const PK_PREFIX = uint8ArrayFromString('/pk/') + +export function removePrivateAddresses (peer: PeerInfo): PeerInfo { + return { + ...peer, + multiaddrs: peer.multiaddrs.filter(multiaddr => { + const [[type, addr]] = multiaddr.stringTuples() + + // treat /dns, /dns4, and /dns6 addrs as public + if (type === 53 || type === 54 || type === 55) { + // localhost can be a dns address but it's private + if (addr === 'localhost') { + return false + } + + return true + } + + if (type !== 4 && type !== 6) { + return false + } + + if (addr == null) { + return false + } + + const isPrivate = isPrivateIp(addr) + + if (isPrivate == null) { + // not an ip address + return true + } + + return !isPrivate + }) + } +} + +export function removePublicAddresses (peer: PeerInfo): PeerInfo { + return { + ...peer, + multiaddrs: peer.multiaddrs.filter(multiaddr => { + const [[type, addr]] = multiaddr.stringTuples() + + if (addr === 'localhost') { + return true + } + + if (type !== 4 && type !== 6) { + return false + } + + if (addr == null) { + return false + } + + const isPrivate = isPrivateIp(addr) + + if (isPrivate == null) { + // not an ip address + return false + } + + return isPrivate + }) + } +} + +/** + * Creates a DHT ID by hashing a given Uint8Array + */ +export async function convertBuffer (buf: Uint8Array): Promise { + const multihash = await sha256.digest(buf) + + return multihash.digest +} + +/** + * Creates a DHT ID by hashing a Peer ID + */ +export async function convertPeerId (peerId: PeerId): Promise { + return convertBuffer(peerId.toBytes()) +} + +/** + * Convert a Uint8Array to their SHA2-256 hash + */ +export function bufferToKey (buf: Uint8Array): Key { + return new Key('/' + uint8ArrayToString(buf, 'base32'), false) +} + +/** + * Convert a Uint8Array to their SHA2-256 hash + */ +export function bufferToRecordKey (buf: Uint8Array): Key { + return new Key(`${RECORD_KEY_PREFIX}/${uint8ArrayToString(buf, 'base32')}`, false) +} + +/** + * Generate the key for a public key. + */ +export function keyForPublicKey (peer: PeerId): Uint8Array { + return uint8ArrayConcat([ + PK_PREFIX, + peer.toBytes() + ]) +} + +export function isPublicKeyKey (key: Uint8Array): boolean { + return uint8ArrayToString(key.subarray(0, 4)) === '/pk/' +} + +export function isIPNSKey (key: Uint8Array): boolean { + return uint8ArrayToString(key.subarray(0, 4)) === '/ipns/' +} + +export function fromPublicKeyKey (key: Uint8Array): PeerId { + return peerIdFromBytes(key.subarray(4)) +} + +/** + * Create a new put record, encodes and signs it if enabled + */ +export function createPutRecord (key: Uint8Array, value: Uint8Array): Uint8Array { + const timeReceived = new Date() + const rec = new Libp2pRecord(key, value, timeReceived) + + return rec.serialize() +} + +export function debounce (callback: () => void, wait: number = 100): () => void { + let timeout: ReturnType + + return (): void => { + clearTimeout(timeout) + timeout = setTimeout(() => { callback() }, wait) + } +} diff --git a/packages/kad-dht/test/enable-server-mode.spec.ts b/packages/kad-dht/test/enable-server-mode.spec.ts new file mode 100644 index 0000000000..80a93abdc1 --- /dev/null +++ b/packages/kad-dht/test/enable-server-mode.spec.ts @@ -0,0 +1,74 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import delay from 'delay' +import { TestDHT } from './utils/test-dht.js' + +const testCases: Array<[string, string, string]> = [ + ['should enable server mode when public IP4 addresses are found', '/ip4/139.178.91.71/udp/4001/quic', 'server'], + ['should enable server mode when public IP6 addresses are found', '/ip6/2604:1380:45e3:6e00::1/udp/4001/quic', 'server'], + ['should enable server mode when DNS4 addresses are found', '/dns4/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should enable server mode when DNS6 addresses are found', '/dns6/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should enable server mode when DNSADDR addresses are found', '/dnsaddr/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 'server'], + ['should not enable server mode when private IP4 addresses are found', '/ip4/127.0.0.1/udp/4001/quic', 'client'], + ['should not enable server mode when private IP6 addresses are found', '/ip6/::1/udp/4001/quic', 'client'], + ['should not enable server mode when otherwise public circuit relay addresses are found', '/dns4/sv15.bootstrap.libp2p.io/tcp/443/wss/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit', 'client'] +] + +describe('enable server mode', () => { + let tdht: TestDHT + + beforeEach(() => { + tdht = new TestDHT() + }) + + afterEach(async () => { + await tdht.teardown() + }) + + testCases.forEach(([name, addr, result]) => { + it(name, async function () { + const dht = await tdht.spawn() + + await expect(dht.getMode()).to.eventually.equal('client') + + dht.components.events.safeDispatchEvent('self:peer:update', { + detail: { + peer: { + addresses: [{ + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), + isCertified: true + }, { + multiaddr: multiaddr('/ip6/::1/tcp/4001'), + isCertified: true + }, { + multiaddr: multiaddr(addr), + isCertified: true + }] + } + } + }) + + await delay(100) + + await expect(dht.getMode()).to.eventually.equal(result, `did not change to "${result}" mode after updating with address ${addr}`) + + dht.components.events.safeDispatchEvent('self:peer:update', { + detail: { + peer: { + addresses: [{ + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), + isCertified: true + }] + } + } + }) + + await delay(100) + + await expect(dht.getMode()).to.eventually.equal('client', `did not reset to client mode after updating with address ${addr}`) + }) + }) +}) diff --git a/packages/kad-dht/test/fixtures/msg-1 b/packages/kad-dht/test/fixtures/msg-1 new file mode 100755 index 0000000000000000000000000000000000000000..85aadeee97e350e86ed221c30a1b1ea4b2f73be7 GIT binary patch literal 14 Vcmd;J5#rD<)6YoF$;l63000z41783D literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-2 b/packages/kad-dht/test/fixtures/msg-2 new file mode 100755 index 0000000000000000000000000000000000000000..3c75f5ed6c59241a60ae0a3678b534e16b7f2f66 GIT binary patch literal 69 zcmd;J5aQ4;)6YoF$;p>8<$|+?Sj+Q^a#Ey}gcOuEEcREdznqf67JfrZ{C7*nrGBw* Ryyf57z3RFcbfU@v7y!;~7VZE5 literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-3 b/packages/kad-dht/test/fixtures/msg-3 new file mode 100755 index 0000000000000000000000000000000000000000..0821b2012985246b55b637ad3a445f35e8bc1958 GIT binary patch literal 14 Vcmd;J6yne?)6YoF$;l63000yo16%+A literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-4 b/packages/kad-dht/test/fixtures/msg-4 new file mode 100755 index 0000000000000000000000000000000000000000..de90e34463c2c45efbae3f427acc53c6eea65ba4 GIT binary patch literal 40 ycmV+@0N4Kr1QH?=AkmLT^JMGp>`)0Q$0|se{<3s;FtI7waNZTqfMRaEyifpJ_!3Y6 literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-5 b/packages/kad-dht/test/fixtures/msg-5 new file mode 100755 index 0000000000000000000000000000000000000000..de90e34463c2c45efbae3f427acc53c6eea65ba4 GIT binary patch literal 40 ycmV+@0N4Kr1QH?=AkmLT^JMGp>`)0Q$0|se{<3s;FtI7waNZTqfMRaEyifpJ_!3Y6 literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-6 b/packages/kad-dht/test/fixtures/msg-6 new file mode 100755 index 0000000000000000000000000000000000000000..de90e34463c2c45efbae3f427acc53c6eea65ba4 GIT binary patch literal 40 ycmV+@0N4Kr1QH?=AkmLT^JMGp>`)0Q$0|se{<3s;FtI7waNZTqfMRaEyifpJ_!3Y6 literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-7 b/packages/kad-dht/test/fixtures/msg-7 new file mode 100755 index 0000000000000000000000000000000000000000..4044711de8a7bcde99696c1732c46574712c0bb2 GIT binary patch literal 88 zcmV-e0H^;50umw;AkmLT^JMGp>`)0Q$0|se{<3s;FtI7waNZTqfMRaEyh<(#A`&2w u$zlq5sJf9opM*I|GDw7!`Hb++P2IbO%S2I`aKzve2n2rs009Q%yHEhlKqkll literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/fixtures/msg-8 b/packages/kad-dht/test/fixtures/msg-8 new file mode 100755 index 0000000000000000000000000000000000000000..4044711de8a7bcde99696c1732c46574712c0bb2 GIT binary patch literal 88 zcmV-e0H^;50umw;AkmLT^JMGp>`)0Q$0|se{<3s;FtI7waNZTqfMRaEyh<(#A`&2w u$zlq5sJf9opM*I|GDw7!`Hb++P2IbO%S2I`aKzve2n2rs009Q%yHEhlKqkll literal 0 HcmV?d00001 diff --git a/packages/kad-dht/test/generate-peers/.gitignore b/packages/kad-dht/test/generate-peers/.gitignore new file mode 100644 index 0000000000..8e593fee20 --- /dev/null +++ b/packages/kad-dht/test/generate-peers/.gitignore @@ -0,0 +1 @@ +generate-peer diff --git a/packages/kad-dht/test/generate-peers/generate-peer.go b/packages/kad-dht/test/generate-peers/generate-peer.go new file mode 100644 index 0000000000..129b5a44c9 --- /dev/null +++ b/packages/kad-dht/test/generate-peers/generate-peer.go @@ -0,0 +1,85 @@ +package main + +// this code has been extracted from https://github.com/libp2p/go-libp2p-kbucket/blob/b90e3fed3255e131058ac337a19beb2ad85da43f/table_refresh.go#L45 +// if the hash of that file changes it may need to be re-extracted + +import ( + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" +) + +// maxCplForRefresh is the maximum cpl we support for refresh. +// This limit exists because we can only generate 'maxCplForRefresh' bit prefixes for now. +const maxCplForRefresh uint = 15 + +// GenRandPeerID generates a random peerID for a given Cpl +func GenRandPeerID(targetCpl uint, randPrefix uint16, localKadId []byte, keyPrefixMap []uint32) ([]byte, error) { + if targetCpl > maxCplForRefresh { + return nil, fmt.Errorf("cannot generate peer ID for Cpl greater than %d", maxCplForRefresh) + } + + localPrefix := binary.BigEndian.Uint16(localKadId) + + // For host with ID `L`, an ID `K` belongs to a bucket with ID `B` ONLY IF CommonPrefixLen(L,K) is EXACTLY B. + // Hence, to achieve a targetPrefix `T`, we must toggle the (T+1)th bit in L & then copy (T+1) bits from L + // to our randomly generated prefix. + toggledLocalPrefix := localPrefix ^ (uint16(0x8000) >> targetCpl) + + // Combine the toggled local prefix and the random bits at the correct offset + // such that ONLY the first `targetCpl` bits match the local ID. + mask := (^uint16(0)) << (16 - (targetCpl + 1)) + targetPrefix := (toggledLocalPrefix & mask) | (randPrefix & ^mask) + + // Convert to a known peer ID. + key := keyPrefixMap[targetPrefix] + + // mh.SHA2_256, peer-id-len + id := [34]byte{18, 32} + binary.BigEndian.PutUint32(id[2:], key) + return id[:], nil +} + +func main() { + jsonFile, err := os.Open("../../src/routing-table/generated-prefix-list.json") + if err != nil { + panic("Could not open generated prefix list") + } + + // defer the closing of our jsonFile so that we can parse it later on + defer jsonFile.Close() + + byteValue, err := ioutil.ReadAll(jsonFile) + if err != nil { + panic("Could not read generated prefix list") + } + + var keyPrefixMap []uint32 + json.Unmarshal([]byte(byteValue), &keyPrefixMap) + + targetCpl, err := strconv.ParseUint(os.Args[1], 10, 32) + if err != nil { + panic("Could not parse targetCpl") + } + + randPrefix, err := strconv.ParseUint(os.Args[2], 10, 16) + if err != nil { + panic("Could not parse randPrefix") + } + + localKadId, err := base64.StdEncoding.DecodeString(os.Args[3]) + if err != nil { + panic("Could not parse localKadId") + } + + b, err := GenRandPeerID(uint(targetCpl), uint16(randPrefix), localKadId, keyPrefixMap) + if err != nil { + panic("Could not generate peerId") + } + + fmt.Println(b) +} diff --git a/packages/kad-dht/test/generate-peers/generate-peers.node.ts b/packages/kad-dht/test/generate-peers/generate-peers.node.ts new file mode 100644 index 0000000000..f429d6534c --- /dev/null +++ b/packages/kad-dht/test/generate-peers/generate-peers.node.ts @@ -0,0 +1,101 @@ +/* eslint-env mocha */ +import path from 'path' +import { fileURLToPath } from 'url' +import { createRSAPeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { execa } from 'execa' +import { stubInterface } from 'ts-sinon' +import { toString as uintArrayToString } from 'uint8arrays/to-string' +import which from 'which' +import { RoutingTable } from '../../src/routing-table/index.js' +import { RoutingTableRefresh } from '../../src/routing-table/refresh.js' +import { + convertPeerId +} from '../../src/utils.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { PeerStore } from '@libp2p/interface-peer-store' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +async function fromGo (targetCpl: number, randPrefix: number, localKadId: string): Promise { + const { stdout } = await execa('./generate-peer', [targetCpl.toString(), randPrefix.toString(), localKadId], { + cwd: dirname + }) + + const arr = stdout + .slice(1, stdout.length - 1) + .split(' ') + .filter(Boolean) + .map(i => parseInt(i, 10)) + + return Uint8Array.from(arr) +} + +describe.skip('generate peers', function () { + this.timeout(540 * 1000) + const go = which.sync('go', { nothrow: true }) + + if (go == null) { + it.skip('No golang installation found on this system', () => {}) + + return + } + + let refresh: RoutingTableRefresh + + before(async () => { + await execa(go, ['build', 'generate-peer.go'], { + cwd: __dirname + }) + }) + + beforeEach(async () => { + const id = await createRSAPeerId({ bits: 512 }) + + const components = { + peerId: id, + connectionManager: stubInterface(), + peerStore: stubInterface() + } + const table = new RoutingTable(components, { + kBucketSize: 20, + lan: false, + protocol: '/ipfs/kad/1.0.0' + }) + refresh = new RoutingTableRefresh({ + routingTable: table, + // @ts-expect-error not a full implementation + peerRouting: {}, + lan: false + }) + }) + + const TEST_CASES = [{ + targetCpl: 2, + randPrefix: 29381 + }, { + targetCpl: 12, + randPrefix: 3821 + }, { + targetCpl: 5, + randPrefix: 9493 + }, { + targetCpl: 9, + randPrefix: 19209 + }, { + targetCpl: 1, + randPrefix: 49898 + }] + + TEST_CASES.forEach(({ targetCpl, randPrefix }) => { + it(`should generate peers targetCpl ${targetCpl} randPrefix ${randPrefix}`, async () => { + const peerId = await createRSAPeerId({ bits: 512 }) + const localKadId = await convertPeerId(peerId) + + const goOutput = await fromGo(targetCpl, randPrefix, uintArrayToString(localKadId, 'base64pad')) + const jsOutput = await refresh._makePeerId(localKadId, randPrefix, targetCpl) + + expect(goOutput).to.deep.equal(jsOutput) + }) + }) +}) diff --git a/packages/kad-dht/test/kad-dht.spec.ts b/packages/kad-dht/test/kad-dht.spec.ts new file mode 100644 index 0000000000..94b3687dc7 --- /dev/null +++ b/packages/kad-dht/test/kad-dht.spec.ts @@ -0,0 +1,889 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { CodeError } from '@libp2p/interfaces/errors' +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import drain from 'it-drain' +import filter from 'it-filter' +import last from 'it-last' +import map from 'it-map' +import { pipe } from 'it-pipe' +import sinon from 'sinon' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as c from '../src/constants.js' +import { EventTypes, type FinalPeerEvent, MessageType, type QueryEvent, type ValueEvent } from '../src/index.js' +import { MESSAGE_TYPE } from '../src/message/index.js' +import { peerResponseEvent } from '../src/query/events.js' +import * as kadUtils from '../src/utils.js' +import { createPeerIds } from './utils/create-peer-id.js' +import { createValues } from './utils/create-values.js' +import { countDiffPeers } from './utils/index.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { CID } from 'multiformats/cid' + +async function findEvent (events: AsyncIterable, name: 'FINAL_PEER'): Promise +async function findEvent (events: AsyncIterable, name: 'VALUE'): Promise +async function findEvent (events: AsyncIterable, name: string): Promise { + const eventTypes = new Set() + + const event = await last( + filter(events, event => { + eventTypes.add(event.name) + return event.name === name + }) + ) + + if (event == null) { + throw new Error(`No ${name} event found, saw ${Array.from(eventTypes).join()}`) + } + + return event +} + +describe('KadDHT', () => { + let peerIds: PeerId[] + let values: Array<{ cid: CID, value: Uint8Array }> + let tdht: TestDHT + + beforeEach(() => { + tdht = new TestDHT() + }) + + afterEach(async () => { + await tdht.teardown() + }) + + before(async function () { + this.timeout(10 * 1000) + + const res = await Promise.all([ + createPeerIds(3), + createValues(20) + ]) + + peerIds = res[0] + values = res[1] + }) + + afterEach(() => { + sinon.restore() + }) + + describe('create', () => { + it('simple', async () => { + const dht = await tdht.spawn({ + kBucketSize: 5 + }) + + expect(dht).to.have.property('put') + expect(dht).to.have.property('get') + expect(dht).to.have.property('provide') + expect(dht).to.have.property('findProviders') + expect(dht).to.have.property('findPeer') + expect(dht).to.have.property('getClosestPeers') + expect(dht).to.have.property('getMode') + expect(dht).to.have.property('setMode') + }) + + it('forward providers init options to providers component', async () => { + const dht = await tdht.spawn({ + kBucketSize: 5, + providers: { + cleanupInterval: 60, + provideValidity: 60 * 10 + } + }) + expect(dht.lan.providers).to.have.property('cleanupInterval', 60) + expect(dht.lan.providers).to.have.property('provideValidity', 60 * 10) + expect(dht.wan.providers).to.have.property('cleanupInterval', 60) + expect(dht.wan.providers).to.have.property('provideValidity', 60 * 10) + }) + }) + + describe('start and stop', () => { + it('simple with defaults', async () => { + const dht = await tdht.spawn(undefined, false) + + sinon.spy(dht.wan.network, 'start') + sinon.spy(dht.wan.network, 'stop') + sinon.spy(dht.lan.network, 'start') + sinon.spy(dht.lan.network, 'stop') + + await dht.start() + expect(dht.wan.network.start).to.have.property('calledOnce', true) + expect(dht.lan.network.start).to.have.property('calledOnce', true) + + await dht.stop() + expect(dht.wan.network.stop).to.have.property('calledOnce', true) + expect(dht.lan.network.stop).to.have.property('calledOnce', true) + }) + + it('server mode', async () => { + // Currently off by default + const dht = await tdht.spawn(undefined, false) + + const registrarHandleSpy = sinon.spy(dht.components.registrar, 'handle') + + await dht.start() + // lan dht is always in server mode + expect(registrarHandleSpy).to.have.property('callCount', 1) + + await dht.setMode('server') + // now wan dht should be in server mode too + expect(registrarHandleSpy).to.have.property('callCount', 2) + + await dht.stop() + }) + + it('client mode', async () => { + // Currently on by default + const dht = await tdht.spawn({ clientMode: true }, false) + + const registrarHandleSpy = sinon.spy(dht.components.registrar, 'handle') + + await dht.start() + await dht.stop() + + // lan dht is always in server mode, wan is not + expect(registrarHandleSpy).to.have.property('callCount', 1) + }) + + it('should not fail when already started', async () => { + const dht = await tdht.spawn(undefined, false) + + await dht.start() + await dht.start() + await dht.start() + + await dht.stop() + }) + + it('should not fail to stop when was not started', async () => { + const dht = await tdht.spawn(undefined, false) + + await dht.stop() + }) + }) + + describe('content fetching', () => { + it('put - get same node', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const dht = await tdht.spawn() + + // Exchange data through the dht + await drain(dht.put(key, value)) + + const res = await last(dht.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + // Connect nodes + await tdht.connect(dhtA, dhtB) + + // Exchange data through the dht + await drain(dhtA.put(key, value)) + + const res = await findEvent(dhtB.get(key), 'VALUE') + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get calls progress handler', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + // Connect nodes + await tdht.connect(dhtA, dhtB) + + const putProgress = sinon.stub() + + // Exchange data through the dht + await drain(dhtA.put(key, value, { + onProgress: putProgress + })) + + expect(putProgress).to.have.property('called', true) + + const getProgress = sinon.stub() + + await drain(dhtB.get(key, { + onProgress: getProgress + })) + + expect(getProgress).to.have.property('called', true) + }) + + it('put - should require a minimum number of peers to have successful puts', async function () { + this.timeout(10 * 1000) + + const errCode = 'ERR_NOT_AVAILABLE' + const error = new CodeError('fake error', errCode) + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB, dhtC, dhtD] = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn({ + // Stub verify record + validators: { + v: sinon.stub().rejects(error) + } + }) + ]) + + await Promise.all([ + tdht.connect(dhtA, dhtB), + tdht.connect(dhtA, dhtC), + tdht.connect(dhtA, dhtD) + ]) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get using key with no prefix (no selector available)', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get using key from provided validator and selector', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/ipns/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn({ + validators: { + ipns: sinon.stub().resolves() + }, + selectors: { + ipns: sinon.stub().returns(0) + } + }), + tdht.spawn({ + validators: { + ipns: sinon.stub().resolves() + }, + selectors: { + ipns: sinon.stub().returns(0) + } + }) + ]) + + await tdht.connect(dhtA, dhtB) + + // DHT operations + await drain(dhtA.put(key, value)) + + const res = await last(dhtB.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('put - get should fail if unrecognized key prefix in get', async function () { + this.timeout(10 * 1000) + + const key = uint8ArrayFromString('/v2/hello') + const value = uint8ArrayFromString('world') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + await drain(dhtA.put(key, value)) + + await expect(last(dhtA.get(key))).to.eventually.be.rejected().property('code', 'ERR_UNRECOGNIZED_KEY_PREFIX') + }) + + it('put - get with update', async function () { + this.timeout(20 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const valueA = uint8ArrayFromString('worldA') + const valueB = uint8ArrayFromString('worldB') + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + const dhtASpy = sinon.spy(dhtA.lan.network, 'sendRequest') + + // Put before peers connected + await drain(dhtA.put(key, valueA)) + await drain(dhtB.put(key, valueB)) + + // Connect peers + await tdht.connect(dhtA, dhtB) + + // Get values + const resA = await last(dhtA.get(key)) + const resB = await last(dhtB.get(key)) + + // First is selected + expect(resA).to.have.property('value').that.equalBytes(valueA) + expect(resB).to.have.property('value').that.equalBytes(valueA) + + let foundGetValue = false + let foundPutValue = false + + for (const call of dhtASpy.getCalls()) { + if (call.args[0].equals(dhtB.components.peerId) && call.args[1].type === 'GET_VALUE') { + // query B + foundGetValue = true + } + + if (call.args[0].equals(dhtB.components.peerId) && call.args[1].type === 'PUT_VALUE') { + // update B + foundPutValue = true + } + } + + expect(foundGetValue).to.be.true('did not get value from dhtB') + expect(foundPutValue).to.be.true('did not update value on dhtB') + }) + + it('layered get', async function () { + this.timeout(40 * 1000) + + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // Connect all + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + // DHT operations + await drain(dhts[3].put(key, value)) + + const res = await last(dhts[0].get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + + it('getMany with nvals=1 goes out to swarm if there is no local value', async () => { + const key = uint8ArrayFromString('/v/hello') + const value = uint8ArrayFromString('world') + const rec = new Libp2pRecord(key, value, new Date()) + const dht = await tdht.spawn() + + // Simulate returning a peer id to query + sinon.stub(dht.lan.routingTable, 'closestPeers').returns([peerIds[1]]) + // Simulate going out to the network and returning the record + sinon.stub(dht.lan.peerRouting, 'getValueOrPeers').callsFake(async function * (peer) { + yield peerResponseEvent({ + messageType: MessageType.GET_VALUE, + from: peer, + record: rec + }) + }) // eslint-disable-line require-await + + const res = await last(dht.get(key)) + expect(res).to.have.property('value').that.equalBytes(value) + }) + }) + + describe('content routing', () => { + it('provides', async function () { + this.timeout(20 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + const ids = dhts.map((d) => d.components.peerId) + const idsB58 = ids.map(id => id.toString()) + sinon.spy(dhts[3].lan.network, 'sendMessage') + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + // provide values + await Promise.all(values.map(async (value) => { await drain(dhts[3].provide(value.cid)) })) + + // Expect an ADD_PROVIDER message to be sent to each peer for each value + const fn = dhts[3].lan.network.sendMessage + const valuesBuffs = values.map(v => v.cid.multihash.bytes) + // @ts-expect-error fn is a spy + const calls = fn.getCalls().map(c => c.args) + + for (const [peerId, msg] of calls) { + expect(idsB58).includes(peerId.toString()) + expect(msg.type).equals(MESSAGE_TYPE.ADD_PROVIDER) + expect(valuesBuffs).includes(msg.key) + expect(msg.providerPeers.length).equals(1) + expect(msg.providerPeers[0].id.toString()).equals(idsB58[3]) + } + + // Expect each DHT to find the provider of each value + let n = 0 + for (const v of values) { + n = (n + 1) % 3 + + const events = await all(dhts[n].findProviders(v.cid)) + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + + expect(provs).to.have.length(1) + expect(provs[0].toString()).to.equal(ids[3].toString()) + } + }) + + it('does not provide to wan if in client mode', async function () { + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + const wanSpy = sinon.spy(dhts[0].wan, 'provide') + const lanSpy = sinon.spy(dhts[0].lan, 'provide') + + await drain(dhts[0].provide(values[0].cid)) + + expect(wanSpy.called).to.be.false() + expect(lanSpy.called).to.be.true() + }) + + it('provides to wan if in server mode', async function () { + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // connect peers + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + const wanSpy = sinon.spy(dhts[0].wan, 'provide') + const lanSpy = sinon.spy(dhts[0].lan, 'provide') + + await dhts[0].setMode('server') + + await drain(dhts[0].provide(values[0].cid)) + + expect(wanSpy.called).to.be.true() + expect(lanSpy.called).to.be.true() + }) + + it('find providers', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + // Connect + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]) + ]) + + await Promise.all(dhts.map(async (dht) => { await drain(dht.provide(val.cid)) })) + + const events = await all(dhts[0].findProviders(val.cid)) + + // find providers find all the 3 providers + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(3) + }) + + it('find providers from client', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + const clientDHT = await tdht.spawn({ clientMode: true }) + + // Connect + await Promise.all([ + tdht.connect(clientDHT, dhts[0]), + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]) + ]) + + await Promise.all(dhts.map(async (dht) => { await drain(dht.provide(val.cid)) })) + + const events = await all(dhts[0].findProviders(val.cid)) + + // find providers find all the 3 providers + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(3) + }) + + it('find client provider', async function () { + this.timeout(20 * 1000) + + const val = values[0] + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + const clientDHT = await tdht.spawn({ clientMode: true }) + + // Connect + await Promise.all([ + tdht.connect(clientDHT, dhts[0]), + tdht.connect(dhts[0], dhts[1]) + ]) + + await drain(clientDHT.provide(val.cid)) + + await delay(1e3) + + const events = await all(dhts[1].findProviders(val.cid)) + + // find providers find the client provider + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(1) + }) + + it('find one provider locally', async function () { + this.timeout(20 * 1000) + const val = values[0] + + const dht = await tdht.spawn() + + sinon.stub(dht.components.peerStore, 'get').withArgs(dht.components.peerId) + .resolves({ + id: dht.components.peerId, + addresses: [], + protocols: [], + tags: new Map(), + metadata: new Map() + }) + sinon.stub(dht.lan.providers, 'getProviders').resolves([dht.components.peerId]) + + // Find provider + const events = await all(dht.findProviders(val.cid)) + const provs = Object.values(events.reduce>((acc, curr) => { + if (curr.name === 'PEER_RESPONSE') { + curr.providers.forEach(peer => { + acc[peer.id.toString()] = peer.id + }) + } + + return acc + }, {})) + expect(provs).to.have.length(1) + }) + }) + + describe('peer routing', () => { + it('findPeer', async function () { + this.timeout(240 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[0], dhts[2]), + tdht.connect(dhts[0], dhts[3]) + ]) + + const ids = dhts.map((d) => d.components.peerId) + + const finalPeer = await findEvent(dhts[0].findPeer(ids[ids.length - 1]), 'FINAL_PEER') + + expect(finalPeer.peer.id.equals(ids[ids.length - 1])).to.eql(true) + }) + + it('find peer query', async function () { + this.timeout(240 * 1000) + + // Create 101 nodes + const nDHTs = 101 + + const dhts = await Promise.all( + new Array(nDHTs).fill(0).map(async () => tdht.spawn()) + ) + + const dhtsById = new Map(dhts.map((d) => [d.components.peerId, d])) + const ids = [...dhtsById.keys()] + + // The origin node for the FIND_PEER query + const originNode = dhts[0] + + // The key + const val = uint8ArrayFromString('foobar') + + // Hash the key into the DHT's key format + const rtval = await kadUtils.convertBuffer(val) + // Make connections between nodes close to each other + const sorted = await sortClosestPeers(ids, rtval) + + const conns = [] + const maxRightIndex = sorted.length - 1 + for (let i = 0; i < sorted.length; i++) { + // Connect to 5 nodes on either side (10 in total) + for (const distance of [1, 3, 11, 31, 63]) { + let rightIndex = i + distance + if (rightIndex > maxRightIndex) { + rightIndex = maxRightIndex * 2 - (rightIndex + 1) + } + let leftIndex = i - distance + if (leftIndex < 0) { + leftIndex = 1 - leftIndex + } + conns.push([sorted[leftIndex], sorted[rightIndex]]) + } + } + + await Promise.all(conns.map(async (conn) => { + const dhtA = dhtsById.get(conn[0]) + const dhtB = dhtsById.get(conn[1]) + + if (dhtA == null || dhtB == null) { + throw new Error('Could not find DHT') + } + + await tdht.connect(dhtA, dhtB) + })) + + // Get the alpha (3) closest peers to the key from the origin's + // routing table + const rtablePeers = originNode.lan.routingTable.closestPeers(rtval, c.ALPHA) + expect(rtablePeers).to.have.length(c.ALPHA) + + // The set of peers used to initiate the query (the closest alpha + // peers to the key that the origin knows about) + const rtableSet: Record = {} + rtablePeers.forEach((p) => { + rtableSet[p.toString()] = true + }) + + const originNodeIndex = ids.findIndex(i => uint8ArrayEquals(i.multihash.bytes, originNode.components.peerId.multihash.bytes)) + const otherIds = ids.slice(0, originNodeIndex).concat(ids.slice(originNodeIndex + 1)) + + // Make the query + const out = await pipe( + originNode.getClosestPeers(val), + source => filter(source, (event) => event.type === EventTypes.FINAL_PEER), + // @ts-expect-error tsc has problems with filtering + source => map(source, (event) => event.peer.id), + async source => all(source) + ) + + const actualClosest = await sortClosestPeers(otherIds, rtval) + + // Expect that the response includes nodes that are were not + // already in the origin's routing table (ie it went out to + // the network to find closer peers) + expect(out.filter((p) => !rtableSet[p.toString()])) + .to.not.be.empty() + + // The expected closest kValue peers to the key + const exp = actualClosest.slice(0, c.K) + + // Expect the kValue peers found to include the kValue closest connected peers + // to the key + expect(countDiffPeers(out, exp)).to.equal(0) + }) + + it('getClosestPeers', async function () { + this.timeout(240 * 1000) + + const nDHTs = 30 + const dhts = await Promise.all( + new Array(nDHTs).fill(0).map(async () => tdht.spawn()) + ) + + const connected: Array> = [] + + for (let i = 0; i < dhts.length - 1; i++) { + connected.push(tdht.connect(dhts[i], dhts[(i + 1) % dhts.length])) + } + + await Promise.all(connected) + + const res = await all(filter(dhts[1].getClosestPeers(uint8ArrayFromString('foo')), event => event.name === 'FINAL_PEER')) + + expect(res).to.not.be.empty() + }) + }) + + describe('errors', () => { + it('get should fail if only has one peer', async function () { + this.timeout(240 * 1000) + + const dht = await tdht.spawn() + + await delay(100) + + await expect(all(dht.get(uint8ArrayFromString('/v/hello')))).to.eventually.be.rejected().property('code', 'ERR_NO_PEERS_IN_ROUTING_TABLE') + }) + + it('get should handle correctly an unexpected error', async function () { + this.timeout(240 * 1000) + + const errCode = 'ERR_INVALID_RECORD_FAKE' + const error = new CodeError('fake error', errCode) + + const [dhtA, dhtB] = await Promise.all([ + tdht.spawn(), + tdht.spawn() + ]) + + await tdht.connect(dhtA, dhtB) + + const stub = sinon.stub(dhtA.components.connectionManager, 'openConnection').rejects(error) + + const errors = await all(filter(dhtA.get(uint8ArrayFromString('/v/hello')), event => event.name === 'QUERY_ERROR')) + + expect(errors).to.have.lengthOf(2) + expect(errors).to.have.nested.property('[0].error.code', errCode) + expect(errors).to.have.nested.property('[1].error.code', 'ERR_NOT_FOUND') + + stub.restore() + }) + + it('findPeer should fail if no closest peers available', async function () { + this.timeout(240 * 1000) + + const dhts = await Promise.all([ + tdht.spawn(), + tdht.spawn(), + tdht.spawn(), + tdht.spawn() + ]) + + const ids = dhts.map((d) => d.components.peerId) + await Promise.all([ + tdht.connect(dhts[0], dhts[1]), + tdht.connect(dhts[1], dhts[2]), + tdht.connect(dhts[2], dhts[3]) + ]) + + dhts[0].lan.findPeer = sinon.stub().returns([]) + dhts[0].wan.findPeer = sinon.stub().returns([]) + + await expect(drain(dhts[0].findPeer(ids[3]))).to.eventually.be.rejected().property('code', 'ERR_LOOKUP_FAILED') + }) + }) +}) diff --git a/packages/kad-dht/test/kad-utils.spec.ts b/packages/kad-dht/test/kad-utils.spec.ts new file mode 100644 index 0000000000..2d99cc4ebf --- /dev/null +++ b/packages/kad-dht/test/kad-utils.spec.ts @@ -0,0 +1,98 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as utils from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' + +describe('kad utils', () => { + describe('bufferToKey', () => { + it('returns the base32 encoded key of the buffer', () => { + const buf = uint8ArrayFromString('hello world') + + const key = utils.bufferToKey(buf) + + expect(key.toString()) + .to.equal('/' + uint8ArrayToString(buf, 'base32')) + }) + }) + + describe('bufferToRecordKey', () => { + it('returns the base32 encoded key of the buffer with the record prefix', () => { + const buf = uint8ArrayFromString('hello world') + + const key = utils.bufferToRecordKey(buf) + + expect(key.toString()) + .to.equal('/dht/record/' + uint8ArrayToString(buf, 'base32')) + }) + }) + + describe('convertBuffer', () => { + it('returns the sha2-256 hash of the buffer', async () => { + const buf = uint8ArrayFromString('hello world') + const digest = await utils.convertBuffer(buf) + + expect(digest) + .to.equalBytes(uint8ArrayFromString('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', 'base16')) + }) + }) + + describe('keyForPublicKey', () => { + it('works', async () => { + const peer = await createPeerId() + expect(utils.keyForPublicKey(peer)) + .to.eql(uint8ArrayConcat([uint8ArrayFromString('/pk/'), peer.multihash.bytes])) + }) + }) + + describe('fromPublicKeyKey', () => { + it('round trips', async function () { + this.timeout(40 * 1000) + + const peers = await createPeerIds(50) + peers.forEach((id, i) => { + expect(utils.isPublicKeyKey(utils.keyForPublicKey(id))).to.eql(true) + expect(utils.fromPublicKeyKey(utils.keyForPublicKey(id)).multihash.bytes) + .to.eql(id.multihash.bytes) + }) + }) + }) + + describe('removePrivateAddresses', () => { + it('filters private multiaddrs', async () => { + const id = await createPeerId() + + const multiaddrs = [ + multiaddr('/dns4/example.com/tcp/4001'), + multiaddr('/ip4/192.168.0.1/tcp/4001'), + multiaddr('/ip4/1.1.1.1/tcp/4001'), + multiaddr('/dns4/localhost/tcp/4001') + ] + + const peerInfo = utils.removePrivateAddresses({ id, multiaddrs, protocols: [] }) + expect(peerInfo.multiaddrs.map((ma) => ma.toString())) + .to.eql(['/dns4/example.com/tcp/4001', '/ip4/1.1.1.1/tcp/4001']) + }) + }) + + describe('removePublicAddresses', () => { + it('filters public multiaddrs', async () => { + const id = await createPeerId() + + const multiaddrs = [ + multiaddr('/dns4/example.com/tcp/4001'), + multiaddr('/ip4/192.168.0.1/tcp/4001'), + multiaddr('/ip4/1.1.1.1/tcp/4001'), + multiaddr('/dns4/localhost/tcp/4001') + ] + + const peerInfo = utils.removePublicAddresses({ id, multiaddrs, protocols: [] }) + expect(peerInfo.multiaddrs.map((ma) => ma.toString())) + .to.eql(['/ip4/192.168.0.1/tcp/4001', '/dns4/localhost/tcp/4001']) + }) + }) +}) diff --git a/packages/kad-dht/test/message.node.ts b/packages/kad-dht/test/message.node.ts new file mode 100644 index 0000000000..7154664498 --- /dev/null +++ b/packages/kad-dht/test/message.node.ts @@ -0,0 +1,31 @@ +/* eslint-env mocha */ + +import fs from 'fs' +import path from 'path' +import { isPeerId } from '@libp2p/interface-peer-id' +import { expect } from 'aegir/chai' +import range from 'lodash.range' +import { Message } from '../src/message/index.js' + +describe('Message', () => { + it('go-interop', () => { + range(1, 9).forEach((i) => { + const raw = fs.readFileSync( + path.join(process.cwd(), 'test', 'fixtures', `msg-${i}`) + ) + + const msg = Message.deserialize(raw) + + expect(msg.clusterLevel).to.gte(0) + if (msg.record != null) { + expect(msg.record.key).to.be.a('Uint8Array') + } + + if (msg.providerPeers.length > 0) { + msg.providerPeers.forEach((p) => { + expect(isPeerId(p.id)).to.be.true() + }) + } + }) + }) +}) diff --git a/packages/kad-dht/test/message.spec.ts b/packages/kad-dht/test/message.spec.ts new file mode 100644 index 0000000000..64cd22bc34 --- /dev/null +++ b/packages/kad-dht/test/message.spec.ts @@ -0,0 +1,86 @@ +/* eslint-env mocha */ + +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { Libp2pRecord } from '@libp2p/record' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import random from 'lodash.random' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../src/message/index.js' + +describe('Message', () => { + it('create', () => { + const k = uint8ArrayFromString('hello') + const msg = new Message(MESSAGE_TYPE.PING, k, 5) + + expect(msg).to.have.property('type', 'PING') + expect(msg).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(msg).to.have.property('clusterLevelRaw', 5) + expect(msg).to.have.property('clusterLevel', 4) + }) + + it('serialize & deserialize', async function () { + this.timeout(10 * 1000) + + const peers = await Promise.all( + Array.from({ length: 5 }).map(async () => createEd25519PeerId())) + + const closer = peers.slice(0, 5).map((p) => ({ + id: p, + multiaddrs: [ + multiaddr(`/ip4/198.176.1.${random(198)}/tcp/1234`), + multiaddr(`/ip4/100.176.1.${random(198)}`) + ], + protocols: [] + })) + + const provider = peers.slice(0, 5).map((p) => ({ + id: p, + multiaddrs: [ + multiaddr(`/ip4/98.176.1.${random(198)}/tcp/1234`), + multiaddr(`/ip4/10.176.1.${random(198)}`) + ], + protocols: [] + })) + + const msg = new Message(MESSAGE_TYPE.GET_VALUE, uint8ArrayFromString('hello'), 5) + const record = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), new Date()) + + msg.closerPeers = closer + msg.providerPeers = provider + msg.record = record + + const enc = msg.serialize() + const dec = Message.deserialize(enc) + + expect(dec.type).to.be.eql(msg.type) + expect(dec.key).to.be.eql(msg.key) + expect(dec.clusterLevel).to.be.eql(msg.clusterLevel) + + if (dec.record == null) { + throw new Error('No record found') + } + + expect(dec.record.serialize()).to.be.eql(record.serialize()) + expect(dec.record.key).to.eql(uint8ArrayFromString('hello')) + + expect(dec.closerPeers).to.have.length(5) + dec.closerPeers.forEach((peer, i) => { + expect(peer.id.equals(msg.closerPeers[i].id)).to.eql(true) + expect(peer.multiaddrs).to.eql(msg.closerPeers[i].multiaddrs) + }) + + expect(dec.providerPeers).to.have.length(5) + dec.providerPeers.forEach((peer, i) => { + expect(peer.id.equals(msg.providerPeers[i].id)).to.equal(true) + expect(peer.multiaddrs).to.eql(msg.providerPeers[i].multiaddrs) + }) + }) + + it('clusterlevel', () => { + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + msg.clusterLevel = 10 + expect(msg.clusterLevel).to.eql(9) + }) +}) diff --git a/packages/kad-dht/test/multiple-nodes.spec.ts b/packages/kad-dht/test/multiple-nodes.spec.ts new file mode 100644 index 0000000000..24ecde0dd7 --- /dev/null +++ b/packages/kad-dht/test/multiple-nodes.spec.ts @@ -0,0 +1,108 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import drain from 'it-drain' +import last from 'it-last' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' + +describe('multiple nodes', function () { + this.timeout(60 * 1000) + const n = 8 + let tdht: TestDHT + let dhts: DefaultDualKadDHT[] + + // spawn nodes + beforeEach(async function () { + tdht = new TestDHT() + dhts = await Promise.all( + new Array(n).fill(0).map(async () => tdht.spawn({ + clientMode: false + })) + ) + + // all nodes except the last one + const range = Array.from(Array(n - 1).keys()) + + // connect the last one with the others one by one + return Promise.all(range.map(async (i) => { await tdht.connect(dhts[n - 1], dhts[i]) })) + }) + + afterEach(async function () { + await tdht.teardown() + }) + + it('put to "bootstrap" node and get with the others', async function () { + const key = uint8ArrayFromString('/v/hello0') + const value = uint8ArrayFromString('world') + + await drain(dhts[7].put(key, value)) + + const res = await Promise.all([ + last(dhts[0].get(key)), + last(dhts[1].get(key)), + last(dhts[2].get(key)), + last(dhts[3].get(key)), + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[1]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[2]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[3]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[4]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[5]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[6]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + }) + + it('put to a node and get with the others', async function () { + const key = uint8ArrayFromString('/v/hello1') + const value = uint8ArrayFromString('world') + + await drain(dhts[1].put(key, value)) + + const res = await Promise.all([ + last(dhts[0].get(key)), + last(dhts[2].get(key)), + last(dhts[3].get(key)), + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)), + last(dhts[7].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[1]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[2]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[3]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[4]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[5]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + expect(res[6]).have.property('value').that.equalBytes(uint8ArrayFromString('world')) + }) + + it('put to several nodes in series with different values and get the last one in a subset of them', async function () { + const key = uint8ArrayFromString('/v/hallo') + const result = uint8ArrayFromString('world4') + + await drain(dhts[0].put(key, uint8ArrayFromString('world0'))) + await drain(dhts[1].put(key, uint8ArrayFromString('world1'))) + await drain(dhts[2].put(key, uint8ArrayFromString('world2'))) + await drain(dhts[3].put(key, uint8ArrayFromString('world3'))) + await drain(dhts[4].put(key, uint8ArrayFromString('world4'))) + + const res = await Promise.all([ + last(dhts[4].get(key)), + last(dhts[5].get(key)), + last(dhts[6].get(key)), + last(dhts[7].get(key)) + ]) + + expect(res[0]).have.property('value').that.equalBytes(result) + expect(res[1]).have.property('value').that.equalBytes(result) + expect(res[2]).have.property('value').that.equalBytes(result) + expect(res[3]).have.property('value').that.equalBytes(result) + }) +}) diff --git a/packages/kad-dht/test/network.spec.ts b/packages/kad-dht/test/network.spec.ts new file mode 100644 index 0000000000..6ab435fadc --- /dev/null +++ b/packages/kad-dht/test/network.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { mockStream } from '@libp2p/interface-mocks' +import { expect } from 'aegir/chai' +import all from 'it-all' +import * as lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../src/message/index.js' +import { TestDHT } from './utils/test-dht.js' +import type { DefaultDualKadDHT } from '../src/dual-kad-dht.js' +import type { Connection } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Sink, Source } from 'it-stream-types' + +describe('Network', () => { + let dht: DefaultDualKadDHT + let tdht: TestDHT + + before(async function () { + this.timeout(10 * 1000) + tdht = new TestDHT() + dht = await tdht.spawn({ + clientMode: false + }) + }) + + after(async () => { await tdht.teardown() }) + + describe('sendRequest', () => { + it('send and response echo', async () => { + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + const events = await all(dht.lan.network.sendRequest(dht.components.peerId, msg)) + const response = events + .filter(event => event.name === 'PEER_RESPONSE') + .pop() + expect(response).to.have.property('messageType', MESSAGE_TYPE.PING) + }) + + it('send and response different messages', async () => { + const defer = pDefer() + let i = 0 + const finish = (): void => { + if (i++ === 1) { + defer.resolve() + } + } + + const msg = new Message(MESSAGE_TYPE.PING, uint8ArrayFromString('hello'), 0) + + // mock it + dht.components.connectionManager.openConnection = async (peer: PeerId | Multiaddr | Multiaddr[]) => { + // @ts-expect-error incomplete implementation + const connection: Connection = { + newStream: async (protocols: string | string[]) => { + const protocol = Array.isArray(protocols) ? protocols[0] : protocols + const msg = new Message(MESSAGE_TYPE.FIND_NODE, uint8ArrayFromString('world'), 0) + + const data = await pipe( + [msg.serialize()], + (source) => lp.encode(source), + source => map(source, arr => new Uint8ArrayList(arr)), + async (source) => all(source) + ) + + const source = (async function * () { + const array = data + + yield * array + })() + + const sink: Sink, Promise> = async source => { + const res = await pipe( + source, + (source) => lp.decode(source), + async (source) => all(source) + ) + expect(Message.deserialize(res[0]).type).to.eql(MESSAGE_TYPE.PING) + finish() + } + + const stream = mockStream({ source, sink }) + + return { + ...stream, + stat: { + ...stream.stat, + protocol + } + } + } + } + + return connection + } + + const events = await all(dht.lan.network.sendRequest(dht.components.peerId, msg)) + const response = events + .filter(event => event.name === 'PEER_RESPONSE') + .pop() + + expect(response).to.have.property('messageType', MESSAGE_TYPE.FIND_NODE) + finish() + + return defer.promise + }) + }) +}) diff --git a/packages/kad-dht/test/peer-distance-list.spec.ts b/packages/kad-dht/test/peer-distance-list.spec.ts new file mode 100644 index 0000000000..06bc6ece8f --- /dev/null +++ b/packages/kad-dht/test/peer-distance-list.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { peerIdFromString } from '@libp2p/peer-id' +import { expect } from 'aegir/chai' +import { PeerDistanceList } from '../src/peer-list/peer-distance-list.js' +import * as kadUtils from '../src/utils.js' + +describe('PeerDistanceList', () => { + const p1 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1') + const p2 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2') + const p3 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE3') + const p4 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE4') + const p5 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1') + const p6 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE5') + const p7 = peerIdFromString('12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2') + + let key: Uint8Array + before(async () => { + key = await kadUtils.convertPeerId(p1) + }) + + describe('basics', () => { + it('add', async () => { + const pdl = new PeerDistanceList(key, 100) + + await pdl.add(p3) + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p4) + await pdl.add(p5) + await pdl.add(p1) + + // Note: p1 and p5 are equal + expect(pdl.length).to.eql(4) + expect(pdl.peers).to.be.eql([p1, p4, p3, p2]) + }) + + it('capacity', async () => { + const pdl = new PeerDistanceList(key, 3) + + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p3) + await pdl.add(p4) + await pdl.add(p5) + await pdl.add(p6) + + // Note: p1 and p5 are equal + expect(pdl.length).to.eql(3) + + // Closer peers added later should replace further + // peers added earlier + expect(pdl.peers).to.be.eql([p1, p4, p3]) + }) + }) + + describe('closer', () => { + let pdl: PeerDistanceList + + before(async () => { + pdl = new PeerDistanceList(key, 100) + + await pdl.add(p1) + await pdl.add(p2) + await pdl.add(p3) + await pdl.add(p4) + }) + + it('single closer peer', async () => { + const closer = await pdl.anyCloser([p6]) + + expect(closer).to.be.eql(true) + }) + + it('single further peer', async () => { + const closer = await pdl.anyCloser([p7]) + + expect(closer).to.be.eql(false) + }) + + it('closer and further peer', async () => { + const closer = await pdl.anyCloser([p6, p7]) + + expect(closer).to.be.eql(true) + }) + + it('single peer equal to furthest in list', async () => { + const closer = await pdl.anyCloser([p2]) + + expect(closer).to.be.eql(false) + }) + + it('no peers', async () => { + const closer = await pdl.anyCloser([]) + + expect(closer).to.be.eql(false) + }) + + it('empty peer distance list', async () => { + const pdl = new PeerDistanceList(key, 100) + const closer = await pdl.anyCloser([p1]) + + expect(closer).to.be.eql(true) + }) + + it('empty peer distance list and no peers', async () => { + const pdl = new PeerDistanceList(key, 100) + const closer = await pdl.anyCloser([]) + + expect(closer).to.be.eql(false) + }) + }) +}) diff --git a/packages/kad-dht/test/peer-list.spec.ts b/packages/kad-dht/test/peer-list.spec.ts new file mode 100644 index 0000000000..f2ebd2fecf --- /dev/null +++ b/packages/kad-dht/test/peer-list.spec.ts @@ -0,0 +1,26 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { PeerList } from '../src/peer-list/index.js' +import { createPeerIds } from './utils/create-peer-id.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('PeerList', () => { + let peers: PeerId[] + + before(async () => { + peers = await createPeerIds(3) + }) + + it('basics', () => { + const l = new PeerList() + + expect(l.push(peers[0])).to.eql(true) + expect(l.push(peers[0])).to.eql(false) + expect(l).to.have.length(1) + expect(l.push(peers[1])).to.eql(true) + expect(l.pop()).to.eql(peers[1]) + expect(l).to.have.length(1) + expect(l.toArray()).to.eql([peers[0]]) + }) +}) diff --git a/packages/kad-dht/test/providers.node.ts b/packages/kad-dht/test/providers.node.ts new file mode 100644 index 0000000000..934142165f --- /dev/null +++ b/packages/kad-dht/test/providers.node.ts @@ -0,0 +1,63 @@ +/* eslint-env mocha */ + +import os from 'os' +import path from 'path' +import { MemoryDatastore } from 'datastore-core/memory' +import { LevelDatastore } from 'datastore-level' +import { Providers } from '../src/providers.js' +import { createPeerIds } from './utils/create-peer-id.js' +import { createValues } from './utils/create-values.js' + +describe('Providers', () => { + let providers: Providers + + before(async function () { + this.timeout(10 * 1000) + }) + + afterEach(async () => { + await providers?.stop() + }) + + // slooow so only run when you need to + it.skip('many', async function () { + const p = path.join( + os.tmpdir(), (Math.random() * 100).toString() + ) + const store = new LevelDatastore(p) + await store.open() + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cacheSize: 10 + }) + + console.log('starting') // eslint-disable-line no-console + const [createdValues, createdPeers] = await Promise.all([ + createValues(100), + createPeerIds(600) + ]) + + console.log('got values and peers') // eslint-disable-line no-console + const total = Date.now() + + for (const v of createdValues) { + for (const p of createdPeers) { + await providers.addProvider(v.cid, p) + } + } + + console.log('addProvider %s peers %s cids in %sms', createdPeers.length, createdValues.length, Date.now() - total) // eslint-disable-line no-console + console.log('starting profile with %s peers and %s cids', createdPeers.length, createdValues.length) // eslint-disable-line no-console + + for (let i = 0; i < 3; i++) { + const start = Date.now() + for (const v of createdValues) { + await providers.getProviders(v.cid) + console.log('query %sms', (Date.now() - start)) // eslint-disable-line no-console + } + } + + await store.close() + }) +}) diff --git a/packages/kad-dht/test/providers.spec.ts b/packages/kad-dht/test/providers.spec.ts new file mode 100644 index 0000000000..84051bf8e0 --- /dev/null +++ b/packages/kad-dht/test/providers.spec.ts @@ -0,0 +1,113 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import { CID } from 'multiformats/cid' +import { sha256 } from 'multiformats/hashes/sha2' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Providers } from '../src/providers.js' +import { createPeerIds } from './utils/create-peer-id.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('Providers', () => { + let peers: PeerId[] + let providers: Providers + + before(async function () { + this.timeout(10 * 1000) + peers = await createPeerIds(3) + }) + + afterEach(async () => { + await providers?.stop() + }) + + it('simple add and get of providers', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }) + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + const ids = new Set(provs.map((peerId) => peerId.toString())) + expect(ids.has(peers[0].toString())).to.be.eql(true) + }) + + it('duplicate add of provider is deduped', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }) + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]), + providers.addProvider(cid, peers[1]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + expect(provs).to.have.length(2) + const ids = new Set(provs.map((peerId) => peerId.toString())) + expect(ids.has(peers[0].toString())).to.be.eql(true) + }) + + it('more providers than space in the lru cache', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cacheSize: 10 + }) + + const hashes = await Promise.all([...new Array(100)].map(async (i: number) => { + return sha256.digest(uint8ArrayFromString(`hello ${i}`)) + })) + + const cids = hashes.map((h) => CID.createV0(h)) + + await Promise.all(cids.map(async cid => { await providers.addProvider(cid, peers[0]) })) + const provs = await Promise.all(cids.map(async cid => providers.getProviders(cid))) + + expect(provs).to.have.length(100) + for (const p of provs) { + expect(p[0].toString()).to.be.equal(peers[0].toString()) + } + }) + + it('expires', async () => { + providers = new Providers({ + datastore: new MemoryDatastore() + }, { + cleanupInterval: 100, + provideValidity: 200 + }) + await providers.start() + + const cid = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + await Promise.all([ + providers.addProvider(cid, peers[0]), + providers.addProvider(cid, peers[1]) + ]) + + const provs = await providers.getProviders(cid) + + expect(provs).to.have.length(2) + expect(provs[0].toString()).to.be.equal(peers[0].toString()) + expect(provs[1].toString()).to.be.deep.equal(peers[1].toString()) + + await delay(400) + + const provsAfter = await providers.getProviders(cid) + expect(provsAfter).to.have.length(0) + await providers.stop() + }) +}) diff --git a/packages/kad-dht/test/query-self.spec.ts b/packages/kad-dht/test/query-self.spec.ts new file mode 100644 index 0000000000..2220419810 --- /dev/null +++ b/packages/kad-dht/test/query-self.spec.ts @@ -0,0 +1,128 @@ +/* eslint-env mocha */ + +import { CustomEvent } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import pDefer from 'p-defer' +import { stubInterface, type StubbedInstance } from 'ts-sinon' +import { finalPeerEvent } from '../src/query/events.js' +import { QuerySelf } from '../src/query-self.js' +import type { PeerRouting } from '../src/peer-routing/index.js' +import type { RoutingTable } from '../src/routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { DeferredPromise } from 'p-defer' + +describe('Query Self', () => { + let peerId: PeerId + let querySelf: QuerySelf + let peerRouting: StubbedInstance + let routingTable: StubbedInstance + let initialQuerySelfHasRun: DeferredPromise + + beforeEach(async () => { + peerId = await createEd25519PeerId() + initialQuerySelfHasRun = pDefer() + routingTable = stubInterface() + peerRouting = stubInterface() + + const components = { + peerId + } + + const init = { + lan: false, + peerRouting, + routingTable, + initialQuerySelfHasRun + } + + querySelf = new QuerySelf(components, init) + }) + + afterEach(() => { + if (querySelf != null) { + querySelf.stop() + } + }) + + it('should not run if not started', async () => { + await querySelf.querySelf() + + expect(peerRouting.getClosestPeers).to.have.property('callCount', 0) + }) + + it('should wait for routing table peers before running first query', async () => { + querySelf.start() + + // @ts-expect-error read-only property + routingTable.size = 0 + + const querySelfPromise = querySelf.querySelf() + const remotePeer = await createEd25519PeerId() + + let initialQuerySelfHasRunResolved = false + + void initialQuerySelfHasRun.promise.then(() => { + initialQuerySelfHasRunResolved = true + }) + + // should have registered a peer:add listener + // @ts-expect-error ts-sinon makes every property access a function and p-event checks this one first + expect(routingTable.on).to.have.property('callCount', 2) + // @ts-expect-error ts-sinon makes every property access a function and p-event checks this one first + expect(routingTable.on.getCall(0)).to.have.nested.property('args[0]', 'peer:add') + + // self query results + peerRouting.getClosestPeers.withArgs(peerId.toBytes()).returns(async function * () { + yield finalPeerEvent({ + from: remotePeer, + peer: { + id: remotePeer, + multiaddrs: [], + protocols: [] + } + }) + }()) + + // @ts-expect-error args[1] type could be an object + routingTable.on.getCall(0).args[1](new CustomEvent('peer:add', { detail: remotePeer })) + + // self-query should complete + await querySelfPromise + + // should have resolved initial query self promise + expect(initialQuerySelfHasRunResolved).to.be.true() + }) + + it('should join an existing query promise and not run twise', async () => { + querySelf.start() + + // @ts-expect-error read-only property + routingTable.size = 0 + + const querySelfPromise1 = querySelf.querySelf() + const querySelfPromise2 = querySelf.querySelf() + const remotePeer = await createEd25519PeerId() + + // self query results + peerRouting.getClosestPeers.withArgs(peerId.toBytes()).returns(async function * () { + yield finalPeerEvent({ + from: remotePeer, + peer: { + id: remotePeer, + multiaddrs: [], + protocols: [] + } + }) + }()) + + // @ts-expect-error args[1] type could be an object + routingTable.on.getCall(0).args[1](new CustomEvent('peer:add', { detail: remotePeer })) + + // both self-query promises should resolve + await Promise.all([querySelfPromise1, querySelfPromise2]) + + // should only have made one query + expect(peerRouting.getClosestPeers).to.have.property('callCount', 1) + }) +}) diff --git a/packages/kad-dht/test/query.spec.ts b/packages/kad-dht/test/query.spec.ts new file mode 100644 index 0000000000..7ffb678f80 --- /dev/null +++ b/packages/kad-dht/test/query.spec.ts @@ -0,0 +1,851 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import drain from 'it-drain' +import pDefer from 'p-defer' +import { type StubbedInstance, stubInterface } from 'ts-sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { EventTypes, type QueryEvent } from '../src/index.js' +import { MESSAGE_TYPE } from '../src/message/index.js' +import { + peerResponseEvent, + valueEvent, + queryErrorEvent +} from '../src/query/events.js' +import { QueryManager, type QueryManagerInit } from '../src/query/manager.js' +import { convertBuffer } from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import type { QueryFunc } from '../src/query/types.js' +import type { RoutingTable } from '../src/routing-table/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +interface TopologyEntry { + delay?: number + error?: Error + value?: Uint8Array + closerPeers?: number[] + event: QueryEvent +} +type Topology = Record + +describe('QueryManager', () => { + let ourPeerId: PeerId + let peers: PeerId[] + let key: Uint8Array + let routingTable: StubbedInstance + + const defaultInit = (): QueryManagerInit => { + const init: QueryManagerInit = { + initialQuerySelfHasRun: pDefer(), + routingTable + } + + init.initialQuerySelfHasRun.resolve() + + return init + } + + function createTopology (opts: Record): Topology { + const topology: Record = {} + + Object.keys(opts).forEach(key => { + const id = parseInt(key) + const from = peers[id] + const config = opts[id] + + let event: QueryEvent + + if (config.value !== undefined) { + event = valueEvent({ from, value: config.value }) + } else if (config.error != null) { + event = queryErrorEvent({ from, error: config.error }) + } else { + event = peerResponseEvent({ + from, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: (config.closerPeers ?? []).map((id) => ({ + id: peers[id], + multiaddrs: [], + protocols: [] + })) + }) + } + + const entry: TopologyEntry = { + event + } + + if (config.delay != null) { + entry.delay = config.delay + } + + topology[from.toString()] = entry + }) + + return topology + } + + function createQueryFunction (topology: Record): QueryFunc { + const queryFunc: QueryFunc = async function * ({ peer }) { + const res = topology[peer.toString()] + + if (res.delay != null) { + await delay(res.delay) + } + + yield res.event + } + + return queryFunc + } + + before(async () => { + routingTable = stubInterface() + + const unsortedPeers = await createPeerIds(39) + ourPeerId = await createPeerId() + key = (await createPeerId()).toBytes() + + // sort remaining peers by XOR distance to the key, low -> high + peers = await sortClosestPeers(unsortedPeers, await convertBuffer(key)) + }) + + beforeEach(async () => { + routingTable.closestPeers.returns(peers) + }) + + it('does not run queries before start', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + + // @ts-expect-error not enough params + await expect(all(manager.run())).to.eventually.be.rejectedWith(/not started/) + }) + + it('does not run queries after stop', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + + await manager.start() + await manager.stop() + + // @ts-expect-error not enough params + await expect(all(manager.run())).to.eventually.be.rejectedWith(/not started/) + }) + + it('should pass query context', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * (context) { // eslint-disable-line require-await + expect(context).to.have.property('key').that.equalBytes(key) + expect(context).to.have.property('peer').that.deep.equals(peers[0]) + expect(context).to.have.property('signal').that.is.an.instanceOf(AbortSignal) + expect(context).to.have.property('pathIndex').that.equals(0) + expect(context).to.have.property('numPaths').that.equals(1) + + yield valueEvent({ + from: context.peer, + value: uint8ArrayFromString('cool') + }) + } + + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(1) + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + + await manager.stop() + }) + + it('simple run - succeed finding value', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const peersQueried = [] + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + expect(signal).to.be.an.instanceOf(AbortSignal) + peersQueried.push(peer) + + if (peersQueried.length === 1) { + // query more peers + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: peers.slice(0, 5).map(id => ({ id, multiaddrs: [], protocols: [] })) + }) + } else if (peersQueried.length === 6) { + // all peers queried, return result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + } else { + // a peer that cannot help in our query + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE + }) + } + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc)) + + // e.g. our starting peer plus the 5x closerPeers returned n the first iteration + expect(results).to.have.lengthOf(6) + + expect(results).to.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + // should be a result in there somewhere + + await manager.stop() + }) + + it('simple run - fail to find value', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const peersQueried = [] + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + peersQueried.push(peer) + + if (peersQueried.length === 1) { + // query more peers + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: peers.slice(0, 5).map(id => ({ id, multiaddrs: [], protocols: [] })) + }) + } else { + // a peer that cannot help in our query + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE + }) + } + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc)) + + // e.g. our starting peer plus the 5x closerPeers returned n the first iteration + expect(results).to.have.lengthOf(6) + // should not be a result in there + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + + await manager.stop() + }) + + it('should abort a query', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2, + alpha: 1 + }) + await manager.start() + + const controller = new AbortController() + let aborted + + // 0 -> 10 -> 11 -> 12... + // 1 -> 20 -> 21 -> 22... + const topology = createTopology({ + 0: { closerPeers: [10] }, + 10: { closerPeers: [11] }, + 11: { closerPeers: [12] }, + 1: { closerPeers: [20] }, + 20: { closerPeers: [21] }, + 21: { closerPeers: [22] } + }) + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + signal.addEventListener('abort', () => { + aborted = true + }) + + await delay(1000) + + yield topology[peer.toString()].event + } + + setTimeout(() => { + controller.abort() + }, 10) + + await expect(all(manager.run(key, queryFunc, { signal: controller.signal }))).to.eventually.be.rejected().with.property('code', 'ERR_QUERY_ABORTED') + + expect(aborted).to.be.true() + + await manager.stop() + }) + + it('should allow a sub-query to timeout without aborting the whole query', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2, + alpha: 2 + }) + await manager.start() + + // 2 -> 1 -> 0 + // 4 -> 3 -> 0 + const topology = createTopology({ + 0: { value: uint8ArrayFromString('true') }, + 1: { delay: 1000, closerPeers: [0] }, + 2: { delay: 1000, closerPeers: [1] }, + 3: { delay: 10, closerPeers: [0] }, + 4: { delay: 10, closerPeers: [3] } + }) + + const queryFunc: QueryFunc = async function * ({ peer, signal }) { // eslint-disable-line require-await + let aborted = false + + signal.addEventListener('abort', () => { + aborted = true + }) + + const res = topology[peer.toString()] + + if (res.delay != null) { + await delay(res.delay) + } + + if (aborted) { + throw new Error('Aborted by signal') + } + + yield res.event + } + + routingTable.closestPeers.returns([peers[2], peers[4]]) + const result = await all(manager.run(key, queryFunc, { queryFuncTimeout: 500 })) + + // should have traversed through the three nodes to the value and the one that timed out + expect(result).to.have.lengthOf(4) + expect(result).to.have.deep.nested.property('[2].value', uint8ArrayFromString('true')) + expect(result).to.have.nested.property('[3].error.message', 'Aborted by signal') + + await manager.stop() + }) + + it('does not return an error if only some queries error', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 10 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer, pathIndex }) { // eslint-disable-line require-await + if (pathIndex % 2 === 0) { + yield queryErrorEvent({ + from: peer, + error: new Error('Urk!') + }) + } else { + yield peerResponseEvent({ from: peer, messageType: MESSAGE_TYPE.GET_VALUE }) + } + } + + const results = await all(manager.run(key, queryFunc)) + + // didn't add any extra peers during the query + expect(results).to.have.lengthOf(manager.disjointPaths) + // should not be a result in there + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + // half of the results should have the error property + expect(results.reduce((acc, curr) => { + if (curr.name === 'QUERY_ERROR') { + return acc + 1 + } + + return acc + }, 0)).to.equal(5) + + await manager.stop() + }) + + it('returns empty run if initial peer list is empty', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 10 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + yield valueEvent({ from: peer, value: uint8ArrayFromString('cool') }) + } + + routingTable.closestPeers.returns([]) + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(0) + + await manager.stop() + }) + + it('should query closer peers first', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 9 -> 8 -> 7 -> 6 -> 5 -> 0 + // \-> 4 -> 3 -> 2 -> 1 -> 0 <-- should take this branch first + const topology = createTopology({ + 9: { closerPeers: [8, 4] }, + 8: { closerPeers: [7] }, + 7: { closerPeers: [6] }, + 6: { closerPeers: [5] }, + 5: { closerPeers: [0] }, + 4: { closerPeers: [3] }, + 3: { closerPeers: [2] }, + 2: { closerPeers: [1] }, + 1: { closerPeers: [0] }, + 0: { value: uint8ArrayFromString('hello world') } + }) + + routingTable.closestPeers.returns([peers[9]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + const traversedPeers = results + .map(event => { + if (event.type !== EventTypes.PEER_RESPONSE && event.type !== EventTypes.VALUE) { + throw new Error(`Unexpected query event type ${event.type}`) + } + + return event.from + }) + + expect(traversedPeers).to.deep.equal([ + peers[9], + peers[4], + peers[3], + peers[2], + peers[1], + peers[0], + peers[8], + peers[7], + peers[6], + peers[5] + ]) + + await manager.stop() + }) + + it('should stop when passing through the same node twice', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 20, + alpha: 1 + }) + await manager.start() + + const topology = createTopology({ + 6: { closerPeers: [2] }, + 5: { closerPeers: [4] }, + 4: { closerPeers: [3] }, + 3: { closerPeers: [2] }, + 2: { closerPeers: [1] }, + 1: { closerPeers: [0] }, + 0: { value: uint8ArrayFromString('hello world') } + }) + + routingTable.closestPeers.returns([peers[6], peers[5]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + const traversedPeers = results + .map(event => { + if (event.type !== EventTypes.PEER_RESPONSE && event.type !== EventTypes.VALUE) { + throw new Error(`Unexpected query event type ${event.type}`) + } + + return event.from + }) + + expect(traversedPeers).lengthOf(7) + + await manager.stop() + }) + + it('only closerPeers', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + yield peerResponseEvent({ + from: peer, + messageType: MESSAGE_TYPE.GET_VALUE, + closer: [{ + id: peers[2], + multiaddrs: [], + protocols: [] + }] + }) + } + + routingTable.closestPeers.returns([peers[3]]) + const results = await all(manager.run(key, queryFunc)) + + expect(results).to.have.lengthOf(2) + expect(results).to.have.deep.nested.property('[0].closer[0].id', peers[2]) + expect(results).to.have.deep.nested.property('[1].closer[0].id', peers[2]) + + await manager.stop() + }) + + it('only closerPeers concurrent', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 3 + }) + await manager.start() + + // 9 -> 2 + // 8 -> 6 -> 4 + // 5 -> 3 + // 7 -> 1 -> 0 + const topology = createTopology({ + 0: { closerPeers: [] }, + 1: { closerPeers: [0] }, + 2: { closerPeers: [] }, + 3: { closerPeers: [] }, + 4: { closerPeers: [] }, + 5: { closerPeers: [3] }, + 6: { closerPeers: [4, 5] }, + 7: { closerPeers: [1] }, + 8: { closerPeers: [6] }, + 9: { closerPeers: [2] } + }) + + routingTable.closestPeers.returns([peers[9], peers[8], peers[7]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // Should visit all peers + expect(results).to.have.lengthOf(10) + + await manager.stop() + }) + + it('queries stop after shutdown', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 3 -> 2 -> 1 -> 0 + const topology = createTopology({ + 0: { closerPeers: [] }, + // Should not reach here because query gets shut down + 1: { closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { closerPeers: [2] } + }) + + const visited: PeerId[] = [] + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + visited.push(peer) + + const getResult = async (): Promise => { + const res = topology[peer.toString()] + // this delay is necessary so `dhtA.stop` has time to stop the + // requests before they all complete + await delay(100) + + return res.event + } + + // Shut down after visiting peers[2] + if (peer === peers[2]) { + await manager.stop() + + yield getResult() + } + + yield getResult() + } + + // shutdown will cause the query to stop early but without an error + routingTable.closestPeers.returns([peers[3]]) + await drain(manager.run(key, queryFunc)) + + // Should only visit peers up to the point where we shut down + expect(visited).to.have.lengthOf(2) + expect(visited).to.deep.include(peers[3]) + expect(visited).to.deep.include(peers[2]) + }) + + it('disjoint path values', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2 + }) + await manager.start() + + const values = ['v0', 'v1'].map((str) => uint8ArrayFromString(str)) + + // 2 -> 1 -> 0 (v0) + // 4 -> 3 (v1) + const topology = createTopology({ + 0: { value: values[0] }, + // Top level node + 1: { closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { value: values[1] }, + 4: { closerPeers: [3] } + }) + + routingTable.closestPeers.returns([peers[2], peers[4]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // visited all the nodes + expect(results).to.have.lengthOf(5) + + // found both values + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: values[0] + }]) + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: values[1] + }]) + + await manager.stop() + }) + + it('disjoint path continue other paths after error on one path', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 2 + }) + await manager.start() + + // 2 -> 1 (delay) -> 0 [pathComplete] + // 5 -> 4 [error] -> 3 + const topology = createTopology({ + 0: { value: uint8ArrayFromString('true') }, + // This query has a delay which means it only returns after the other + // path has already returned an error + 1: { delay: 100, closerPeers: [0] }, + 2: { closerPeers: [1] }, + 3: { value: uint8ArrayFromString('false') }, + // Return an error at this point + 4: { closerPeers: [3], error: new Error('Nooo!') }, + 5: { closerPeers: [4] } + }) + + routingTable.closestPeers.returns([peers[2], peers[5]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // @ts-expect-error types are wrong + expect(results).to.deep.containSubset([{ + value: uint8ArrayFromString('true') + }]) + // @ts-expect-error types are wrong + expect(results).to.not.deep.containSubset([{ + value: uint8ArrayFromString('false') + }]) + + await manager.stop() + }) + + it('should allow the self-query query to run', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + initialQuerySelfHasRun: pDefer(), + routingTable + }) + await manager.start() + + const queryFunc: QueryFunc = async function * ({ peer }) { // eslint-disable-line require-await + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + } + + routingTable.closestPeers.returns([peers[7]]) + const results = await all(manager.run(key, queryFunc, { + // this bypasses awaiting on the initialQuerySelfHasRun deferred promise + isSelfQuery: true + })) + + // should have the result + expect(results).to.containSubset([{ + value: uint8ArrayFromString('cool') + }]) + + await manager.stop() + }) + + it('should wait for the self-query query to run before running other queries', async () => { + const initialQuerySelfHasRun = pDefer() + + const manager = new QueryManager({ + peerId: ourPeerId + }, { + initialQuerySelfHasRun, + alpha: 2, + routingTable + }) + await manager.start() + + let regularQueryTimeStarted: number = 0 + let selfQueryTimeStarted: number = Infinity + + routingTable.closestPeers.returns([peers[7]]) + + // run a regular query and the self query together + await Promise.all([ + all(manager.run(key, async function * ({ peer }) { // eslint-disable-line require-await + regularQueryTimeStarted = Date.now() + + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('cool') + }) + })), + all(manager.run(key, async function * ({ peer }) { // eslint-disable-line require-await + selfQueryTimeStarted = Date.now() + + // make sure we take enough time so that the `regularQuery` time diff is big enough to measure + await delay(100) + + // yield query result + yield valueEvent({ + from: peer, + value: uint8ArrayFromString('it me') + }) + + // normally done by the QuerySelf component + initialQuerySelfHasRun.resolve() + }, { + // this bypasses awaiting on the initialQuerySelfHasRun deferred promise + isSelfQuery: true + })) + ]) + + // should have started the regular query after the self query finished + expect(regularQueryTimeStarted).to.be.greaterThan(selfQueryTimeStarted) + + await manager.stop() + }) + + it('should end paths when they have no closer peers to those already queried', async () => { + const manager = new QueryManager({ + peerId: ourPeerId + }, { + ...defaultInit(), + disjointPaths: 1, + alpha: 1 + }) + await manager.start() + + // 3 -> 2 -> 1 -> 4 -> 5 -> 6 // should stop at 1 + const topology = createTopology({ + 1: { closerPeers: [4] }, + 2: { closerPeers: [1] }, + 3: { closerPeers: [2] }, + 4: { closerPeers: [5] }, + 5: { closerPeers: [6] }, + 6: {} + }) + + routingTable.closestPeers.returns([peers[3]]) + const results = await all(manager.run(key, createQueryFunction(topology))) + + // should not have a value + expect(results.find(res => res.name === 'VALUE')).to.not.be.ok() + + // should have traversed peers 3, 2 & 1 + expect(results).to.containSubset([{ + from: peers[3] + }, { + from: peers[2] + }, { + from: peers[1] + }]) + + // should not have traversed peers 4, 5 & 6 + expect(results).to.not.containSubset([{ + from: peers[4] + }, { + from: peers[5] + }, { + from: peers[6] + }]) + + await manager.stop() + }) +}) diff --git a/packages/kad-dht/test/routing-table.spec.ts b/packages/kad-dht/test/routing-table.spec.ts new file mode 100644 index 0000000000..5fafc29784 --- /dev/null +++ b/packages/kad-dht/test/routing-table.spec.ts @@ -0,0 +1,345 @@ +/* eslint-env mocha */ + +import { mockConnectionManager } from '@libp2p/interface-mocks' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { PeerSet } from '@libp2p/peer-collections' +import { peerIdFromString } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import all from 'it-all' +import { pipe } from 'it-pipe' +import random from 'lodash.random' +import { pEvent } from 'p-event' +import pWaitFor from 'p-wait-for' +import sinon from 'sinon' +import { stubInterface } from 'ts-sinon' +import { PROTOCOL_DHT } from '../src/constants.js' +import { KAD_CLOSE_TAG_NAME, KAD_CLOSE_TAG_VALUE, KBUCKET_SIZE, RoutingTable, type RoutingTableComponents } from '../src/routing-table/index.js' +import * as kadUtils from '../src/utils.js' +import { createPeerId, createPeerIds } from './utils/create-peer-id.js' +import { sortClosestPeers } from './utils/sort-closest-peers.js' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' + +describe('Routing Table', () => { + let table: RoutingTable + let components: RoutingTableComponents + + beforeEach(async function () { + this.timeout(20 * 1000) + + const events = new EventEmitter() + + components = { + peerId: await createPeerId(), + connectionManager: stubInterface(), + peerStore: stubInterface() + } + components.connectionManager = mockConnectionManager({ + ...components, + registrar: stubInterface(), + events + }) + components.peerStore = new PersistentPeerStore({ + ...components, + datastore: new MemoryDatastore(), + events + }) + + table = new RoutingTable(components, { + lan: false, + protocol: PROTOCOL_DHT + }) + await table.start() + }) + + afterEach(async () => { + await table.stop() + }) + + it('add', async function () { + this.timeout(20 * 1000) + + const ids = await createPeerIds(20) + + await Promise.all( + Array.from({ length: 1000 }).map(async () => { await table.add(ids[random(ids.length - 1)]) }) + ) + + await Promise.all( + Array.from({ length: 20 }).map(async () => { + const id = ids[random(ids.length - 1)] + const key = await kadUtils.convertPeerId(id) + + expect(table.closestPeers(key, 5).length) + .to.be.above(0) + }) + ) + }) + + it('emits peer:add event', async () => { + const id = await createEd25519PeerId() + const eventPromise = pEvent<'peer:add', CustomEvent>(table, 'peer:add') + + await table.add(id) + + const event = await eventPromise + expect(event.detail.toString()).to.equal(id.toString()) + }) + + it('remove', async function () { + this.timeout(20 * 1000) + + const peers = await createPeerIds(10) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const key = await kadUtils.convertPeerId(peers[2]) + expect(table.closestPeers(key, 10)).to.have.length(10) + + await table.remove(peers[5]) + expect(table.closestPeers(key, 10)).to.have.length(9) + expect(table.size).to.be.eql(9) + }) + + it('emits peer:remove event', async () => { + const id = await createEd25519PeerId() + const eventPromise = pEvent<'peer:remove', CustomEvent>(table, 'peer:remove') + + await table.add(id) + await table.remove(id) + + const event = await eventPromise + expect(event.detail.toString()).to.equal(id.toString()) + }) + + it('closestPeer', async function () { + this.timeout(10 * 1000) + + const peers = await createPeerIds(4) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const id = peers[2] + const key = await kadUtils.convertPeerId(id) + expect(table.closestPeer(key)).to.eql(id) + }) + + it('closestPeers', async function () { + this.timeout(20 * 1000) + + const peers = await createPeerIds(18) + await Promise.all(peers.map(async (peer) => { await table.add(peer) })) + + const key = await kadUtils.convertPeerId(peers[2]) + expect(table.closestPeers(key, 15)).to.have.length(15) + }) + + it('favours old peers that respond to pings', async () => { + let fn: (() => Promise) | undefined + + // execute queued functions immediately + // @ts-expect-error incomplete implementation + table.pingQueue = { + add: async (f: () => Promise) => { + fn = f + }, + clear: () => {} + } + + const peerIds = [ + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi5'), + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi6') + ] + + const oldPeer = { + id: peerIds[0].toBytes(), + peer: peerIds[0] + } + const newPeer = { + id: peerIds[1].toBytes(), + peer: peerIds[1] + } + + table._onPing(new CustomEvent('ping', { detail: { oldContacts: [oldPeer], newContact: newPeer } })) + + if (table.kb == null) { + throw new Error('kbucket not defined') + } + + // add the old peer + table.kb.add(oldPeer) + + // simulate connection succeeding + const newStreamStub = sinon.stub().withArgs(PROTOCOL_DHT).resolves({ close: sinon.stub() }) + const openConnectionStub = sinon.stub().withArgs(oldPeer.peer).resolves({ + newStream: newStreamStub + }) + components.connectionManager.openConnection = openConnectionStub + + if (fn == null) { + throw new Error('nothing added to queue') + } + + // perform the ping + await fn() + + expect(openConnectionStub.calledOnce).to.be.true() + expect(openConnectionStub.calledWith(oldPeer.peer)).to.be.true() + + expect(newStreamStub.callCount).to.equal(1) + expect(newStreamStub.calledWith(PROTOCOL_DHT)).to.be.true() + + // did not add the new peer + expect(table.kb.get(newPeer.id)).to.be.undefined() + + // kept the old peer + expect(table.kb.get(oldPeer.id)).to.not.be.undefined() + }) + + it('evicts oldest peer that does not respond to ping', async () => { + let fn: (() => Promise) | undefined + + // execute queued functions immediately + // @ts-expect-error incomplete implementation + table.pingQueue = { + add: async (f: () => Promise) => { + fn = f + }, + clear: () => {} + } + + const peerIds = [ + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi5'), + peerIdFromString('QmYobx1VAHP7Mi88LcDvLeQoWcc1Aa2rynYHpdEPBqHZi6') + ] + + const oldPeer = { + id: peerIds[0].toBytes(), + peer: peerIds[0] + } + const newPeer = { + id: peerIds[1].toBytes(), + peer: peerIds[1] + } + + table._onPing(new CustomEvent('ping', { detail: { oldContacts: [oldPeer], newContact: newPeer } })) + + if (table.kb == null) { + throw new Error('kbucket not defined') + } + + // add the old peer + table.kb.add(oldPeer) + + // libp2p fails to dial the old peer + const openConnectionStub = sinon.stub().withArgs(oldPeer.peer).rejects(new Error('Could not dial peer')) + components.connectionManager.openConnection = openConnectionStub + + if (fn == null) { + throw new Error('nothing added to queue') + } + + // perform the ping + await fn() + + expect(openConnectionStub.callCount).to.equal(1) + expect(openConnectionStub.calledWith(oldPeer.peer)).to.be.true() + + // added the new peer + expect(table.kb.get(newPeer.id)).to.not.be.undefined() + + // evicted the old peer + expect(table.kb.get(oldPeer.id)).to.be.undefined() + }) + + it('tags newly found kad-close peers', async () => { + const remotePeer = await createEd25519PeerId() + const tagPeerSpy = sinon.spy(components.peerStore, 'merge') + + await table.add(remotePeer) + + expect(tagPeerSpy.callCount).to.equal(0, 'did not debounce call to peerStore.tagPeer') + + await pWaitFor(() => { + return tagPeerSpy.callCount === 1 + }) + + expect(tagPeerSpy.callCount).to.equal(1, 'did not tag kad-close peer') + expect(tagPeerSpy.getCall(0).args[0].toString()).to.equal(remotePeer.toString()) + expect(tagPeerSpy.getCall(0).args[1].tags).to.deep.equal({ + [KAD_CLOSE_TAG_NAME]: { + value: KAD_CLOSE_TAG_VALUE + } + }) + }) + + it('removes tags from kad-close peers when closer peers are found', async () => { + async function getTaggedPeers (): Promise { + return new PeerSet(await pipe( + await components.peerStore.all(), + async function * (source) { + for await (const peer of source) { + const peerData = await components.peerStore.get(peer.id) + + if (peerData.tags.has(KAD_CLOSE_TAG_NAME)) { + yield peer.id + } + } + }, + async (source) => all(source) + )) + } + + const tagPeerSpy = sinon.spy(components.peerStore, 'merge') + const localNodeId = await kadUtils.convertPeerId(components.peerId) + const sortedPeerList = await sortClosestPeers( + await Promise.all( + new Array(KBUCKET_SIZE + 1).fill(0).map(async () => createEd25519PeerId()) + ), + localNodeId + ) + + // sort list furthest -> closest + sortedPeerList.reverse() + + // fill the table up to the first kbucket size + for (let i = 0; i < KBUCKET_SIZE; i++) { + await table.add(sortedPeerList[i]) + } + + // should have all added contacts in the root kbucket + expect(table.kb?.count()).to.equal(KBUCKET_SIZE, 'did not fill kbuckets') + expect(table.kb?.root.contacts).to.have.lengthOf(KBUCKET_SIZE, 'split root kbucket when we should not have') + expect(table.kb?.root.left).to.be.null('split root kbucket when we should not have') + expect(table.kb?.root.right).to.be.null('split root kbucket when we should not have') + + await pWaitFor(() => { + return tagPeerSpy.callCount === KBUCKET_SIZE + }) + + // make sure we tagged all of the peers as kad-close + const taggedPeers = await getTaggedPeers() + expect(taggedPeers.difference(new PeerSet(sortedPeerList.slice(0, sortedPeerList.length - 1)))).to.have.property('size', 0) + tagPeerSpy.resetHistory() + + // add a node that is closer than any added so far + await table.add(sortedPeerList[sortedPeerList.length - 1]) + + expect(table.kb?.count()).to.equal(KBUCKET_SIZE + 1, 'did not fill kbuckets') + expect(table.kb?.root.left).to.not.be.null('did not split root kbucket when we should have') + expect(table.kb?.root.right).to.not.be.null('did not split root kbucket when we should have') + + // wait for tag new peer and untag old peer + await pWaitFor(() => { + return tagPeerSpy.callCount === 2 + }) + + // should have updated list of tagged peers + const finalTaggedPeers = await getTaggedPeers() + expect(finalTaggedPeers.difference(new PeerSet(sortedPeerList.slice(1)))).to.have.property('size', 0) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts new file mode 100644 index 0000000000..3455ce5e70 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts @@ -0,0 +1,85 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { Providers } from '../../../src/providers.js' +import { AddProviderHandler } from '../../../src/rpc/handlers/add-provider.js' +import { createPeerIds } from '../../utils/create-peer-id.js' +import { createValues } from '../../utils/create-values.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { CID } from 'multiformats' + +describe('rpc - handlers - AddProvider', () => { + let peerIds: PeerId[] + let values: Array<{ cid: CID, value: Uint8Array }> + let handler: DHTMessageHandler + let providers: Providers + + before(async () => { + [peerIds, values] = await Promise.all([ + createPeerIds(3), + createValues(2) + ]) + }) + + beforeEach(async () => { + const datastore = new MemoryDatastore() + + providers = new Providers({ datastore }) + + handler = new AddProviderHandler({ + providers + }) + }) + + describe('invalid messages', () => { + const tests = [{ + message: new Message(MESSAGE_TYPE.ADD_PROVIDER, new Uint8Array(0), 0), + error: 'ERR_MISSING_KEY' + }, { + message: new Message(MESSAGE_TYPE.ADD_PROVIDER, uint8ArrayFromString('hello world'), 0), + error: 'ERR_INVALID_CID' + }] + + tests.forEach((t) => { + it(t.error.toString(), async () => { + try { + await handler.handle(peerIds[0], t.message) + } catch (err: any) { + expect(err).to.exist() + expect(err.code).to.equal(t.error) + return + } + throw new Error() + }) + }) + }) + + it('ignore providers that do not match the sender', async () => { + const cid = values[0].cid + const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, cid.bytes, 0) + + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/1234') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/2345') + + msg.providerPeers = [{ + id: peerIds[0], + multiaddrs: [ma1], + protocols: [] + }, { + id: peerIds[1], + multiaddrs: [ma2], + protocols: [] + }] + + await handler.handle(peerIds[0], msg) + + const provs = await providers.getProviders(cid) + expect(provs).to.have.length(1) + expect(provs[0].toString()).to.equal(peerIds[0].toString()) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts new file mode 100644 index 0000000000..98ec8e84b0 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts @@ -0,0 +1,166 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { stubInterface } from 'ts-sinon' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { FindNodeHandler } from '../../../src/rpc/handlers/find-node.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { StubbedInstance } from 'ts-sinon' + +const T = MESSAGE_TYPE.FIND_NODE + +describe('rpc - handlers - FindNode', () => { + let peerId: PeerId + let sourcePeer: PeerId + let targetPeer: PeerId + let handler: DHTMessageHandler + let peerRouting: SinonStubbedInstance + let addressManager: StubbedInstance + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + targetPeer = await createPeerId() + peerRouting = Sinon.createStubInstance(PeerRouting) + addressManager = stubInterface() + + handler = new FindNodeHandler({ + peerId, + addressManager + }, { + peerRouting, + lan: false + }) + }) + + it('returns self, if asked for self', async () => { + const msg = new Message(T, peerId.multihash.bytes, 0) + + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/4002/p2p/${peerId.toString()}`), + multiaddr(`/ip4/192.168.1.5/tcp/4002/p2p/${peerId.toString()}`), + multiaddr(`/ip4/221.4.67.0/tcp/4002/p2p/${peerId.toString()}`) + ]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(peerId) + }) + + it('returns closer peers', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs).to.not.be.empty() + }) + + it('handles no peers found', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline.resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + expect(response).to.have.property('closerPeers').that.is.empty() + }) + + it('returns only lan addresses', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + handler = new FindNodeHandler({ + peerId, + addressManager + }, { + peerRouting, + lan: true + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs.map(ma => ma.toString())).to.include('/ip4/192.168.1.5/tcp/4002') + expect(peer.multiaddrs.map(ma => ma.toString())).to.not.include('/ip4/221.4.67.0/tcp/4002') + }) + + it('returns only wan addresses', async () => { + const msg = new Message(T, targetPeer.multihash.bytes, 0) + + peerRouting.getCloserPeersOffline + .withArgs(targetPeer.multihash.bytes, sourcePeer) + .resolves([{ + id: targetPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/221.4.67.0/tcp/4002') + ], + protocols: [] + }]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.closerPeers).to.have.length(1) + const peer = response.closerPeers[0] + + expect(peer.id).to.be.eql(targetPeer) + expect(peer.multiaddrs.map(ma => ma.toString())).to.not.include('/ip4/192.168.1.5/tcp/4002') + expect(peer.multiaddrs.map(ma => ma.toString())).to.include('/ip4/221.4.67.0/tcp/4002') + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts new file mode 100644 index 0000000000..49feba7fcc --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts @@ -0,0 +1,112 @@ +/* eslint-env mocha */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { Providers } from '../../../src/providers.js' +import { GetProvidersHandler, type GetProvidersHandlerComponents } from '../../../src/rpc/handlers/get-providers.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import { createValues, type Value } from '../../utils/create-values.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' + +const T = MESSAGE_TYPE.GET_PROVIDERS + +describe('rpc - handlers - GetProviders', () => { + let peerId: PeerId + let sourcePeer: PeerId + let closerPeer: PeerId + let providerPeer: PeerId + let peerStore: PeerStore + let providers: SinonStubbedInstance + let peerRouting: SinonStubbedInstance + let handler: GetProvidersHandler + let values: Value[] + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + closerPeer = await createPeerId() + providerPeer = await createPeerId() + values = await createValues(1) + + peerRouting = Sinon.createStubInstance(PeerRouting) + providers = Sinon.createStubInstance(Providers) + peerStore = new PersistentPeerStore({ + peerId, + datastore: new MemoryDatastore(), + events: new EventEmitter() + }) + + const components: GetProvidersHandlerComponents = { + peerStore + } + + handler = new GetProvidersHandler(components, { + peerRouting, + providers, + lan: false + }) + }) + + it('errors with an invalid key ', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 0) + + await expect(handler.handle(sourcePeer, msg)).to.eventually.be.rejected().with.property('code', 'ERR_INVALID_CID') + }) + + it('responds with providers and closer peers', async () => { + const v = values[0] + const msg = new Message(T, v.cid.bytes, 0) + + const closer: PeerInfo[] = [{ + id: closerPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.2.6/tcp/4002'), + multiaddr('/ip4/21.31.57.23/tcp/4002') + ], + protocols: [] + }] + + const provider: PeerInfo[] = [{ + id: providerPeer, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.5/tcp/4002'), + multiaddr('/ip4/135.4.67.0/tcp/4002') + ], + protocols: [] + }] + + providers.getProviders.withArgs(v.cid).resolves([providerPeer]) + peerRouting.getCloserPeersOffline.withArgs(msg.key, sourcePeer).resolves(closer) + + await peerStore.merge(providerPeer, { + multiaddrs: provider[0].multiaddrs + }) + await peerStore.merge(closerPeer, { + multiaddrs: closer[0].multiaddrs + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.key).to.be.eql(v.cid.bytes) + expect(response.providerPeers).to.have.lengthOf(1) + expect(response.providerPeers[0].id.toString()).to.equal(provider[0].id.toString()) + expect(response.closerPeers).to.have.lengthOf(1) + expect(response.closerPeers[0].id.toString()).to.equal(closer[0].id.toString()) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts new file mode 100644 index 0000000000..b8cb3b7b6b --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts @@ -0,0 +1,148 @@ +/* eslint-env mocha */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import Sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PeerRouting } from '../../../src/peer-routing/index.js' +import { GetValueHandler, type GetValueHandlerComponents } from '../../../src/rpc/handlers/get-value.js' +import * as utils from '../../../src/utils.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' +import type { SinonStubbedInstance } from 'sinon' + +const T = MESSAGE_TYPE.GET_VALUE + +describe('rpc - handlers - GetValue', () => { + let peerId: PeerId + let sourcePeer: PeerId + let closerPeer: PeerId + let targetPeer: PeerId + let handler: GetValueHandler + let peerRouting: SinonStubbedInstance + let peerStore: PeerStore + let datastore: Datastore + + beforeEach(async () => { + peerId = await createPeerId() + sourcePeer = await createPeerId() + closerPeer = await createPeerId() + targetPeer = await createPeerId() + peerRouting = Sinon.createStubInstance(PeerRouting) + datastore = new MemoryDatastore() + peerStore = new PersistentPeerStore({ + peerId, + datastore, + events: new EventEmitter() + }) + + const components: GetValueHandlerComponents = { + datastore, + peerStore + } + + handler = new GetValueHandler(components, { + peerRouting + }) + }) + + it('errors when missing key', async () => { + const msg = new Message(T, new Uint8Array(0), 0) + + try { + await handler.handle(sourcePeer, msg) + } catch (err: any) { + expect(err.code).to.eql('ERR_INVALID_KEY') + return + } + + throw new Error('should error when missing key') + }) + + it('responds with a local value', async () => { + const key = uint8ArrayFromString('hello') + const value = uint8ArrayFromString('world') + const record = new Libp2pRecord(key, value, new Date()) + + await datastore.put(utils.bufferToRecordKey(key), record.serialize().subarray()) + + const msg = new Message(T, key, 0) + + peerRouting.getCloserPeersOffline.withArgs(msg.key, sourcePeer).resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.record).to.exist() + expect(response).to.have.nested.property('record.key').that.equalBytes(key) + expect(response).to.have.nested.property('record.value').that.equalBytes(value) + }) + + it('responds with closerPeers returned from the dht', async () => { + const key = uint8ArrayFromString('hello') + + peerRouting.getCloserPeersOffline.withArgs(key, sourcePeer) + .resolves([{ + id: closerPeer, + multiaddrs: [], + protocols: [] + }]) + + const msg = new Message(T, key, 0) + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response).to.have.nested.property('closerPeers[0].id').that.deep.equals(closerPeer) + }) + + describe('public key', () => { + it('peer in peerstore', async () => { + const key = utils.keyForPublicKey(targetPeer) + const msg = new Message(T, key, 0) + + if (targetPeer.publicKey == null) { + throw new Error('targetPeer had no public key') + } + + await peerStore.merge(targetPeer, { + publicKey: targetPeer.publicKey + }) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response).to.have.nested.property('record.value').that.equalBytes(targetPeer.publicKey) + }) + + it('peer not in peerstore', async () => { + const key = utils.keyForPublicKey(targetPeer) + const msg = new Message(T, key, 0) + + peerRouting.getCloserPeersOffline.resolves([]) + + const response = await handler.handle(sourcePeer, msg) + + if (response == null) { + throw new Error('No response received from handler') + } + + expect(response.record).to.not.be.ok() + }) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/ping.spec.ts b/packages/kad-dht/test/rpc/handlers/ping.spec.ts new file mode 100644 index 0000000000..0c696c5441 --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/ping.spec.ts @@ -0,0 +1,31 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PingHandler } from '../../../src/rpc/handlers/ping.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { DHTMessageHandler } from '../../../src/rpc/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const T = MESSAGE_TYPE.PING + +describe('rpc - handlers - Ping', () => { + let sourcePeer: PeerId + let handler: DHTMessageHandler + + beforeEach(async () => { + sourcePeer = await createPeerId() + }) + + beforeEach(async () => { + handler = new PingHandler() + }) + + it('replies with the same message', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 5) + const response = await handler.handle(sourcePeer, msg) + + expect(response).to.be.deep.equal(msg) + }) +}) diff --git a/packages/kad-dht/test/rpc/handlers/put-value.spec.ts b/packages/kad-dht/test/rpc/handlers/put-value.spec.ts new file mode 100644 index 0000000000..720b6308bf --- /dev/null +++ b/packages/kad-dht/test/rpc/handlers/put-value.spec.ts @@ -0,0 +1,80 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +import { Libp2pRecord } from '@libp2p/record' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import delay from 'delay' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../../src/message/index.js' +import { PutValueHandler } from '../../../src/rpc/handlers/put-value.js' +import * as utils from '../../../src/utils.js' +import { createPeerId } from '../../utils/create-peer-id.js' +import type { Validators } from '../../../src/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +const T = MESSAGE_TYPE.PUT_VALUE + +describe('rpc - handlers - PutValue', () => { + let sourcePeer: PeerId + let handler: PutValueHandler + let datastore: Datastore + let validators: Validators + + beforeEach(async () => { + sourcePeer = await createPeerId() + datastore = new MemoryDatastore() + validators = {} + + const components = { + datastore + } + + handler = new PutValueHandler(components, { + validators + }) + }) + + it('errors on missing record', async () => { + const msg = new Message(T, uint8ArrayFromString('hello'), 5) + + try { + await handler.handle(sourcePeer, msg) + } catch (err: any) { + expect(err.code).to.eql('ERR_EMPTY_RECORD') + return + } + + throw new Error('should error on missing record') + }) + + it('stores the record in the datastore', async () => { + const msg = new Message(T, uint8ArrayFromString('/val/hello'), 5) + const record = new Libp2pRecord( + uint8ArrayFromString('hello'), + uint8ArrayFromString('world'), + new Date() + ) + msg.record = record + validators.val = async () => {} + + const response = await handler.handle(sourcePeer, msg) + expect(response).to.deep.equal(msg) + + const key = utils.bufferToRecordKey(uint8ArrayFromString('hello')) + const res = await datastore.get(key) + + const rec = Libp2pRecord.deserialize(res) + + expect(rec).to.have.property('key').eql(uint8ArrayFromString('hello')) + + if (rec.timeReceived == null) { + throw new Error('Libp2pRecord timeReceived not set') + } + + // make sure some time has passed + await delay(10) + expect(rec.timeReceived.getTime()).to.be.lessThan(Date.now()) + }) +}) diff --git a/packages/kad-dht/test/rpc/index.node.ts b/packages/kad-dht/test/rpc/index.node.ts new file mode 100644 index 0000000000..a519b3f34f --- /dev/null +++ b/packages/kad-dht/test/rpc/index.node.ts @@ -0,0 +1,114 @@ +/* eslint-env mocha */ + +import { mockStream } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { start } from '@libp2p/interfaces/startable' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import all from 'it-all' +import * as lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import Sinon, { type SinonStubbedInstance } from 'sinon' +import { stubInterface } from 'ts-sinon' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Message, MESSAGE_TYPE } from '../../src/message/index.js' +import { PeerRouting } from '../../src/peer-routing/index.js' +import { Providers } from '../../src/providers.js' +import { RoutingTable } from '../../src/routing-table/index.js' +import { RPC, type RPCComponents } from '../../src/rpc/index.js' +import { createPeerId } from '../utils/create-peer-id.js' +import type { Validators } from '../../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { Connection } from '@libp2p/interface-connection' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Datastore } from 'interface-datastore' +import type { Duplex, Source } from 'it-stream-types' + +describe('rpc', () => { + let peerId: PeerId + let rpc: RPC + let providers: SinonStubbedInstance + let peerRouting: SinonStubbedInstance + let validators: Validators + let datastore: Datastore + let routingTable: RoutingTable + + beforeEach(async () => { + peerId = await createPeerId() + datastore = new MemoryDatastore() + + const components: RPCComponents = { + peerId, + datastore, + peerStore: stubInterface(), + addressManager: stubInterface() + } + components.peerStore = new PersistentPeerStore({ + ...components, + events: new EventEmitter() + }) + + await start(...Object.values(components)) + + providers = Sinon.createStubInstance(Providers) + peerRouting = Sinon.createStubInstance(PeerRouting) + routingTable = Sinon.createStubInstance(RoutingTable) + validators = {} + + rpc = new RPC(components, { + routingTable, + providers, + peerRouting, + validators, + lan: false + }) + }) + + it('calls back with the response', async () => { + const defer = pDefer() + const msg = new Message(MESSAGE_TYPE.GET_VALUE, uint8ArrayFromString('hello'), 5) + + const validateMessage = (res: Uint8ArrayList[]): void => { + const msg = Message.deserialize(res[0]) + expect(msg).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(msg).to.have.property('closerPeers').eql([]) + defer.resolve() + } + + peerRouting.getCloserPeersOffline.resolves([]) + + const source = pipe( + [msg.serialize()], + (source) => lp.encode(source), + source => map(source, arr => new Uint8ArrayList(arr)), + (source) => all(source) + ) + + const duplexStream: Duplex, Source, Promise> = { + source: (async function * () { + yield * source + })(), + sink: async (source) => { + const res = await pipe( + source, + (source) => lp.decode(source), + async (source) => all(source) + ) + validateMessage(res) + } + } + + rpc.onIncomingStream({ + stream: mockStream(duplexStream), + connection: stubInterface() + }) + + await defer.promise + }) +}) diff --git a/packages/kad-dht/test/utils/create-peer-id.ts b/packages/kad-dht/test/utils/create-peer-id.ts new file mode 100644 index 0000000000..5295535fb2 --- /dev/null +++ b/packages/kad-dht/test/utils/create-peer-id.ts @@ -0,0 +1,20 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import type { Ed25519PeerId } from '@libp2p/interface-peer-id' + +/** + * Creates multiple PeerIds + */ +export async function createPeerIds (length: number): Promise { + return Promise.all( + new Array(length).fill(0).map(async () => createEd25519PeerId()) + ) +} + +/** + * Creates a PeerId + */ +export async function createPeerId (): Promise { + const ids = await createPeerIds(1) + + return ids[0] +} diff --git a/packages/kad-dht/test/utils/create-values.ts b/packages/kad-dht/test/utils/create-values.ts new file mode 100644 index 0000000000..b4e2e75611 --- /dev/null +++ b/packages/kad-dht/test/utils/create-values.ts @@ -0,0 +1,22 @@ +import { randomBytes } from '@libp2p/crypto' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' + +export interface Value { + cid: CID + value: Uint8Array +} + +export async function createValues (length: number): Promise { + return Promise.all( + Array.from({ length }).map(async () => { + const bytes = randomBytes(32) + const h = await sha256.digest(bytes) + return { + cid: CID.createV1(raw.code, h), + value: bytes + } + }) + ) +} diff --git a/packages/kad-dht/test/utils/index.ts b/packages/kad-dht/test/utils/index.ts new file mode 100644 index 0000000000..933d9e7c8b --- /dev/null +++ b/packages/kad-dht/test/utils/index.ts @@ -0,0 +1,11 @@ +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * Count how many peers are in b but are not in a + */ +export function countDiffPeers (a: PeerId[], b: PeerId[]): number { + const s = new Set() + a.forEach((p) => s.add(p.toString())) + + return b.filter((p) => !s.has(p.toString())).length +} diff --git a/packages/kad-dht/test/utils/sort-closest-peers.ts b/packages/kad-dht/test/utils/sort-closest-peers.ts new file mode 100644 index 0000000000..1fb81e5322 --- /dev/null +++ b/packages/kad-dht/test/utils/sort-closest-peers.ts @@ -0,0 +1,28 @@ +import all from 'it-all' +import map from 'it-map' +import { compare as uint8ArrayCompare } from 'uint8arrays/compare' +import { xor as uint8ArrayXor } from 'uint8arrays/xor' +import { convertPeerId } from '../../src/utils.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * Sort peers by distance to the given `kadId` + */ +export async function sortClosestPeers (peers: PeerId[], kadId: Uint8Array): Promise { + const distances = await all( + map(peers, async (peer) => { + const id = await convertPeerId(peer) + + return { + peer, + distance: uint8ArrayXor(id, kadId) + } + }) + ) + + return distances + .sort((a, b) => { + return uint8ArrayCompare(a.distance, b.distance) + }) + .map((d) => d.peer) +} diff --git a/packages/kad-dht/test/utils/test-dht.ts b/packages/kad-dht/test/utils/test-dht.ts new file mode 100644 index 0000000000..e490b944d3 --- /dev/null +++ b/packages/kad-dht/test/utils/test-dht.ts @@ -0,0 +1,178 @@ +import { mockRegistrar, mockConnectionManager, mockNetwork } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { start, stop } from '@libp2p/interfaces/startable' +import { logger } from '@libp2p/logger' +import { PersistentPeerStore } from '@libp2p/peer-store' +import { multiaddr } from '@multiformats/multiaddr' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import pRetry from 'p-retry' +import { stubInterface } from 'ts-sinon' +import { DefaultDualKadDHT } from '../../src/dual-kad-dht.js' +import { createPeerId } from './create-peer-id.js' +import type { DualKadDHT, KadDHTComponents, KadDHTInit } from '../../src/index.js' +import type { DefaultKadDHT } from '../../src/kad-dht.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { ConnectionManager } from '@libp2p/interface-connection-manager' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Registrar } from '@libp2p/interface-registrar' + +const log = logger('libp2p:kad-dht:test-dht') + +export class TestDHT { + private readonly peers: Map + + constructor () { + this.peers = new Map() + } + + async spawn (options: Partial = {}, autoStart = true): Promise { + const events = new EventEmitter() + const components: KadDHTComponents = { + peerId: await createPeerId(), + datastore: new MemoryDatastore(), + registrar: mockRegistrar(), + // connectionGater: mockConnectionGater(), + addressManager: stubInterface(), + peerStore: stubInterface(), + connectionManager: stubInterface(), + events + } + components.connectionManager = mockConnectionManager({ + ...components, + events + }) + components.peerStore = new PersistentPeerStore({ + ...components, + events + }) + + await start(...Object.values(components)) + + mockNetwork.addNode({ + ...components, + events + }) + + const addressManager = stubInterface() + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/4002/p2p/${components.peerId.toString()}`), + multiaddr(`/ip4/192.168.1.1/tcp/4002/p2p/${components.peerId.toString()}`), + multiaddr(`/ip4/85.3.31.0/tcp/4002/p2p/${components.peerId.toString()}`) + ]) + + components.addressManager = addressManager + + const opts: KadDHTInit = { + validators: { + async v () { + + }, + async v2 () { + + } + }, + selectors: { + v: () => 0 + }, + querySelfInterval: 600000, + initialQuerySelfInterval: 600000, + allowQueryWithZeroPeers: true, + ...options + } + + const dht = new DefaultDualKadDHT(components, opts) + + // simulate libp2p._onDiscoveryPeer + dht.addEventListener('peer', (evt) => { + const peerData = evt.detail + + if (components.peerId.equals(peerData.id)) { + return + } + + components.peerStore.merge(peerData.id, { + multiaddrs: peerData.multiaddrs, + protocols: peerData.protocols + }) + .catch(err => { log.error(err) }) + }) + + if (autoStart) { + await dht.start() + } + + this.peers.set(components.peerId.toString(), { + dht, + registrar: components.registrar + }) + + return dht + } + + async connect (dhtA: DefaultDualKadDHT, dhtB: DefaultDualKadDHT): Promise { + // need addresses in the address book otherwise we won't know whether to add + // the peer to the public or private DHT and will do nothing + await dhtA.components.peerStore.merge(dhtB.components.peerId, { + multiaddrs: dhtB.components.addressManager.getAddresses() + }) + await dhtB.components.peerStore.merge(dhtA.components.peerId, { + multiaddrs: dhtA.components.addressManager.getAddresses() + }) + + await dhtA.components.connectionManager.openConnection(dhtB.components.peerId) + + // wait for peers to appear in each others' routing tables + await checkConnected(dhtA.lan, dhtB.lan) + + // only wait for WANs to connect if we are in server mode + if ((await dhtA.wan.getMode()) === 'server' && (await dhtB.wan.getMode()) === 'server') { + await checkConnected(dhtA.wan, dhtB.wan) + } + + async function checkConnected (a: DefaultKadDHT, b: DefaultKadDHT): Promise { + const routingTableChecks = [] + + routingTableChecks.push(async () => { + const match = await a.routingTable.find(dhtB.components.peerId) + + if (match == null) { + await delay(100) + throw new Error('not found') + } + + return match + }) + + routingTableChecks.push(async () => { + const match = await b.routingTable.find(dhtA.components.peerId) + + if (match == null) { + await delay(100) + throw new Error('not found') + } + + return match + }) + + // Check routing tables + return Promise.all( + routingTableChecks + .map( + async check => pRetry(check, { retries: 50 }) + ) + ) + } + } + + async teardown (): Promise { + await Promise.all( + Array.from(this.peers.entries()).map(async ([_, { dht }]) => { + await stop(dht) + }) + ) + this.peers.clear() + } +} diff --git a/packages/kad-dht/tsconfig.json b/packages/kad-dht/tsconfig.json new file mode 100644 index 0000000000..b0b4b2243b --- /dev/null +++ b/packages/kad-dht/tsconfig.json @@ -0,0 +1,78 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-address-manager" + }, + { + "path": "../interface-connection" + }, + { + "path": "../interface-connection-manager" + }, + { + "path": "../interface-content-routing" + }, + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interface-peer-routing" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interface-registrar" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-store" + }, + { + "path": "../record" + }, + { + "path": "../topology" + } + ] +} diff --git a/packages/keychain/CHANGELOG.md b/packages/keychain/CHANGELOG.md new file mode 100644 index 0000000000..0f7c302ac5 --- /dev/null +++ b/packages/keychain/CHANGELOG.md @@ -0,0 +1,204 @@ +## [2.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v2.0.0...v2.0.1) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7fd8023](https://github.com/libp2p/js-libp2p-keychain/commit/7fd80233db0b8706eb0ffe5372c6bad584ec211f)) +* Update .github/workflows/stale.yml [skip ci] ([c185b0d](https://github.com/libp2p/js-libp2p-keychain/commit/c185b0de456611ca42ec49bc7d52f803e4a76930)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#70](https://github.com/libp2p/js-libp2p-keychain/issues/70)) ([4da4a08](https://github.com/libp2p/js-libp2p-keychain/commit/4da4a08b86f436c36e2fae48ecc48817e9b8066f)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.1...v2.0.0) (2023-03-13) + + +### ⚠ BREAKING CHANGES + +* requires most recent datastore implementation + +### Bug Fixes + +* update datastore dependency ([#58](https://github.com/libp2p/js-libp2p-keychain/issues/58)) ([a8a1628](https://github.com/libp2p/js-libp2p-keychain/commit/a8a162875e48f23611190c3fb31e439da1d2d64b)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.0...v1.0.1) (2023-03-13) + + +### Bug Fixes + +* replace err-code with CodeError ([#57](https://github.com/libp2p/js-libp2p-keychain/issues/57)) ([cc752d9](https://github.com/libp2p/js-libp2p-keychain/commit/cc752d9349a622f013cb3b713d09a663b1169766)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([f3985cc](https://github.com/libp2p/js-libp2p-keychain/commit/f3985cc47ae966a33537af3f58c071f6c58184c9)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d8b81ff](https://github.com/libp2p/js-libp2p-keychain/commit/d8b81ff5e03ca56541ae2117a928dedf180e85ac)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a0a6972](https://github.com/libp2p/js-libp2p-keychain/commit/a0a6972d7af40488344e619e116f4d665190db6e)) +* Update .github/workflows/stale.yml [skip ci] ([b2cf129](https://github.com/libp2p/js-libp2p-keychain/commit/b2cf129fb1a3e0263a03d5a8a0e1ee74cd543004)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.1...v1.0.0) (2023-01-27) + + +### ⚠ BREAKING CHANGES + +* this module is now typescript and does not store the self key on startup. cms operations have also been moved to [@libp2p/cms](https://www.npmjs.com/@libp2p/cms) + +### Features + +* convert to typescript ([#53](https://github.com/libp2p/js-libp2p-keychain/issues/53)) ([3544df7](https://github.com/libp2p/js-libp2p-keychain/commit/3544df7c119b8cebded3f5c483e9f44bf499280f)) + + +### Trivial Changes + +* add deprecation notice ([#50](https://github.com/libp2p/js-libp2p-keychain/issues/50)) ([2a9b99c](https://github.com/libp2p/js-libp2p-keychain/commit/2a9b99cd402ed7260ebcac49d9e44905697beee0)) + + +## [0.6.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.0...v0.6.1) (2020-06-09) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.4...v0.6.0) (2019-12-18) + + + + +## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18) + + + + +## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16) + + +* refactor: use async/await instead of callbacks (#37) ([dda315a](https://github.com/libp2p/js-libp2p-keychain/commit/dda315a)), closes [#37](https://github.com/libp2p/js-libp2p-keychain/issues/37) + + +### BREAKING CHANGES + +* The api now uses async/await instead of callbacks. + +Co-Authored-By: Vasco Santos + + + + +## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13) + + +### Bug Fixes + +* throw errors with correct stack trace ([#35](https://github.com/libp2p/js-libp2p-keychain/issues/35)) ([7051b9c](https://github.com/libp2p/js-libp2p-keychain/commit/7051b9c)) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26) + + +### Features + +* adds support for ed25199 and secp256k1 ([#31](https://github.com/libp2p/js-libp2p-keychain/issues/31)) ([9eb11f4](https://github.com/libp2p/js-libp2p-keychain/commit/9eb11f4)) + + + + +## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10) + + +### Bug Fixes + +* reduce bundle size ([#28](https://github.com/libp2p/js-libp2p-keychain/issues/28)) ([7eeed87](https://github.com/libp2p/js-libp2p-keychain/commit/7eeed87)) + + + + +## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10) + + + + +## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04) + + + + +## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18) + + +### Bug Fixes + +* validate createKey params properly ([#26](https://github.com/libp2p/js-libp2p-keychain/issues/26)) ([8dfaab1](https://github.com/libp2p/js-libp2p-keychain/commit/8dfaab1)) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29) + + +### Bug Fixes + +* deepmerge 2.0.1 fails in browser, stay with 1.5.2 ([2ce4444](https://github.com/libp2p/js-libp2p-keychain/commit/2ce4444)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28) + + +### Features + +* generate unique options for a key chain ([#20](https://github.com/libp2p/js-libp2p-keychain/issues/20)) ([89a451c](https://github.com/libp2p/js-libp2p-keychain/commit/89a451c)) + + + + +# 0.2.0 (2017-12-20) + + +### Bug Fixes + +* error message ([8305d20](https://github.com/libp2p/js-libp2p-keychain/commit/8305d20)) +* lint errors ([06917f7](https://github.com/libp2p/js-libp2p-keychain/commit/06917f7)) +* lint errors ([ff4f656](https://github.com/libp2p/js-libp2p-keychain/commit/ff4f656)) +* linting ([409a999](https://github.com/libp2p/js-libp2p-keychain/commit/409a999)) +* maps an IPFS hash name to its forge equivalent ([f71d3a6](https://github.com/libp2p/js-libp2p-keychain/commit/f71d3a6)), closes [#12](https://github.com/libp2p/js-libp2p-keychain/issues/12) +* more linting ([7c44c91](https://github.com/libp2p/js-libp2p-keychain/commit/7c44c91)) +* return info on removed key [#10](https://github.com/libp2p/js-libp2p-keychain/issues/10) ([f49e753](https://github.com/libp2p/js-libp2p-keychain/commit/f49e753)) + + +### Features + +* move bits from https://github.com/richardschneider/ipfs-encryption ([1a96ae8](https://github.com/libp2p/js-libp2p-keychain/commit/1a96ae8)) +* use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9)) diff --git a/packages/keychain/LICENSE b/packages/keychain/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/keychain/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/keychain/LICENSE-APACHE b/packages/keychain/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/keychain/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/keychain/LICENSE-MIT b/packages/keychain/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/keychain/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/keychain/README.md b/packages/keychain/README.md new file mode 100644 index 0000000000..1998e1b7a1 --- /dev/null +++ b/packages/keychain/README.md @@ -0,0 +1,96 @@ +# @libp2p/keychain + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Key management and cryptographically protected messages + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Features + +- Manages the lifecycle of a key +- Keys are encrypted at rest +- Enforces the use of safe key names +- Uses encrypted PKCS 8 for key storage +- Uses PBKDF2 for a "stetched" key encryption key +- Enforces NIST SP 800-131A and NIST SP 800-132 +- Delays reporting errors to slow down brute force attacks + +### KeyInfo + +The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain. + +```js +{ + name: 'rsa-key', + id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW' +} +``` + +The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt). + +### Private key storage + +A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. + +The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function. + +```js +const defaultOptions = { + //See https://cryptosense.com/parameter-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 1000, + salt: 'at least 16 characters long', + hash: 'sha2-512' + } +} +``` + +![key storage](./doc/private-key.png?raw=true) + +### Physical storage + +The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benefit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/keychain/doc/private-key.png b/packages/keychain/doc/private-key.png new file mode 100644 index 0000000000000000000000000000000000000000..4c85dc610c883942212ff3b419cad3cae3c24769 GIT binary patch literal 25518 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 NO7iM*Wil3F{{{GIyypM_ literal 0 HcmV?d00001 diff --git a/packages/keychain/doc/private-key.xml b/packages/keychain/doc/private-key.xml new file mode 100644 index 0000000000..51cb8c5a9b --- /dev/null +++ b/packages/keychain/doc/private-key.xml @@ -0,0 +1 @@ +7VlNb6MwEP01HLfCGBJ6bNJ2V9pdqVIP2x4dcMAKYGScJumvXxNsvkw+SmgSVe2hMs9mbL839swQA07j9U+G0vAv9XFkWKa/NuC9YVmua4n/ObApAOjCAggY8QsIVMAzeccSNCW6JD7OGgM5pREnaRP0aJJgjzcwxBhdNYfNadScNUUB1oBnD0U6+o/4PJTbssYV/guTIFQzg9Ft0TND3iJgdJnI+QwLzrd/RXeMlC250SxEPl3VIPhgwCmjlBeteD3FUU6toq1473FHb7luhhN+zAtSpzcULeXWU5RluYmQoQzLRfKNIobjtbA7CXkcCQCIZsYZXeApjSgTSEITMXIyJ1HUglBEgkQ8emJlWOCTN8w4EZTfyY6Y+H4+zWQVEo6fU+Tlc66EfwlsSynOF22KJ7loYQCvd24clHQKL8U0xpxtxBDlolIA6aBgJJ9Xldy2hMKa0ko3JB0sKA1XJIuG5Lmbc6hx/jT5ff9oaWQL50jzZsqoh4Uq3dTUtBiAF9AmxtaJAVYHM6MBmLE1Zny8EABNOaFJ9nW9sfQryfr4fN7oaJxrNOPEv8sv1ZyvSFwPxGuSLjbJNi85GzcmGCvgdQvAUQk8YUbE8nK6a7xhX7uKD7JWo8XpoEVhDEeIk7em+S6u5AxPlIiJq6PQEgWMraaJjC6Zh+Vb9Uu2bUiFw12GOGIB5pqhrXTlto9SczSomk5Dyw9IJsL1dku1C+9SKpYHR5Fvmj1VhE1D2ukbTkX3WlQsuGmErbqw4KLnE5oHBDlWWbt10K22i+xQVgiANrVhaT4g271g22xfKI3kTDQKi33d5rY7fB4Mmgxn5B3NtgNy/5D7EKOdieHcfyhcRmiGo0mZBauwW+XBe+KlzOblSoxSz7pjunvj6A8RgcpaY9Mw3tfZ1BA6n2f41IOt6puaRAucrz/AiSbUNaR/Fjxj+geAxk668PJqRLiPexX8QPuS/OjVmo84yjhleqV2CXac9o18Vnb06uEm3e01PvWW8XZfh4iZFdn+n9mQTLWSCQhcjanRntB5ElF6yl9cQl++zGpfbo7unp9VZgE9M2dJoFFdbRmc5cRarRMLLd0P3S5KnAEoGWuUaHwcTHPXhL/U2q/NjPdF+k6tIHV6J8AqeF9PBtzyZxu2HLVvaQPdlqHhShswaG0zmLQdVWsRbb+lPV5avf44Qdpm2Vo/67JLnfb+oo86RDeNKxLdHkr0208TXcXGz/pW0S066C+61SG6/S36x0TXC7VTRP9SH43VLahyzHZpc/xHY7DfUG85xWP1A2MxvPoRFz78Bw== \ No newline at end of file diff --git a/packages/keychain/package.json b/packages/keychain/package.json new file mode 100644 index 0000000000..9bc7e246b0 --- /dev/null +++ b/packages/keychain/package.json @@ -0,0 +1,76 @@ +{ + "name": "@libp2p/keychain", + "version": "2.0.1", + "description": "Key management and cryptographically protected messages", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/keychain#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "crypto", + "encryption", + "keys", + "libp2p", + "secure" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-keychain": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "interface-datastore": "^8.2.0", + "merge-options": "^3.0.4", + "sanitize-filename": "^1.6.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10", + "datastore-core": "^9.0.1", + "multiformats": "^11.0.1" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/keychain/src/errors.ts b/packages/keychain/src/errors.ts new file mode 100644 index 0000000000..791d87fd3b --- /dev/null +++ b/packages/keychain/src/errors.ts @@ -0,0 +1,18 @@ + +export enum codes { + ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', + ERR_INVALID_KEY_NAME = 'ERR_INVALID_KEY_NAME', + ERR_INVALID_KEY_TYPE = 'ERR_INVALID_KEY_TYPE', + ERR_KEY_ALREADY_EXISTS = 'ERR_KEY_ALREADY_EXISTS', + ERR_INVALID_KEY_SIZE = 'ERR_INVALID_KEY_SIZE', + ERR_KEY_NOT_FOUND = 'ERR_KEY_NOT_FOUND', + ERR_OLD_KEY_NAME_INVALID = 'ERR_OLD_KEY_NAME_INVALID', + ERR_NEW_KEY_NAME_INVALID = 'ERR_NEW_KEY_NAME_INVALID', + ERR_PASSWORD_REQUIRED = 'ERR_PASSWORD_REQUIRED', + ERR_PEM_REQUIRED = 'ERR_PEM_REQUIRED', + ERR_CANNOT_READ_KEY = 'ERR_CANNOT_READ_KEY', + ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY', + ERR_INVALID_OLD_PASS_TYPE = 'ERR_INVALID_OLD_PASS_TYPE', + ERR_INVALID_NEW_PASS_TYPE = 'ERR_INVALID_NEW_PASS_TYPE', + ERR_INVALID_PASS_LENGTH = 'ERR_INVALID_PASS_LENGTH' +} diff --git a/packages/keychain/src/index.ts b/packages/keychain/src/index.ts new file mode 100644 index 0000000000..5c0a94ee3f --- /dev/null +++ b/packages/keychain/src/index.ts @@ -0,0 +1,579 @@ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { pbkdf2, randomBytes } from '@libp2p/crypto' +import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Key } from 'interface-datastore/key' +import mergeOptions from 'merge-options' +import sanitize from 'sanitize-filename' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { codes } from './errors.js' +import type { KeyChain, KeyInfo, KeyType } from '@libp2p/interface-keychain' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:keychain') + +export interface DEKConfig { + hash: string + salt: string + iterationCount: number + keyLength: number +} + +export interface KeyChainInit { + pass?: string + dek?: DEKConfig +} + +const keyPrefix = '/pkcs8/' +const infoPrefix = '/info/' +const privates = new WeakMap() + +// NIST SP 800-132 +const NIST = { + minKeyLength: 112 / 8, + minSaltLength: 128 / 8, + minIterationCount: 1000 +} + +const defaultOptions = { + // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + salt: 'you should override this value with a crypto secure random number', + hash: 'sha2-512' + } +} + +function validateKeyName (name: string): boolean { + if (name == null) { + return false + } + if (typeof name !== 'string') { + return false + } + return name === sanitize(name.trim()) && name.length > 0 +} + +/** + * Throws an error after a delay + * + * This assumes than an error indicates that the keychain is under attack. Delay returning an + * error to make brute force attacks harder. + */ +async function randomDelay (): Promise { + const min = 200 + const max = 1000 + const delay = Math.random() * (max - min) + min + + await new Promise(resolve => setTimeout(resolve, delay)) +} + +/** + * Converts a key name into a datastore name + */ +function DsName (name: string): Key { + return new Key(keyPrefix + name) +} + +/** + * Converts a key name into a datastore info name + */ +function DsInfoName (name: string): Key { + return new Key(infoPrefix + name) +} + +export interface KeyChainComponents { + datastore: Datastore +} + +/** + * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. + * + * A key in the store has two entries + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key + * + */ +export class DefaultKeyChain implements KeyChain { + private readonly components: KeyChainComponents + private readonly init: KeyChainInit + + /** + * Creates a new instance of a key chain + */ + constructor (components: KeyChainComponents, init: KeyChainInit) { + this.components = components + this.init = mergeOptions(defaultOptions, init) + + // Enforce NIST SP 800-132 + if (this.init.pass != null && this.init.pass?.length < 20) { + throw new Error('pass must be least 20 characters') + } + if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { + throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) + } + if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { + throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) + } + if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { + throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) + } + + const dek = this.init.pass != null && this.init.dek?.salt != null + ? pbkdf2( + this.init.pass, + this.init.dek?.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + + privates.set(this, { dek }) + } + + /** + * Generates the options for a keychain. A random salt is produced. + * + * @returns {object} + */ + static generateOptions (): KeyChainInit { + const options = Object.assign({}, defaultOptions) + const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding + options.dek.salt = uint8ArrayToString(randomBytes(saltLength), 'base64') + return options + } + + /** + * Gets an object that can encrypt/decrypt protected data. + * The default options for a keychain. + * + * @returns {object} + */ + static get options (): typeof defaultOptions { + return defaultOptions + } + + /** + * Create a new key. + * + * @param {string} name - The local key name; cannot already exist. + * @param {string} type - One of the key types; 'rsa'. + * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only + */ + async createKey (name: string, type: KeyType, size = 2048): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) + } + + if (typeof type !== 'string') { + await randomDelay() + throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE) + } + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) + } + + switch (type.toLowerCase()) { + case 'rsa': + if (!Number.isSafeInteger(size) || size < 2048) { + await randomDelay() + throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE) + } + break + default: + break + } + + let keyInfo + try { + const keypair = await generateKeyPair(type, size) + const kid = await keypair.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await keypair.export(dek) + keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + + await batch.commit() + } catch (err: any) { + await randomDelay() + throw err + } + + return keyInfo + } + + /** + * List all the keys. + * + * @returns {Promise} + */ + async listKeys (): Promise { + const query = { + prefix: infoPrefix + } + + const info = [] + for await (const value of this.components.datastore.query(query)) { + info.push(JSON.parse(uint8ArrayToString(value.value))) + } + + return info + } + + /** + * Find a key by it's id + */ + async findKeyById (id: string): Promise { + try { + const keys = await this.listKeys() + const key = keys.find((k) => k.id === id) + + if (key == null) { + throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + + return key + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Find a key by it's name. + * + * @param {string} name - The local key name. + * @returns {Promise} + */ + async findKeyByName (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + const dsname = DsInfoName(name) + try { + const res = await this.components.datastore.get(dsname) + return JSON.parse(uint8ArrayToString(res)) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Remove an existing key. + * + * @param {string} name - The local key name; must already exist. + * @returns {Promise} + */ + async removeKey (name: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + const dsname = DsName(name) + const keyInfo = await this.findKeyByName(name) + const batch = this.components.datastore.batch() + batch.delete(dsname) + batch.delete(DsInfoName(name)) + await batch.commit() + return keyInfo + } + + /** + * Rename a key + * + * @param {string} oldName - The old local key name; must already exist. + * @param {string} newName - The new local key name; must not already exist. + * @returns {Promise} + */ + async renameKey (oldName: string, newName: string): Promise { + if (!validateKeyName(oldName) || oldName === 'self') { + await randomDelay() + throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) + } + if (!validateKeyName(newName) || newName === 'self') { + await randomDelay() + throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID) + } + const oldDsname = DsName(oldName) + const newDsname = DsName(newName) + const oldInfoName = DsInfoName(oldName) + const newInfoName = DsInfoName(newName) + + const exists = await this.components.datastore.has(newDsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + try { + const pem = await this.components.datastore.get(oldDsname) + const res = await this.components.datastore.get(oldInfoName) + + const keyInfo = JSON.parse(uint8ArrayToString(res)) + keyInfo.name = newName + const batch = this.components.datastore.batch() + batch.put(newDsname, pem) + batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) + batch.delete(oldDsname) + batch.delete(oldInfoName) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PEM encrypted PKCS #8 string + */ + async exportKey (name: string, password: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (password == null) { + await randomDelay() + throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED) + } + + const dsname = DsName(name) + try { + const res = await this.components.datastore.get(dsname) + const pem = uint8ArrayToString(res) + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const privateKey = await importKey(pem, dek) + const keyString = await privateKey.export(password) + + return keyString + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PeerId + */ + async exportPeerId (name: string): Promise { + const password = 'temporary-password' + const pem = await this.exportKey(name, password) + const privateKey = await importKey(pem, password) + + return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) + } + + /** + * Import a new key from a PEM encoded PKCS #8 string + * + * @param {string} name - The local key name; must not already exist. + * @param {string} pem - The PEM encoded PKCS #8 string + * @param {string} password - The password. + * @returns {Promise} + */ + async importKey (name: string, pem: string, password: string): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (pem == null) { + await randomDelay() + throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED) + } + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + let privateKey + try { + privateKey = await importKey(pem, password) + } catch (err: any) { + await randomDelay() + throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY) + } + + let kid + try { + kid = await privateKey.id() + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + pem = await privateKey.export(dek) + } catch (err: any) { + await randomDelay() + throw err + } + + const keyInfo = { + name, + id: kid + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + + return keyInfo + } + + /** + * Import a peer key + */ + async importPeer (name: string, peer: PeerId): Promise { + try { + if (!validateKeyName(name)) { + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (peer == null) { + throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) + } + if (peer.privateKey == null) { + throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY) + } + + const privateKey = await unmarshalPrivateKey(peer.privateKey) + + const dsname = DsName(name) + const exists = await this.components.datastore.has(dsname) + if (exists) { + await randomDelay() + throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await privateKey.export(dek) + const keyInfo: KeyInfo = { + name, + id: peer.toString() + } + const batch = this.components.datastore.batch() + batch.put(dsname, uint8ArrayFromString(pem)) + batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + return keyInfo + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Gets the private key as PEM encoded PKCS #8 string + */ + async getPrivateKey (name: string): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + + try { + const dsname = DsName(name) + const res = await this.components.datastore.get(dsname) + return uint8ArrayToString(res) + } catch (err: any) { + await randomDelay() + log.error(err) + throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + } + + /** + * Rotate keychain password and re-encrypt all associated keys + */ + async rotateKeychainPass (oldPass: string, newPass: string): Promise { + if (typeof oldPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) + } + if (typeof newPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) + } + if (newPass.length < 20) { + await randomDelay() + throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) + } + log('recreating keychain') + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const oldDek = cached.dek + this.init.pass = newPass + const newDek = newPass != null && this.init.dek?.salt != null + ? pbkdf2( + newPass, + this.init.dek.salt, + this.init.dek?.iterationCount, + this.init.dek?.keyLength, + this.init.dek?.hash) + : '' + privates.set(this, { dek: newDek }) + const keys = await this.listKeys() + for (const key of keys) { + const res = await this.components.datastore.get(DsName(key.name)) + const pem = uint8ArrayToString(res) + const privateKey = await importKey(pem, oldDek) + const password = newDek.toString() + const keyAsPEM = await privateKey.export(password) + + // Update stored key + const batch = this.components.datastore.batch() + const keyInfo = { + name: key.name, + id: key.id + } + batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) + batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) + await batch.commit() + } + log('keychain reconstructed') + } +} diff --git a/packages/keychain/src/util.ts b/packages/keychain/src/util.ts new file mode 100644 index 0000000000..e708fd1e03 --- /dev/null +++ b/packages/keychain/src/util.ts @@ -0,0 +1,16 @@ +/** + * Finds the first item in a collection that is matched in the + * `asyncCompare` function. + * + * `asyncCompare` is an async function that must + * resolve to either `true` or `false`. + * + * @param {Array} array + * @param {function(*)} asyncCompare - An async function that returns a boolean + */ +export async function findAsync (array: T[], asyncCompare: (val: T) => Promise): Promise { + const promises = array.map(asyncCompare) + const results = await Promise.all(promises) + const index = results.findIndex(result => result) + return array[index] +} diff --git a/packages/keychain/test/keychain.spec.ts b/packages/keychain/test/keychain.spec.ts new file mode 100644 index 0000000000..6089b90734 --- /dev/null +++ b/packages/keychain/test/keychain.spec.ts @@ -0,0 +1,552 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { pbkdf2 } from '@libp2p/crypto' +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { createFromPrivKey } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { Key } from 'interface-datastore/key' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { DefaultKeyChain, type KeyChainInit } from '../src/index.js' +import type { KeyChain, KeyInfo } from '@libp2p/interface-keychain' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Datastore } from 'interface-datastore' + +describe('keychain', () => { + const passPhrase = 'this is not a secure phrase' + const rsaKeyName = 'tajné jméno' + const renamedRsaKeyName = 'ชื่อลับ' + let rsaKeyInfo: KeyInfo + let ks: DefaultKeyChain + let datastore2: Datastore + + before(async () => { + datastore2 = new MemoryDatastore() + + ks = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase }) + }) + + it('can start without a password', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, {}) + }()).to.eventually.be.ok() + }) + + it('needs a NIST SP 800-132 non-weak pass phrase', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, { pass: '< 20 character' }) + }()).to.eventually.be.rejected() + }) + + it('has default options', () => { + expect(DefaultKeyChain.options).to.exist() + }) + + it('supports supported hashing alorithms', async () => { + const ok = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) + expect(ok).to.exist() + }) + + it('does not support unsupported hashing alorithms', async () => { + await expect(async function () { + return new DefaultKeyChain({ + datastore: datastore2 + }, { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) + }()).to.eventually.be.rejected() + }) + + it('can list keys without a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + + expect(await keychain.listKeys()).to.have.lengthOf(0) + }) + + it('can find a key without a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + const keychainWithPassword = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: `hello-${Date.now()}-${Date.now()}` }) + const name = `key-${Math.random()}` + + const { id } = await keychainWithPassword.createKey(name, 'Ed25519') + + await expect(keychain.findKeyById(id)).to.eventually.be.ok() + }) + + it('can remove a key without a password', async () => { + const keychainWithoutPassword = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + const keychainWithPassword = new DefaultKeyChain({ + datastore: datastore2 + }, { pass: `hello-${Date.now()}-${Date.now()}` }) + const name = `key-${Math.random()}` + + expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) + expect(await keychainWithoutPassword.findKeyByName(name)).to.have.property('name', name) + await keychainWithoutPassword.removeKey(name) + await expect(keychainWithoutPassword.findKeyByName(name)).to.be.rejectedWith(/does not exist/) + }) + + it('requires a name to create a password', async () => { + const keychain = new DefaultKeyChain({ + datastore: datastore2 + }, {}) + + // @ts-expect-error invalid parameters + await expect(keychain.createKey(undefined, 'derp')).to.eventually.be.rejected() + }) + + it('can generate options', async () => { + const options = DefaultKeyChain.generateOptions() + options.pass = passPhrase + const chain = new DefaultKeyChain({ + datastore: datastore2 + }, options) + expect(chain).to.exist() + }) + + describe('key name', () => { + it('is a valid filename and non-ASCII', async () => { + const errors = await Promise.all([ + ks.removeKey('../../nasty').catch(err => err), + ks.removeKey('').catch(err => err), + ks.removeKey(' ').catch(err => err), + // @ts-expect-error invalid parameters + ks.removeKey(null).catch(err => err), + // @ts-expect-error invalid parameters + ks.removeKey(undefined).catch(err => err) + ]) + + expect(errors).to.have.length(5) + errors.forEach(error => { + expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + }) + + describe('key', () => { + it('can be an RSA key', async () => { + rsaKeyInfo = await ks.createKey(rsaKeyName, 'RSA', 2048) + expect(rsaKeyInfo).to.exist() + expect(rsaKeyInfo).to.have.property('name', rsaKeyName) + expect(rsaKeyInfo).to.have.property('id') + }) + + it('is encrypted PEM encoded PKCS #8', async () => { + const pem = await ks.getPrivateKey(rsaKeyName) + return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('throws if an invalid private key name is given', async () => { + // @ts-expect-error invalid parameters + await expect(ks.getPrivateKey(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('throws if a private key cant be found', async () => { + await expect(ks.getPrivateKey('not real')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.createKey(rsaKeyName, 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + await expect(ks.createKey('self', 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate name is string', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(5, 'rsa', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('should validate type is string', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(`TEST-${Date.now()}`, null, 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_TYPE') + }) + + it('should validate size is integer', async () => { + // @ts-expect-error invalid parameters + await expect(ks.createKey(`TEST-${Date.now()}`, 'RSA', 'string')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') + }) + + describe('implements NIST SP 800-131A', () => { + it('disallows RSA length < 2048', async () => { + await expect(ks.createKey('bad-nist-rsa', 'RSA', 1024)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') + }) + }) + }) + + describe('Ed25519 keys', () => { + const keyName = 'my custom key' + it('can be an Ed25519 key', async () => { + const keyInfo = await ks.createKey(keyName, 'Ed25519') + expect(keyInfo).to.exist() + expect(keyInfo).to.have.property('name', keyName) + expect(keyInfo).to.have.property('id') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.createKey(keyName, 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('can export/import a key', async () => { + const keyName = 'a new key' + const password = 'my sneaky password' + const keyInfo = await ks.createKey(keyName, 'Ed25519') + const exportedKey = await ks.exportKey(keyName, password) + // remove it so we can import it + await ks.removeKey(keyName) + const importedKey = await ks.importKey(keyName, exportedKey, password) + expect(importedKey.id).to.eql(keyInfo.id) + }) + + it('cannot create the "self" key', async () => { + await expect(ks.createKey('self', 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + + describe('query', () => { + it('finds all existing keys', async () => { + const keys = await ks.listKeys() + expect(keys).to.exist() + const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) + expect(mykey).to.exist() + }) + + it('finds a key by name', async () => { + const key = await ks.findKeyByName(rsaKeyName) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('finds a key by id', async () => { + const key = await ks.findKeyById(rsaKeyInfo.id) + expect(key).to.exist() + expect(key).to.deep.equal(rsaKeyInfo) + }) + + it('returns the key\'s name and id', async () => { + const keys = await ks.listKeys() + expect(keys).to.exist() + keys.forEach((key) => { + expect(key).to.have.property('name') + expect(key).to.have.property('id') + }) + }) + }) + + describe('exported key', () => { + let pemKey: string + + it('requires the password', async () => { + // @ts-expect-error invalid parameters + await expect(ks.exportKey(rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_PASSWORD_REQUIRED') + }) + + it('requires the key name', async () => { + // @ts-expect-error invalid parameters + await expect(ks.exportKey(undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('is a PKCS #8 encrypted pem', async () => { + pemKey = await ks.exportKey(rsaKeyName, 'password') + expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') + }) + + it('can be imported', async () => { + const key = await ks.importKey('imported-key', pemKey, 'password') + expect(key.name).to.equal('imported-key') + expect(key.id).to.equal(rsaKeyInfo.id) + }) + + it('requires the pem', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importKey('imported-key', undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_PEM_REQUIRED') + }) + + it('cannot be imported as an existing key name', async () => { + await expect(ks.importKey(rsaKeyName, pemKey, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot be imported with the wrong password', async () => { + await expect(ks.importKey('a-new-name-for-import', pemKey, 'not the password')).to.eventually.be.rejected.with.property('code', 'ERR_CANNOT_READ_KEY') + }) + }) + + describe('peer id', () => { + const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' + let alice: PeerId + + before(async function () { + const encoded = uint8ArrayFromString(alicePrivKey, 'base64pad') + const privateKey = await unmarshalPrivateKey(encoded) + alice = await createFromPrivKey(privateKey) + }) + + it('private key can be imported', async () => { + const key = await ks.importPeer('alice', alice) + expect(key.name).to.equal('alice') + expect(key.id).to.equal(alice.toString()) + }) + + it('private key can be exported', async () => { + const alice2 = await ks.exportPeerId('alice') + + expect(alice.equals(alice2)).to.be.true() + expect(alice2).to.have.property('privateKey').that.is.ok() + expect(alice2).to.have.property('publicKey').that.is.ok() + }) + + it('private key import requires a valid name', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importPeer(undefined, alice)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('private key import requires the peer', async () => { + // @ts-expect-error invalid parameters + await expect(ks.importPeer('alice')).to.eventually.be.rejected.with.property('code', 'ERR_MISSING_PRIVATE_KEY') + }) + + it('key id exists', async () => { + const key = await ks.findKeyById(alice.toString()) + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toString()) + }) + + it('key name exists', async () => { + const key = await ks.findKeyByName('alice') + expect(key).to.exist() + expect(key).to.have.property('name', 'alice') + expect(key).to.have.property('id', alice.toString()) + }) + + it('can create Ed25519 peer id', async () => { + const name = 'ed-key' + await ks.createKey(name, 'Ed25519') + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'Ed25519') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + + it('can create RSA peer id', async () => { + const name = 'rsa-key' + await ks.createKey(name, 'RSA', 2048) + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'RSA') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + + it('can create secp256k1 peer id', async () => { + const name = 'secp256k1-key' + await ks.createKey(name, 'secp256k1') + const peer = await ks.exportPeerId(name) + + expect(peer).to.have.property('type', 'secp256k1') + expect(peer).to.have.property('privateKey').that.is.ok() + expect(peer).to.have.property('publicKey').that.is.ok() + }) + }) + + describe('rename', () => { + it('requires an existing key name', async () => { + await expect(ks.renameKey('not-there', renamedRsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_NOT_FOUND') + }) + + it('requires a valid new key name', async () => { + await expect(ks.renameKey(rsaKeyName, '..\not-valid')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('does not overwrite existing key', async () => { + await expect(ks.renameKey(rsaKeyName, rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') + }) + + it('cannot create the "self" key', async () => { + await expect(ks.renameKey(rsaKeyName, 'self')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') + }) + + it('removes the existing key name', async () => { + const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + // Try to find the changed key + await expect(ks.findKeyByName(rsaKeyName)).to.eventually.be.rejected() + }) + + it('creates the new key name', async () => { + const key = await ks.findKeyByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + }) + + it('does not change the key ID', async () => { + const key = await ks.findKeyByName(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + + it('throws with invalid key names', async () => { + // @ts-expect-error invalid parameters + await expect(ks.findKeyByName(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + }) + + describe('key removal', () => { + it('cannot remove the "self" key', async () => { + await expect(ks.removeKey('self')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') + }) + + it('cannot remove an unknown key', async () => { + await expect(ks.removeKey('not-there')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') + }) + + it('can remove a known key', async () => { + const key = await ks.removeKey(renamedRsaKeyName) + expect(key).to.exist() + expect(key).to.have.property('name', renamedRsaKeyName) + expect(key).to.have.property('id', rsaKeyInfo.id) + }) + }) + + describe('rotate keychain passphrase', () => { + let oldPass: string + let kc: KeyChain + let options: KeyChainInit + let ds: Datastore + before(async () => { + ds = new MemoryDatastore() + oldPass = `hello-${Date.now()}-${Date.now()}` + options = { + pass: oldPass, + dek: { + salt: '3Nd/Ya4ENB3bcByNKptb4IR', + iterationCount: 10000, + keyLength: 64, + hash: 'sha2-512' + } + } + kc = new DefaultKeyChain({ + datastore: ds + }, options) + }) + + it('should validate newPass is a string', async () => { + // @ts-expect-error invalid parameters + await expect(kc.rotateKeychainPass(oldPass, 1234567890)).to.eventually.be.rejected() + }) + + it('should validate oldPass is a string', async () => { + // @ts-expect-error invalid parameters + await expect(kc.rotateKeychainPass(1234, 'newInsecurePassword1')).to.eventually.be.rejected() + }) + + it('should validate newPass is at least 20 characters', async () => { + try { + await kc.rotateKeychainPass(oldPass, 'not20Chars') + } catch (err: any) { + expect(err).to.exist() + } + }) + + it('can rotate keychain passphrase', async () => { + await kc.createKey('keyCreatedWithOldPassword', 'RSA', 2048) + await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase') + + // Get Key PEM from datastore + const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword') + const res = await ds.get(dsname) + const pem = uint8ArrayToString(res) + + const oldDek = options.pass != null + ? pbkdf2( + options.pass, + options.dek?.salt ?? 'salt', + options.dek?.iterationCount ?? 0, + options.dek?.keyLength ?? 0, + options.dek?.hash ?? 'sha2-256' + ) + : '' + + const newDek = pbkdf2( + 'newInsecurePassphrase', + options.dek?.salt ?? 'salt', + options.dek?.iterationCount ?? 0, + options.dek?.keyLength ?? 0, + options.dek?.hash ?? 'sha2-256' + ) + + // Dek with old password should not work: + await expect(kc.importKey('keyWhosePassChanged', pem, oldDek)) + .to.eventually.be.rejected() + // Dek with new password should work: + await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek)) + .to.eventually.have.property('name', 'keyWhosePasswordChanged') + }).timeout(10000) + }) +}) + +describe('libp2p.keychain', () => { + it('needs a passphrase to be used, otherwise throws an error', async () => { + expect(() => { + return new DefaultKeyChain({ + datastore: new MemoryDatastore() + }, { + pass: '' + }) + }).to.throw() + }) + + it('can be used when a passphrase is provided', async () => { + const keychain = new DefaultKeyChain({ + datastore: new MemoryDatastore() + }, { + pass: '12345678901234567890' + }) + + const kInfo = await keychain.createKey('keyName', 'Ed25519') + expect(kInfo).to.exist() + }) + + it('can reload keys', async () => { + const datastore = new MemoryDatastore() + const keychain = new DefaultKeyChain({ + datastore + }, { + pass: '12345678901234567890' + }) + + const kInfo = await keychain.createKey('keyName', 'Ed25519') + expect(kInfo).to.exist() + + const keychain2 = new DefaultKeyChain({ + datastore + }, { + pass: '12345678901234567890' + }) + + const key = await keychain2.findKeyByName('keyName') + + expect(key).to.exist() + }) +}) diff --git a/packages/keychain/test/peerid.spec.ts b/packages/keychain/test/peerid.spec.ts new file mode 100644 index 0000000000..cc8e8a5fcf --- /dev/null +++ b/packages/keychain/test/peerid.spec.ts @@ -0,0 +1,76 @@ +/* eslint-env mocha */ + +import { supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { createFromPrivKey } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { base58btc } from 'multiformats/bases/base58' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { PeerId } from '@libp2p/interface-peer-id' + +const sample = { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=' +} + +describe('peer ID', () => { + let peer: PeerId + let publicKeyDer: Uint8Array // a buffer + + before(async () => { + const encoded = uint8ArrayFromString(sample.privKey, 'base64pad') + peer = await createFromPrivKey(await unmarshalPrivateKey(encoded)) + }) + + it('decoded public key', async () => { + if (peer.publicKey == null) { + throw new Error('PublicKey missing from PeerId') + } + + if (peer.privateKey == null) { + throw new Error('PrivateKey missing from PeerId') + } + + // get protobuf version of the public key + const publicKeyProtobuf = peer.publicKey + const publicKey = unmarshalPublicKey(publicKeyProtobuf) + publicKeyDer = publicKey.marshal() + + // get protobuf version of the private key + const privateKeyProtobuf = peer.privateKey + const key = await unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() + }) + + it('encoded public key with DER', async () => { + const rsa = supportedKeys.rsa.unmarshalRsaPublicKey(publicKeyDer) + const keyId = await rsa.hash() + const kids = base58btc.encode(keyId).substring(1) + expect(kids).to.equal(peer.toString()) + }) + + it('encoded public key with JWT', async () => { + const jwk = { + kty: 'RSA', + n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw', + e: 'AQAB', + alg: 'RS256', + kid: '2011-04-29' + } + const rsa = new supportedKeys.rsa.RsaPublicKey(jwk) + const keyId = await rsa.hash() + const kids = base58btc.encode(keyId).substring(1) + expect(kids).to.equal(peer.toString()) + }) + + it('decoded private key', async () => { + if (peer.privateKey == null) { + throw new Error('PrivateKey missing from PeerId') + } + + // get protobuf version of the private key + const privateKeyProtobuf = peer.privateKey + const key = await unmarshalPrivateKey(privateKeyProtobuf) + expect(key).to.exist() + }) +}) diff --git a/packages/keychain/tsconfig.json b/packages/keychain/tsconfig.json new file mode 100644 index 0000000000..f3aa4228a3 --- /dev/null +++ b/packages/keychain/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-keychain" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 546978e727..83cb7e33c7 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -115,7 +115,7 @@ }, "dependencies": { "@achingbrain/nat-port-mapper": "^1.0.9", - "@libp2p/crypto": "^1.0.17", + "@libp2p/crypto": "^1.0.0", "@libp2p/interface-address-manager": "^3.0.0", "@libp2p/interface-connection": "^5.0.0", "@libp2p/interface-connection-encrypter": "^4.0.0", @@ -137,23 +137,23 @@ "@libp2p/interface-transport": "^4.0.0", "@libp2p/interfaces": "^3.0.0", "@libp2p/keychain": "^2.0.0", - "@libp2p/logger": "^2.1.1", - "@libp2p/multistream-select": "^3.1.8", - "@libp2p/peer-collections": "^3.0.1", + "@libp2p/logger": "^2.0.0", + "@libp2p/multistream-select": "^3.0.0", + "@libp2p/peer-collections": "^3.0.0", "@libp2p/peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", "@libp2p/peer-record": "^5.0.0", - "@libp2p/peer-store": "^8.2.0", - "@libp2p/topology": "^4.0.1", + "@libp2p/peer-store": "^8.0.0", + "@libp2p/topology": "^4.0.0", "@libp2p/tracked-map": "^3.0.0", - "@libp2p/utils": "^3.0.10", - "@multiformats/mafmt": "^12.0.0", - "@multiformats/multiaddr": "^12.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", - "datastore-core": "^9.0.0", - "interface-datastore": "^8.0.0", - "it-all": "^3.0.1", + "datastore-core": "^9.0.1", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", "it-drain": "^3.0.1", "it-filter": "^3.0.1", "it-first": "^3.0.1", @@ -167,7 +167,7 @@ "it-pipe": "^3.0.1", "it-stream-types": "^2.0.1", "merge-options": "^3.0.4", - "multiformats": "^11.0.0", + "multiformats": "^11.0.2", "p-defer": "^4.0.0", "p-queue": "^7.3.4", "p-retry": "^5.0.0", @@ -175,8 +175,8 @@ "protons-runtime": "^5.0.0", "rate-limiter-flexible": "^2.3.11", "uint8arraylist": "^2.4.3", - "uint8arrays": "^4.0.2", - "wherearewe": "^2.0.0", + "uint8arrays": "^4.0.3", + "wherearewe": "^2.0.1", "xsalsa20": "^1.1.0" }, "devDependencies": { @@ -192,27 +192,27 @@ "@libp2p/interface-connection-encrypter-compliance-tests": "^5.0.0", "@libp2p/interface-mocks": "^12.0.0", "@libp2p/interop": "^8.0.0", - "@libp2p/kad-dht": "^9.2.0", + "@libp2p/kad-dht": "^9.0.0", "@libp2p/mdns": "^8.0.0", - "@libp2p/mplex": "^8.0.1", + "@libp2p/mplex": "^8.0.0", "@libp2p/pubsub": "^7.0.1", - "@libp2p/tcp": "^7.0.1", - "@libp2p/websockets": "^6.0.1", + "@libp2p/tcp": "^7.0.0", + "@libp2p/websockets": "^6.0.0", "@types/varint": "^6.0.0", "@types/xsalsa20": "^1.1.0", - "aegir": "^39.0.5", - "cborg": "^1.8.1", + "aegir": "^39.0.10", + "cborg": "^2.0.1", "delay": "^6.0.0", "execa": "^7.0.0", - "go-libp2p": "^1.0.1", + "go-libp2p": "^1.1.1", "it-pushable": "^3.0.0", "it-to-buffer": "^4.0.1", "npm-run-all": "^4.1.5", - "p-event": "^5.0.1", + "p-event": "^6.0.0", "p-times": "^4.0.0", "p-wait-for": "^5.0.0", "protons": "^7.0.2", - "sinon": "^15.0.1", + "sinon": "^15.1.0", "sinon-ts": "^1.0.0" }, "browser": { diff --git a/packages/libp2p/tsconfig.json b/packages/libp2p/tsconfig.json index 9ac0d9429c..e4667a71b2 100644 --- a/packages/libp2p/tsconfig.json +++ b/packages/libp2p/tsconfig.json @@ -8,6 +8,9 @@ "test" ], "references": [ + { + "path": "../crypto" + }, { "path": "../interface-address-manager" }, @@ -79,6 +82,57 @@ }, { "path": "../interfaces" + }, + { + "path": "../kad-dht" + }, + { + "path": "../keychain" + }, + { + "path": "../logger" + }, + { + "path": "../multistream-select" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-discovery-bootstrap" + }, + { + "path": "../peer-discovery-mdns" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-record" + }, + { + "path": "../peer-store" + }, + { + "path": "../stream-multiplexer-mplex" + }, + { + "path": "../topology" + }, + { + "path": "../tracked-map" + }, + { + "path": "../transport-tcp" + }, + { + "path": "../transport-websockets" + }, + { + "path": "../utils" } ] } diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md new file mode 100644 index 0000000000..a05e46a74a --- /dev/null +++ b/packages/logger/CHANGELOG.md @@ -0,0 +1,184 @@ +## [2.1.1](https://github.com/libp2p/js-libp2p-logger/compare/v2.1.0...v2.1.1) (2023-06-02) + + +### Bug Fixes + +* specify updated formatter for multiaddrs ([#36](https://github.com/libp2p/js-libp2p-logger/issues/36)) ([abaefb4](https://github.com/libp2p/js-libp2p-logger/commit/abaefb490a0d9464a23b422d9fc5b80051532d10)) + +## [2.1.0](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.7...v2.1.0) (2023-05-31) + + +### Features + +* added multiaddr formatter to logging ([#34](https://github.com/libp2p/js-libp2p-logger/issues/34)) ([1051708](https://github.com/libp2p/js-libp2p-logger/commit/1051708697299a51bfb9dac77bdd14f644ac0fe2)) + +## [2.0.7](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.6...v2.0.7) (2023-03-24) + + +### Bug Fixes + +* disable trace logging by default ([#32](https://github.com/libp2p/js-libp2p-logger/issues/32)) ([47915fe](https://github.com/libp2p/js-libp2p-logger/commit/47915fe85dc8b50713c4344d15cb9082d8147b8f)) + +## [2.0.6](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.5...v2.0.6) (2023-03-13) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([a24089e](https://github.com/libp2p/js-libp2p-logger/commit/a24089e1ad4bf47185828a880879fa60adf867d6)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([36c0fe1](https://github.com/libp2p/js-libp2p-logger/commit/36c0fe124ebd82c06c167b999d960343a4601468)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([e73d815](https://github.com/libp2p/js-libp2p-logger/commit/e73d8153a0b93a25b2fbdfd6950eaa41e778ab4f)) + + +### Dependencies + +* update interface-datastore and aegir ([#29](https://github.com/libp2p/js-libp2p-logger/issues/29)) ([451d936](https://github.com/libp2p/js-libp2p-logger/commit/451d936b77b6e4588e31665e32f98e13da6c1899)) + +## [2.0.5](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.4...v2.0.5) (2023-01-06) + + +### Documentation + +* publish typedocs ([#18](https://github.com/libp2p/js-libp2p-logger/issues/18)) ([ff0a8ce](https://github.com/libp2p/js-libp2p-logger/commit/ff0a8ce535474bb30e7977fa7684bb4c17181ead)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.3...v2.0.4) (2023-01-06) + + +### Dependencies + +* update @libp2p/interface-peer-id to 2.x.x ([#17](https://github.com/libp2p/js-libp2p-logger/issues/17)) ([9107f4e](https://github.com/libp2p/js-libp2p-logger/commit/9107f4ed0e8b5db713572b0c528b6860d6f572c3)) + +## [2.0.3](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.2...v2.0.3) (2023-01-06) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#16](https://github.com/libp2p/js-libp2p-logger/issues/16)) ([892f906](https://github.com/libp2p/js-libp2p-logger/commit/892f906e884af0fd4af0f5cd7e01dc1dceacad6f)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.1...v2.0.2) (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([0d54ade](https://github.com/libp2p/js-libp2p-logger/commit/0d54ade029fcf26d77b35b043e7a521088930231)) +* update project config ([#11](https://github.com/libp2p/js-libp2p-logger/issues/11)) ([e993132](https://github.com/libp2p/js-libp2p-logger/commit/e9931326baa5de5ff8614d685413e1f5bb2bf0bc)) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([#10](https://github.com/libp2p/js-libp2p-logger/issues/10)) ([1cbf8c9](https://github.com/libp2p/js-libp2p-logger/commit/1cbf8c973605316f9560c1a50e2a19543ec36f26)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-logger/compare/v2.0.0...v2.0.1) (2022-08-12) + + +### Trivial Changes + +* **deps:** bump interface-datastore from 6.1.1 to 7.0.0 ([#7](https://github.com/libp2p/js-libp2p-logger/issues/7)) ([13aac56](https://github.com/libp2p/js-libp2p-logger/commit/13aac5661ecf77cd5a6d79eb92008435249eb428)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-logger/compare/v1.1.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p dependencies ([#4](https://github.com/libp2p/js-libp2p-logger/issues/4)) ([7775b9b](https://github.com/libp2p/js-libp2p-logger/commit/7775b9b2a51be5672bdc2640da85cae0510ef6bd)) + +### [1.1.6](https://github.com/libp2p/js-libp2p-logger/compare/v1.1.5...v1.1.6) (2022-06-09) + + +### Trivial Changes + +* add badge ([#1](https://github.com/libp2p/js-libp2p-logger/issues/1)) ([3109e30](https://github.com/libp2p/js-libp2p-logger/commit/3109e30508e32a8e24bd921afcd1b18df57483cd)) +* fix release ([3f850bc](https://github.com/libp2p/js-libp2p-logger/commit/3f850bc6d1c11248a6afb1d712e0074cf4b705af)) +* **release:** 1.0.0 [skip ci] ([91cfcba](https://github.com/libp2p/js-libp2p-logger/commit/91cfcba8635434262b11c40fe077c359e842e973)), closes [#1](https://github.com/libp2p/js-libp2p-logger/issues/1) + +## [@libp2p/logger-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.4...@libp2p/logger-v1.1.5) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/logger-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.3...@libp2p/logger-v1.1.4) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/logger-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.2...@libp2p/logger-v1.1.3) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/logger-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.1...@libp2p/logger-v1.1.2) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/logger-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.1.0...@libp2p/logger-v1.1.1) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/logger-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.4...@libp2p/logger-v1.1.0) (2022-02-26) + + +### Features + +* add trace option to logger ([#177](https://github.com/libp2p/js-libp2p-interfaces/issues/177)) ([19774eb](https://github.com/libp2p/js-libp2p-interfaces/commit/19774ebe05cc4ff8c8200dfdde046016abf5d19e)) + +## [@libp2p/logger-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.3...@libp2p/logger-v1.0.4) (2022-02-21) + + +### Bug Fixes + +* remove unused dht query option ([#176](https://github.com/libp2p/js-libp2p-interfaces/issues/176)) ([e0ce46d](https://github.com/libp2p/js-libp2p-interfaces/commit/e0ce46d371a92a7063f02e7a1729a39def80e15e)) + +## [@libp2p/logger-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.2...@libp2p/logger-v1.0.3) (2022-02-11) + + +### Bug Fixes + +* log cids ([#165](https://github.com/libp2p/js-libp2p-interfaces/issues/165)) ([68831e8](https://github.com/libp2p/js-libp2p-interfaces/commit/68831e804630e1f45ffee56a7585af62072d7145)) + +## [@libp2p/logger-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.1...@libp2p/logger-v1.0.2) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/logger-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/logger-v1.0.0...@libp2p/logger-v1.0.1) (2022-02-07) + + +### Bug Fixes + +* add logging formatters ([#159](https://github.com/libp2p/js-libp2p-interfaces/issues/159)) ([2fa518c](https://github.com/libp2p/js-libp2p-interfaces/commit/2fa518c7489dcd31d5b28f79114dfdc94133d784)) + +## @libp2p/logger-v1.0.0 (2022-02-07) + + +### Features + +* add logger package ([#158](https://github.com/libp2p/js-libp2p-interfaces/issues/158)) ([f327cd2](https://github.com/libp2p/js-libp2p-interfaces/commit/f327cd24825d9ce2f45a02fdb9b47c9735c847e0)) + +## @libp2p/tracked-map-v1.0.0 (2022-02-05) + + +### Features + +* add tracked-map ([#156](https://github.com/libp2p/js-libp2p-interfaces/issues/156)) ([c17730f](https://github.com/libp2p/js-libp2p-interfaces/commit/c17730f8bca172db85507740eaba81b3cf514d04)) diff --git a/packages/logger/LICENSE b/packages/logger/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/logger/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/logger/LICENSE-APACHE b/packages/logger/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/logger/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/logger/LICENSE-MIT b/packages/logger/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/logger/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 0000000000..def89c67a1 --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,77 @@ +# @libp2p/logger + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> A logging component for use in js-libp2p modules + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A map that reports it's size to the libp2p [Metrics](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/metrics#readme) system. + +If metrics are disabled a regular map is used. + +## Example + +```JavaScript +import { logger } from '@libp2p/logger' + +const log = logger('libp2p:my:component:name') + +log('something happened: %s', 'it was ok') +log.error('something bad happened: %o', err) + +log('with this peer: %p', aPeerId) +log('and this base58btc: %b', aUint8Array) +log('and this base32: %t', aUint8Array) +``` + +```console +$ DEBUG=libp2p:* node index.js +something happened: it was ok +something bad happened: +with this peer: 12D3Foo +with this base58btc: Qmfoo +with this base32: bafyfoo +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000000..0a24ab4199 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,67 @@ +{ + "name": "@libp2p/logger", + "version": "2.1.1", + "description": "A logging component for use in js-libp2p modules", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/logger#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "debug": "^4.3.4", + "interface-datastore": "^8.2.0", + "multiformats": "^11.0.2" + }, + "devDependencies": { + "@libp2p/peer-id": "^2.0.0", + "@types/debug": "^4.1.7", + "aegir": "^39.0.10", + "sinon": "^15.1.0", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000000..c50728146c --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,90 @@ +import debug from 'debug' +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' +import { base64 } from 'multiformats/bases/base64' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Key } from 'interface-datastore' +import type { CID } from 'multiformats/cid' + +// Add a formatter for converting to a base58 string +debug.formatters.b = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base58btc.baseEncode(v) +} + +// Add a formatter for converting to a base32 string +debug.formatters.t = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base32.baseEncode(v) +} + +// Add a formatter for converting to a base64 string +debug.formatters.m = (v?: Uint8Array): string => { + return v == null ? 'undefined' : base64.baseEncode(v) +} + +// Add a formatter for stringifying peer ids +debug.formatters.p = (v?: PeerId): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying CIDs +debug.formatters.c = (v?: CID): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying Datastore keys +debug.formatters.k = (v: Key): string => { + return v == null ? 'undefined' : v.toString() +} + +// Add a formatter for stringifying Multiaddrs +debug.formatters.a = (v?: Multiaddr): string => { + return v == null ? 'undefined' : v.toString() +} + +export interface Logger { + (formatter: any, ...args: any[]): void + error: (formatter: any, ...args: any[]) => void + trace: (formatter: any, ...args: any[]) => void + enabled: boolean +} + +function createDisabledLogger (namespace: string): debug.Debugger { + const logger = (): void => {} + logger.enabled = false + logger.color = '' + logger.diff = 0 + logger.log = (): void => {} + logger.namespace = namespace + logger.destroy = () => true + logger.extend = () => logger + + return logger +} + +export function logger (name: string): Logger { + // trace logging is a no-op by default + let trace: debug.Debugger = createDisabledLogger(`${name}:trace`) + + // look at all the debug names and see if trace logging has explicitly been enabled + if (debug.enabled(`${name}:trace`) && debug.names.map(r => r.toString()).find(n => n.includes(':trace')) != null) { + trace = debug(`${name}:trace`) + } + + return Object.assign(debug(name), { + error: debug(`${name}:error`), + trace + }) +} + +export function disable (): void { + debug.disable() +} + +export function enable (namespaces: string): void { + debug.enable(namespaces) +} + +export function enabled (namespaces: string): boolean { + return debug.enabled(namespaces) +} diff --git a/packages/logger/test/index.spec.ts b/packages/logger/test/index.spec.ts new file mode 100644 index 0000000000..8b8b1b6731 --- /dev/null +++ b/packages/logger/test/index.spec.ts @@ -0,0 +1,134 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import debug from 'debug' +import { Key } from 'interface-datastore' +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' +import { base64 } from 'multiformats/bases/base64' +import sinon from 'sinon' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as unint8ArrayToString } from 'uint8arrays/to-string' +import { logger } from '../src/index.js' + +describe('logger', () => { + it('creates a logger', () => { + const log = logger('hello') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.not.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.not.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with logging enabled', () => { + debug.enable('enabled-logger') + + const log = logger('enabled-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.not.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with logging and errors enabled', () => { + debug.enable('enabled-with-error-logger*') + + const log = logger('enabled-with-error-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.not.true() + }) + + it('creates a logger with trace enabled', () => { + debug.enable('enabled-with-trace-logger*,*:trace') + + const log = logger('enabled-with-trace-logger') + + expect(log).to.be.a('function') + expect(log).to.a.property('enabled').that.is.true() + expect(log).to.have.property('error').that.is.a('function') + expect(log).to.have.nested.property('error.enabled').that.is.true() + expect(log).to.have.property('trace').that.is.a('function') + expect(log).to.have.nested.property('trace.enabled').that.is.true() + }) + + it('has all formatters', () => { + debug.enable('enabled-with-formatters') + + expect(debug.formatters).to.have.property('b').that.is.a('function') + expect(debug.formatters).to.have.property('t').that.is.a('function') + expect(debug.formatters).to.have.property('m').that.is.a('function') + expect(debug.formatters).to.have.property('p').that.is.a('function') + expect(debug.formatters).to.have.property('c').that.is.a('function') + expect(debug.formatters).to.have.property('k').that.is.a('function') + expect(debug.formatters).to.have.property('a').that.is.a('function') + }) + + it('test printf style formatting', () => { + const log = logger('printf-style') + debug.enable('printf-style') + + const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') + + const debugSpy = sinon.spy(debug, 'log') + + log('multiaddr %a', ma) + + expect(debugSpy.firstCall.args[0], 'Multiaddr formatting not included').to.include(`multiaddr ${ma.toString()}`) + }) + + it('test ma formatter', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') + + expect(debug.formatters.a(ma)).to.equal(ma.toString()) + }) + + it('test peerId formatter', () => { + const peerId = peerIdFromString('QmZ8eiDPqQqWR17EPxiwCDgrKPVhCHLcyn6xSCNpFAdAZb') + + expect(debug.formatters.p(peerId)).to.equal(peerId.toString()) + }) + + it('test cid formatter', () => { + const peerId = peerIdFromString('QmZ8eiDPqQqWR17EPxiwCDgrKPVhCHLcyn6xSCNpFAdAZb') + const cid = peerId.toCID() + + expect(debug.formatters.c(cid)).to.equal(cid.toString()) + }) + + it('test base58 formatter', () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + + expect(debug.formatters.b(buf)).to.equal(base58btc.baseEncode(buf)) + }) + + it('test base32 formatter', () => { + const buf = uint8ArrayFromString('jbswy3dpfqqho33snrscc===', 'base32') + + expect(debug.formatters.t(buf)).to.equal(base32.baseEncode(buf)) + }) + + it('test base64 formatter', () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base64') + + expect(debug.formatters.m(buf)).to.equal(base64.baseEncode(buf)) + }) + + it('test datastore key formatter', () => { + const buf = uint8ArrayFromString('jbswy3dpfqqho33snrscc===', 'base32') + + const key = new Key('/' + unint8ArrayToString(buf, 'base32'), false) + + expect(debug.formatters.k(key)).to.equal(key.toString()) + }) +}) diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000000..38a80e7a63 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/metrics-prometheus/.aegir.js b/packages/metrics-prometheus/.aegir.js new file mode 100644 index 0000000000..000760bbdb --- /dev/null +++ b/packages/metrics-prometheus/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} \ No newline at end of file diff --git a/packages/metrics-prometheus/CHANGELOG.md b/packages/metrics-prometheus/CHANGELOG.md new file mode 100644 index 0000000000..237f9f466c --- /dev/null +++ b/packages/metrics-prometheus/CHANGELOG.md @@ -0,0 +1,86 @@ +## [1.1.5](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.4...v1.1.5) (2023-05-23) + + +### Bug Fixes + +* move prom-client to deps ([#32](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/32)) ([73acad0](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/73acad0a20a9a0ad024cd47a53f154668dbae77b)) + +## [1.1.4](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.3...v1.1.4) (2023-05-12) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7756331](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/77563319cdb0edcc75be7cd4ad7758054595991b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1a3861f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/1a3861fd7c76a8fa296ff3aad39de633aecf3570)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b66c4a0](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/b66c4a08983c1bbb2d1285aa2ea749ae00088643)) + + +### Documentation + +* added examples for package documentation for methods ([#31](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/31)) ([7dbd895](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/7dbd895dbf75f98b5730ad750b3e1aa9bc676c77)), closes [#30](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/30) + +## [1.1.3](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.2...v1.1.3) (2022-12-16) + + +### Documentation + +* publish api docs ([#14](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/14)) ([78e708f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/78e708f4a4b5040988da90ecfb636a1e59a96ee4)) + +## [1.1.2](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.1...v1.1.2) (2022-11-22) + + +### Bug Fixes + +* use collectDefaultMetrics option ([#7](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/7)) ([3e4f00c](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/3e4f00c539da19bdfd8a26d335f00a2457545b53)) + +## [1.1.1](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.1.0...v1.1.1) (2022-11-21) + + +### Bug Fixes + +* allow multiple consumers of metrics ([#6](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/6)) ([92bde9b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/92bde9b8d9a533c4e8aca6a98c02fa1bdc37156e)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.0.1...v1.1.0) (2022-11-21) + + +### Features + +* register metrics with custom registry ([#4](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/4)) ([5da2897](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/5da289702186b73862cce39ecd1752792e6f9751)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([351b00c](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/351b00cad878cd3269a18da3f725613f991a83ae)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-prometheus-metrics/compare/v1.0.0...v1.0.1) (2022-11-05) + + +### Bug Fixes + +* pass numbers to prom-client ([#1](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/1)) ([7c38140](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/7c38140d97dc4cbfb5d21e63a214df133eae9d73)) + +## 1.0.0 (2022-11-05) + + +### Bug Fixes + +* add tests for counters ([627b5c5](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/627b5c5886380433ef30efc80e8d32700f478f0b)) +* update release config ([ee1542d](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/ee1542d18863b3d9d12cdb2a8ebb21241c61d993)) + + +### Trivial Changes + +* add components ([92fccf7](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/92fccf71ef23ccf9ca819ccc050ce12ae088ed76)) +* fix tests ([f3f72f6](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/f3f72f6229969a6e947408cd3ba67a6b20607394)) +* initial implementation ([b3a4d8b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/b3a4d8b721b0974ce42a889d4d1029fe288553fe)) +* linting ([9758456](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/9758456c2cf6dee949967609dc88655a671d0b25)) +* linting ([0f84e4f](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/0f84e4f796801b87def8d4718e1e150a2af29065)) +* simplified metrics ([12e6077](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/12e6077318155bc844a0d100f1c00d1bf7789111)) +* stricter name/label parsing and tests ([0cf651d](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/0cf651de02102f406a45411ceb044a3a116a7436)) +* update comments ([10b8c98](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/10b8c98718ff3579b1a2b236ed294c319bdc4ac4)) +* update readme ([4176169](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/41761694d442f561a425c7bf6963e49627e8204e)) + + +### Documentation + +* update readme to link to correct branch ([1a7565b](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/1a7565b5986ba689eb7a6d555b15ca1a4e4d3f31)) diff --git a/packages/metrics-prometheus/LICENSE b/packages/metrics-prometheus/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/metrics-prometheus/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/metrics-prometheus/LICENSE-APACHE b/packages/metrics-prometheus/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/metrics-prometheus/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/metrics-prometheus/LICENSE-MIT b/packages/metrics-prometheus/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/metrics-prometheus/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/metrics-prometheus/README.md b/packages/metrics-prometheus/README.md new file mode 100644 index 0000000000..cd5a86c3b4 --- /dev/null +++ b/packages/metrics-prometheus/README.md @@ -0,0 +1,97 @@ +# @libp2p/prometheus-metrics + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Collect libp2p metrics for scraping by Prometheus or Graphana + +## Table of contents + +- [Install](#install) +- [Usage](#usage) + - [Queries](#queries) + - [Data sent/received](#data-sentreceived) + - [CPU usage](#cpu-usage) + - [Memory usage](#memory-usage) + - [DHT query time](#dht-query-time) + - [TCP transport dialer errors](#tcp-transport-dialer-errors) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/prometheus-metrics +``` + +## Usage + +Configure your libp2p node with Prometheus metrics: + +```js +import { createLibp2p } from 'libp2p' +import { prometheusMetrics } from '@libp2p/prometheus-metrics' + +const node = await createLibp2p({ + metrics: prometheusMetrics() +}) +``` + +Then use the `prom-client` module to supply metrics to the Prometheus/Graphana client using your http framework: + +```js +import client from 'prom-client' + +async handler (request, h) { + return h.response(await client.register.metrics()) + .type(client.register.contentType) +} +``` + +All Prometheus metrics are global so there's no other work required to extract them. + +### Queries + +Some useful queries are: + +#### Data sent/received + + rate(libp2p_data_transfer_bytes_total[30s]) + +#### CPU usage + + rate(process_cpu_user_seconds_total[30s]) * 100 + +#### Memory usage + + nodejs_memory_usage_bytes + +#### DHT query time + + libp2p_kad_dht_wan_query_time_seconds + +or + + libp2p_kad_dht_lan_query_time_seconds + +#### TCP transport dialer errors + + rate(libp2p_tcp_dialer_errors_total[30s]) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/metrics-prometheus/package.json b/packages/metrics-prometheus/package.json new file mode 100644 index 0000000000..a6c2fce62f --- /dev/null +++ b/packages/metrics-prometheus/package.json @@ -0,0 +1,64 @@ +{ + "name": "@libp2p/prometheus-metrics", + "version": "1.1.5", + "description": "Collect libp2p metrics for scraping by Prometheus or Graphana", + "author": "", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/metrics-prometheus#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main --cov" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/logger": "^2.0.0", + "it-foreach": "^2.0.3", + "it-stream-types": "^2.0.1", + "prom-client": "^14.1.0" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "aegir": "^39.0.10", + "it-drain": "^3.0.2", + "it-pipe": "^3.0.1", + "p-defer": "^4.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/metrics-prometheus/src/counter-group.ts b/packages/metrics-prometheus/src/counter-group.ts new file mode 100644 index 0000000000..6cd52abdde --- /dev/null +++ b/packages/metrics-prometheus/src/counter-group.ts @@ -0,0 +1,58 @@ +import { Counter as PromCounter, type CollectFunction } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CounterGroup, CalculateMetric } from '@libp2p/interface-metrics' + +export class PrometheusCounterGroup implements CounterGroup, CalculatedMetric> { + private readonly counter: PromCounter + private readonly label: string + private readonly calculators: Array>> + + constructor (name: string, opts: PrometheusCalculatedMetricOptions>) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const label = this.label = normaliseString(opts.label ?? name) + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + await Promise.all(self.calculators.map(async calculate => { + const values = await calculate() + + Object.entries(values).forEach(([key, value]) => { + this.inc({ [label]: key }, value) + }) + })) + } + } + + this.counter = new PromCounter({ + name, + help, + labelNames: [this.label], + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric>): void { + this.calculators.push(calculator) + } + + increment (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const inc = typeof value === 'number' ? value : 1 + + this.counter.inc({ [this.label]: key }, inc) + }) + } + + reset (): void { + this.counter.reset() + } +} diff --git a/packages/metrics-prometheus/src/counter.ts b/packages/metrics-prometheus/src/counter.ts new file mode 100644 index 0000000000..3dacf85322 --- /dev/null +++ b/packages/metrics-prometheus/src/counter.ts @@ -0,0 +1,50 @@ +import { type CollectFunction, Counter as PromCounter } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CalculateMetric, Counter } from '@libp2p/interface-metrics' + +export class PrometheusCounter implements Counter, CalculatedMetric { + private readonly counter: PromCounter + private readonly calculators: CalculateMetric[] + + constructor (name: string, opts: PrometheusCalculatedMetricOptions) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const labels = opts.label != null ? [normaliseString(opts.label)] : [] + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + const values = await Promise.all(self.calculators.map(async calculate => calculate())) + const sum = values.reduce((acc, curr) => acc + curr, 0) + + this.inc(sum) + } + } + + this.counter = new PromCounter({ + name, + help, + labelNames: labels, + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric): void { + this.calculators.push(calculator) + } + + increment (value: number = 1): void { + this.counter.inc(value) + } + + reset (): void { + this.counter.reset() + } +} diff --git a/packages/metrics-prometheus/src/index.ts b/packages/metrics-prometheus/src/index.ts new file mode 100644 index 0000000000..e8fad3b0bd --- /dev/null +++ b/packages/metrics-prometheus/src/index.ts @@ -0,0 +1,357 @@ +/** + * @packageDocumentation + * + * Collect libp2p metrics for scraping by Prometheus or Graphana. + * @module libp2p-prometheus-metrics + * + * A tracked metric can be created by calling either `registerMetric` on the metrics object + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetric = metrics.registerMetric({ + * name: 'my_metric', + * label: 'my_label', + * help: 'my help text' + * }) + * + * myMetric.update(1) + * ``` + * A metric that is expensive to calculate can be created by passing a `calculate` function that will only be invoked when metrics are being scraped: + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetric = metrics.registerMetric({ + * name: 'my_metric', + * label: 'my_label', + * help: 'my help text', + * calculate: async () => { + * // do something expensive + * return 1 + * } + * }) + * ``` + * + * If several metrics should be grouped together (e.g. for graphing purposes) `registerMetricGroup` can be used instead: + * + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * + * const metrics = prometheusMetrics()() + * const myMetricGroup = metrics.registerMetricGroup({ + * name: 'my_metric_group', + * label: 'my_label', + * help: 'my help text' + * }) + * + * myMetricGroup.increment({ my_label: 'my_value' }) + * ``` + * + * There are specific metric groups for tracking libp2p connections and streams: + * + * Track a newly opened multiaddr connection: + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * import { createLibp2p } from 'libp2p' + * + * + * const metrics = prometheusMetrics()() + * + * const libp2p = await createLibp2p({ + * metrics: metrics, + * }) + * // set up a multiaddr connection + * const connection = await libp2p.dial('multiaddr') + * const connections = metrics.trackMultiaddrConnection(connection) + * ``` + * + * Track a newly opened stream: + * @example + * + * ```typescript + * import { prometheusMetrics } from '@libp2p/prometheus-metrics' + * import { createLibp2p } from 'libp2p' + * + * const metrics = prometheusMetrics()() + * + * const libp2p = await createLibp2p({ + * metrics: metrics, + * }) + * + * const stream = await connection.newStream('/my/protocol') + * const streams = metrics.trackProtocolStream(stream) + * ``` + */ + +import { logger } from '@libp2p/logger' +import each from 'it-foreach' +import { collectDefaultMetrics, type DefaultMetricsCollectorConfiguration, register, type Registry } from 'prom-client' +import { PrometheusCounterGroup } from './counter-group.js' +import { PrometheusCounter } from './counter.js' +import { PrometheusMetricGroup } from './metric-group.js' +import { PrometheusMetric } from './metric.js' +import type { MultiaddrConnection, Stream, Connection } from '@libp2p/interface-connection' +import type { CalculatedMetricOptions, Counter, CounterGroup, Metric, MetricGroup, MetricOptions, Metrics } from '@libp2p/interface-metrics' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:prometheus-metrics') + +// prom-client metrics are global +const metrics = new Map() + +export interface PrometheusMetricsInit { + /** + * Use a custom registry to register metrics. + * By default, the global registry is used to register metrics. + */ + registry?: Registry + + /** + * By default we collect default metrics - CPU, memory etc, to not do + * this, pass true here + */ + collectDefaultMetrics?: boolean + + /** + * prom-client options to pass to the `collectDefaultMetrics` function + */ + defaultMetrics?: DefaultMetricsCollectorConfiguration + + /** + * All metrics in prometheus are global so to prevent clashes in naming + * we reset the global metrics registry on creation - to not do this, + * pass true here + */ + preserveExistingMetrics?: boolean +} + +export interface PrometheusCalculatedMetricOptions extends CalculatedMetricOptions { + registry?: Registry +} + +class PrometheusMetrics implements Metrics { + private transferStats: Map + private readonly registry?: Registry + + constructor (init?: Partial) { + this.registry = init?.registry + + if (init?.preserveExistingMetrics !== true) { + log('Clearing existing metrics') + metrics.clear() + ;(this.registry ?? register).clear() + } + + if (init?.collectDefaultMetrics !== false) { + log('Collecting default metrics') + collectDefaultMetrics({ ...init?.defaultMetrics, register: this.registry ?? init?.defaultMetrics?.register }) + } + + // holds global and per-protocol sent/received stats + this.transferStats = new Map() + + log('Collecting data transfer metrics') + this.registerCounterGroup('libp2p_data_transfer_bytes_total', { + label: 'protocol', + calculate: () => { + const output: Record = {} + + for (const [key, value] of this.transferStats.entries()) { + output[key] = value + } + + // reset counts for next time + this.transferStats = new Map() + + return output + } + }) + + log('Collecting memory metrics') + this.registerMetricGroup('nodejs_memory_usage_bytes', { + label: 'memory', + calculate: () => { + return { + ...process.memoryUsage() + } + } + }) + } + + /** + * Increment the transfer stat for the passed key, making sure + * it exists first + */ + _incrementValue (key: string, value: number): void { + const existing = this.transferStats.get(key) ?? 0 + + this.transferStats.set(key, existing + value) + } + + /** + * Override the sink/source of the stream to count the bytes + * in and out + */ + _track (stream: Duplex>, name: string): void { + const self = this + + const sink = stream.sink + stream.sink = async function trackedSink (source) { + await sink(each(source, buf => { + self._incrementValue(`${name} sent`, buf.byteLength) + })) + } + + const source = stream.source + stream.source = each(source, buf => { + self._incrementValue(`${name} received`, buf.byteLength) + }) + } + + trackMultiaddrConnection (maConn: MultiaddrConnection): void { + this._track(maConn, 'global') + } + + trackProtocolStream (stream: Stream, connection: Connection): void { + if (stream.stat.protocol == null) { + // protocol not negotiated yet, should not happen as the upgrader + // calls this handler after protocol negotiation + return + } + + this._track(stream, stream.stat.protocol) + } + + registerMetric (name: string, opts: PrometheusCalculatedMetricOptions): void + registerMetric (name: string, opts?: MetricOptions): Metric + registerMetric (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Metric name is required') + } + + let metric = metrics.get(name) + + if (metrics.has(name)) { + log('Reuse existing metric', name) + + if (opts.calculate != null) { + metric.addCalculator(opts.calculate) + } + + return metrics.get(name) + } + + log('Register metric', name) + metric = new PrometheusMetric(name, { registry: this.registry, ...opts }) + + metrics.set(name, metric) + + if (opts.calculate == null) { + return metric + } + } + + registerMetricGroup (name: string, opts: PrometheusCalculatedMetricOptions>): void + registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup + registerMetricGroup (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Metric group name is required') + } + + let metricGroup = metrics.get(name) + + if (metricGroup != null) { + log('Reuse existing metric group', name) + + if (opts.calculate != null) { + metricGroup.addCalculator(opts.calculate) + } + + return metricGroup + } + + log('Register metric group', name) + metricGroup = new PrometheusMetricGroup(name, { registry: this.registry, ...opts }) + + metrics.set(name, metricGroup) + + if (opts.calculate == null) { + return metricGroup + } + } + + registerCounter (name: string, opts: PrometheusCalculatedMetricOptions): void + registerCounter (name: string, opts?: MetricOptions): Counter + registerCounter (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Counter name is required') + } + + let counter = metrics.get(name) + + if (counter != null) { + log('Reuse existing counter', name) + + if (opts.calculate != null) { + counter.addCalculator(opts.calculate) + } + + return metrics.get(name) + } + + log('Register counter', name) + counter = new PrometheusCounter(name, { registry: this.registry, ...opts }) + + metrics.set(name, counter) + + if (opts.calculate == null) { + return counter + } + } + + registerCounterGroup (name: string, opts: PrometheusCalculatedMetricOptions>): void + registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup + registerCounterGroup (name: string, opts: any = {}): any { + if (name == null ?? name.trim() === '') { + throw new Error('Counter group name is required') + } + + let counterGroup = metrics.get(name) + + if (counterGroup != null) { + log('Reuse existing counter group', name) + + if (opts.calculate != null) { + counterGroup.addCalculator(opts.calculate) + } + + return counterGroup + } + + log('Register counter group', name) + counterGroup = new PrometheusCounterGroup(name, { registry: this.registry, ...opts }) + + metrics.set(name, counterGroup) + + if (opts.calculate == null) { + return counterGroup + } + } +} + +export function prometheusMetrics (init?: Partial): () => Metrics { + return () => { + return new PrometheusMetrics(init) + } +} diff --git a/packages/metrics-prometheus/src/metric-group.ts b/packages/metrics-prometheus/src/metric-group.ts new file mode 100644 index 0000000000..302def85fc --- /dev/null +++ b/packages/metrics-prometheus/src/metric-group.ts @@ -0,0 +1,78 @@ +import { type CollectFunction, Gauge } from 'prom-client' +import { normaliseString, type CalculatedMetric } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { CalculateMetric, MetricGroup, StopTimer } from '@libp2p/interface-metrics' + +export class PrometheusMetricGroup implements MetricGroup, CalculatedMetric> { + private readonly gauge: Gauge + private readonly label: string + private readonly calculators: Array>> + + constructor (name: string, opts: PrometheusCalculatedMetricOptions>) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const label = this.label = normaliseString(opts.label ?? name) + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + await Promise.all(self.calculators.map(async calculate => { + const values = await calculate() + + Object.entries(values).forEach(([key, value]) => { + this.set({ [label]: key }, value) + }) + })) + } + } + + this.gauge = new Gauge({ + name, + help, + labelNames: [this.label], + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric>): void { + this.calculators.push(calculator) + } + + update (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + this.gauge.set({ [this.label]: key }, value) + }) + } + + increment (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const inc = typeof value === 'number' ? value : 1 + + this.gauge.inc({ [this.label]: key }, inc) + }) + } + + decrement (values: Record): void { + Object.entries(values).forEach(([key, value]) => { + const dec = typeof value === 'number' ? value : 1 + + this.gauge.dec({ [this.label]: key }, dec) + }) + } + + reset (): void { + this.gauge.reset() + } + + timer (key: string): StopTimer { + return this.gauge.startTimer({ + key: 0 + }) + } +} diff --git a/packages/metrics-prometheus/src/metric.ts b/packages/metrics-prometheus/src/metric.ts new file mode 100644 index 0000000000..bc65b5c916 --- /dev/null +++ b/packages/metrics-prometheus/src/metric.ts @@ -0,0 +1,62 @@ +import { type CollectFunction, Gauge } from 'prom-client' +import { normaliseString } from './utils.js' +import type { PrometheusCalculatedMetricOptions } from './index.js' +import type { Metric, StopTimer, CalculateMetric } from '@libp2p/interface-metrics' + +export class PrometheusMetric implements Metric { + private readonly gauge: Gauge + private readonly calculators: CalculateMetric[] + + constructor (name: string, opts: PrometheusCalculatedMetricOptions) { + name = normaliseString(name) + const help = normaliseString(opts.help ?? name) + const labels = opts.label != null ? [normaliseString(opts.label)] : [] + let collect: CollectFunction> | undefined + this.calculators = [] + + // calculated metric + if (opts?.calculate != null) { + this.calculators.push(opts.calculate) + const self = this + + collect = async function () { + const values = await Promise.all(self.calculators.map(async calculate => calculate())) + const sum = values.reduce((acc, curr) => acc + curr, 0) + + this.set(sum) + } + } + + this.gauge = new Gauge({ + name, + help, + labelNames: labels, + registers: opts.registry !== undefined ? [opts.registry] : undefined, + collect + }) + } + + addCalculator (calculator: CalculateMetric): void { + this.calculators.push(calculator) + } + + update (value: number): void { + this.gauge.set(value) + } + + increment (value: number = 1): void { + this.gauge.inc(value) + } + + decrement (value: number = 1): void { + this.gauge.dec(value) + } + + reset (): void { + this.gauge.reset() + } + + timer (): StopTimer { + return this.gauge.startTimer() + } +} diff --git a/packages/metrics-prometheus/src/utils.ts b/packages/metrics-prometheus/src/utils.ts new file mode 100644 index 0000000000..4ec136b846 --- /dev/null +++ b/packages/metrics-prometheus/src/utils.ts @@ -0,0 +1,18 @@ +import type { CalculateMetric } from '@libp2p/interface-metrics' + +export interface CalculatedMetric { + addCalculator: (calculator: CalculateMetric) => void +} + +export const ONE_SECOND = 1000 +export const ONE_MINUTE = 60 * ONE_SECOND + +/** + * See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + * for rules on valid naming + */ +export function normaliseString (str: string): string { + return str + .replace(/[^a-zA-Z0-9_]/g, '_') + .replace(/_+/g, '_') +} diff --git a/packages/metrics-prometheus/test/counter-groups.spec.ts b/packages/metrics-prometheus/test/counter-groups.spec.ts new file mode 100644 index 0000000000..dbb618bd86 --- /dev/null +++ b/packages/metrics-prometheus/test/counter-groups.spec.ts @@ -0,0 +1,120 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('counter groups', () => { + it('should increment a counter group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: true + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} 1`, 'did not include updated metric') + }) + + it('should increment a counter group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a counter group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounterGroup(metricName, { + label: metricLabel, + calculate: () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a counter group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounterGroup(metricName, { + label: metricLabel, + calculate: async () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a counter group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + + metric.reset() + + await expect(client.register.metrics()).to.eventually.not.include(metricKey, 'still included metric key') + }) + + it('should allow use of the same counter group from multiple reporters', async () => { + const metricName = randomMetricName() + const metricKey1 = randomMetricName('key_') + const metricKey2 = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric1.increment({ + [metricKey1]: metricValue1 + }) + const metric2 = metrics.registerCounterGroup(metricName, { + label: metricLabel + }) + metric2.increment({ + [metricKey2]: metricValue2 + }) + + const reportedMetrics = await client.register.metrics() + + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey1}"} ${metricValue1}`, 'did not include updated metric') + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey2}"} ${metricValue2}`, 'did not include updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/counters.spec.ts b/packages/metrics-prometheus/test/counters.spec.ts new file mode 100644 index 0000000000..73618430ed --- /dev/null +++ b/packages/metrics-prometheus/test/counters.spec.ts @@ -0,0 +1,85 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('counters', () => { + it('should set a counter', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment() + + const report = await client.register.metrics() + expect(report).to.include(`# TYPE ${metricName} counter`, 'did not include metric type') + expect(report).to.include(`${metricName} 1`, 'did not include updated metric') + }) + + it('should increment a counter with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounter(metricName, { + calculate: () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerCounter(metricName, { + calculate: async () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a counter', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerCounter(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`) + + metric.reset() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 0`, 'did not include updated metric') + }) + + it('should allow use of the same counter from multiple reporters', async () => { + const metricName = randomMetricName() + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerCounter(metricName, { + label: metricLabel + }) + metric1.increment(metricValue1) + const metric2 = metrics.registerCounter(metricName, { + label: metricLabel + }) + metric2.increment(metricValue2) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue1 + metricValue2}`) + }) +}) diff --git a/packages/metrics-prometheus/test/custom-registry.spec.ts b/packages/metrics-prometheus/test/custom-registry.spec.ts new file mode 100644 index 0000000000..754179654b --- /dev/null +++ b/packages/metrics-prometheus/test/custom-registry.spec.ts @@ -0,0 +1,23 @@ +import { expect } from 'aegir/chai' +import client, { Registry } from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('custom registry', () => { + it('should set a metric in the custom registry and not in the global registry', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const registry = new Registry() + const metrics = prometheusMetrics({ registry })() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + const customRegistryReport = await registry.metrics() + expect(customRegistryReport).to.include(`# TYPE ${metricName} gauge`, 'did not include metric type') + expect(customRegistryReport).to.include(`${metricName} ${metricValue}`, 'did not include updated metric') + + const globalRegistryReport = await client.register.metrics() + expect(globalRegistryReport).to.not.include(`# TYPE ${metricName} gauge`, 'erroneously includes metric type') + expect(globalRegistryReport).to.not.include(`${metricName} ${metricValue}`, 'erroneously includes updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/fixtures/random-metric-name.ts b/packages/metrics-prometheus/test/fixtures/random-metric-name.ts new file mode 100644 index 0000000000..be87c5d21b --- /dev/null +++ b/packages/metrics-prometheus/test/fixtures/random-metric-name.ts @@ -0,0 +1,7 @@ +/** + * Prometheus metric names are global and can only contain + * a limited set of, at least /a-z_0-9/i + */ +export function randomMetricName (key = ''): string { + return `my_metric_${key}${Math.random().toString().split('.').pop() ?? ''}` +} diff --git a/packages/metrics-prometheus/test/metric-groups.spec.ts b/packages/metrics-prometheus/test/metric-groups.spec.ts new file mode 100644 index 0000000000..b609da5119 --- /dev/null +++ b/packages/metrics-prometheus/test/metric-groups.spec.ts @@ -0,0 +1,167 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('metric groups', () => { + it('should set a metric group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.update({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should increment a metric group without a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: false + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} 1`, 'did not include updated metric') + }) + + it('should increment a metric group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.increment({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should decrement a metric group without a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.decrement({ + [metricKey]: false + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} -1`, 'did not include updated metric') + }) + + it('should decrement a metric group with a value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.decrement({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} -${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a metric group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetricGroup(metricName, { + label: metricLabel, + calculate: () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a metric group value', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetricGroup(metricName, { + label: metricLabel, + calculate: async () => { + return { + [metricKey]: metricValue + } + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a metric group', async () => { + const metricName = randomMetricName() + const metricKey = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric.update({ + [metricKey]: metricValue + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName}{${metricLabel}="${metricKey}"} ${metricValue}`, 'did not include updated metric') + + metric.reset() + + await expect(client.register.metrics()).to.eventually.not.include(metricKey, 'still included metric key') + }) + + it('should allow use of the same metric group from multiple reporters', async () => { + const metricName = randomMetricName() + const metricKey1 = randomMetricName('key_') + const metricKey2 = randomMetricName('key_') + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric1.update({ + [metricKey1]: metricValue1 + }) + const metric2 = metrics.registerMetricGroup(metricName, { + label: metricLabel + }) + metric2.update({ + [metricKey2]: metricValue2 + }) + + const reportedMetrics = await client.register.metrics() + + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey1}"} ${metricValue1}`, 'did not include updated metric') + expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey2}"} ${metricValue2}`, 'did not include updated metric') + }) +}) diff --git a/packages/metrics-prometheus/test/metrics.spec.ts b/packages/metrics-prometheus/test/metrics.spec.ts new file mode 100644 index 0000000000..b238cb937e --- /dev/null +++ b/packages/metrics-prometheus/test/metrics.spec.ts @@ -0,0 +1,114 @@ +import { expect } from 'aegir/chai' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import { randomMetricName } from './fixtures/random-metric-name.js' + +describe('metrics', () => { + it('should set a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + const report = await client.register.metrics() + expect(report).to.include(`# TYPE ${metricName} gauge`, 'did not include metric type') + expect(report).to.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should increment a metric without a value', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.increment() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 1`, 'did not include updated metric') + }) + + it('should increment a metric with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.increment(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should decrement a metric without a value', async () => { + const metricName = randomMetricName() + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.decrement() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} -1`, 'did not include updated metric') + }) + + it('should decrement a metric with a value', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.decrement(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} -${metricValue}`, 'did not include updated metric') + }) + + it('should calculate a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetric(metricName, { + calculate: () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should promise to calculate a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + metrics.registerMetric(metricName, { + calculate: async () => { + return metricValue + } + }) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`, 'did not include updated metric') + }) + + it('should reset a metric', async () => { + const metricName = randomMetricName() + const metricValue = 5 + const metrics = prometheusMetrics()() + const metric = metrics.registerMetric(metricName) + metric.update(metricValue) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue}`) + + metric.reset() + + await expect(client.register.metrics()).to.eventually.include(`${metricName} 0`, 'did not include updated metric') + }) + + it('should allow use of the same metric from multiple reporters', async () => { + const metricName = randomMetricName() + const metricLabel = randomMetricName('label_') + const metricValue1 = 5 + const metricValue2 = 7 + const metrics = prometheusMetrics()() + const metric1 = metrics.registerMetric(metricName, { + label: metricLabel + }) + metric1.update(metricValue1) + const metric2 = metrics.registerMetric(metricName, { + label: metricLabel + }) + metric2.update(metricValue2) + + await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue2}`) + }) +}) diff --git a/packages/metrics-prometheus/test/streams.spec.ts b/packages/metrics-prometheus/test/streams.spec.ts new file mode 100644 index 0000000000..5cfba71e83 --- /dev/null +++ b/packages/metrics-prometheus/test/streams.spec.ts @@ -0,0 +1,167 @@ +import { connectionPair, mockRegistrar, mockMultiaddrConnPair } from '@libp2p/interface-mocks' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import drain from 'it-drain' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import client from 'prom-client' +import { prometheusMetrics } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' + +describe('streams', () => { + let connectionA: Connection + let connectionB: Connection + + afterEach(async () => { + if (connectionA != null) { + await connectionA.close() + } + + if (connectionB != null) { + await connectionB.close() + } + }) + + it('should track bytes sent over connections', async () => { + const deferred = defer() + const remotePeer = await createEd25519PeerId() + + const { outbound, inbound } = mockMultiaddrConnPair({ + addrs: [ + multiaddr('/ip4/123.123.123.123/tcp/5923'), + multiaddr('/ip4/123.123.123.123/tcp/5924') + ], + remotePeer + }) + + // process all the bytes + void pipe(inbound, drain).then(() => { + deferred.resolve() + }) + + const metrics = prometheusMetrics()() + + // track outgoing stream + metrics.trackMultiaddrConnection(outbound) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await outbound.sink([ + data + ]) + + // wait for all bytes to be received + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="global sent"} ${data.length}`) + }) + + it('should track bytes received over connections', async () => { + const deferred = defer() + const remotePeer = await createEd25519PeerId() + + const { outbound, inbound } = mockMultiaddrConnPair({ + addrs: [ + multiaddr('/ip4/123.123.123.123/tcp/5923'), + multiaddr('/ip4/123.123.123.123/tcp/5924') + ], + remotePeer + }) + + const metrics = prometheusMetrics()() + + // track incoming stream + metrics.trackMultiaddrConnection(inbound) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await outbound.sink([ + data + ]) + + // process all the bytes + void pipe(inbound, drain).then(() => { + deferred.resolve() + }) + + // wait for all bytes to be received + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="global received"} ${data.length}`) + }) + + it('should track sent stream metrics', async () => { + const protocol = '/my-protocol-send/1.0.0' + const peerA = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + const peerB = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + await peerB.registrar.handle(protocol, ({ stream }) => { + void pipe(stream, drain) + }) + + ;[connectionA, connectionB] = connectionPair(peerA, peerB) + const aToB = await connectionA.newStream(protocol) + + const metrics = prometheusMetrics()() + + // track outgoing stream + metrics.trackProtocolStream(aToB, connectionA) + + // send data to the remote over the tracked stream + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await aToB.sink([ + data + ]) + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="${protocol} sent"} ${data.length}`) + }) + + it('should track sent received metrics', async () => { + const deferred = defer() + const protocol = '/my-protocol-receive/1.0.0' + const peerA = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + await peerA.registrar.handle(protocol, ({ stream, connection }) => { + // track incoming stream + metrics.trackProtocolStream(stream, connectionA) + + // ignore data + void pipe(stream, drain).then(() => { + deferred.resolve() + }) + }) + const peerB = { + peerId: await createEd25519PeerId(), + registrar: mockRegistrar() + } + + const metrics = prometheusMetrics()() + + ;[connectionA, connectionB] = connectionPair(peerA, peerB) + + const bToA = await connectionB.newStream(protocol) + + // send data from remote to local + const data = Uint8Array.from([0, 1, 2, 3, 4]) + await bToA.sink([ + data + ]) + + // wait for data to have been transferred + await deferred.promise + + const scrapedMetrics = await client.register.metrics() + expect(scrapedMetrics).to.include(`libp2p_data_transfer_bytes_total{protocol="${protocol} received"} ${data.length}`) + }) +}) diff --git a/packages/metrics-prometheus/test/utils.spec.ts b/packages/metrics-prometheus/test/utils.spec.ts new file mode 100644 index 0000000000..20da4a9340 --- /dev/null +++ b/packages/metrics-prometheus/test/utils.spec.ts @@ -0,0 +1,12 @@ +import { expect } from 'aegir/chai' +import { normaliseString } from '../src/utils.js' + +describe('utils', () => { + describe('normaliseString', () => { + it('should normalise string', () => { + expect(normaliseString('hello-world')).to.equal('hello_world') + expect(normaliseString('hello---world')).to.equal('hello_world') + expect(normaliseString('hello-world_0.0.0.0:1234-some-metric')).to.equal('hello_world_0_0_0_0_1234_some_metric') + }) + }) +}) diff --git a/packages/metrics-prometheus/tsconfig.json b/packages/metrics-prometheus/tsconfig.json new file mode 100644 index 0000000000..20f43dc904 --- /dev/null +++ b/packages/metrics-prometheus/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/multistream-select/CHANGELOG.md b/packages/multistream-select/CHANGELOG.md new file mode 100644 index 0000000000..2b8fdbdeaf --- /dev/null +++ b/packages/multistream-select/CHANGELOG.md @@ -0,0 +1,200 @@ +## [3.1.9](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.8...v3.1.9) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([c03b6cd](https://github.com/libp2p/js-libp2p-multistream-select/commit/c03b6cd8013a82605f414a5ddbde7c66c84e4db1)) +* Update .github/workflows/stale.yml [skip ci] ([e8d5014](https://github.com/libp2p/js-libp2p-multistream-select/commit/e8d5014b6da7bf4db1cc542c5d923760a6067903)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#70](https://github.com/libp2p/js-libp2p-multistream-select/issues/70)) ([f87b1c3](https://github.com/libp2p/js-libp2p-multistream-select/commit/f87b1c3505934ebeed6eff018af8d3042e7e6e06)) + +## [3.1.8](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.7...v3.1.8) (2023-04-19) + + +### Dependencies + +* update abortable iterator to 5.x.x ([#61](https://github.com/libp2p/js-libp2p-multistream-select/issues/61)) ([5bc4293](https://github.com/libp2p/js-libp2p-multistream-select/commit/5bc42936e25e14791d19fdd790d3c3987c56e784)) + +## [3.1.7](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.6...v3.1.7) (2023-04-18) + + +### Bug Fixes + +* specify protocol stream sink return type ([#60](https://github.com/libp2p/js-libp2p-multistream-select/issues/60)) ([12d6b9c](https://github.com/libp2p/js-libp2p-multistream-select/commit/12d6b9c4ea26b26d0428df2d05c3078464068392)) + +## [3.1.6](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.5...v3.1.6) (2023-04-18) + + +### Dependencies + +* bump it-stream-types from 1.0.5 to 2.0.1 ([#58](https://github.com/libp2p/js-libp2p-multistream-select/issues/58)) ([0b0ebca](https://github.com/libp2p/js-libp2p-multistream-select/commit/0b0ebcadd0ccbbfd373ebbf8e9fb5a8b793fc924)) + +## [3.1.5](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.4...v3.1.5) (2023-04-17) + + +### Bug Fixes + +* use trace logging for happy paths ([#59](https://github.com/libp2p/js-libp2p-multistream-select/issues/59)) ([184ef21](https://github.com/libp2p/js-libp2p-multistream-select/commit/184ef21c930c1557d657ce0891471d86f76fb271)) + +## [3.1.4](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.3...v3.1.4) (2023-04-03) + + +### Dependencies + +* update all it-* deps to the latest versions ([#57](https://github.com/libp2p/js-libp2p-multistream-select/issues/57)) ([cf9133a](https://github.com/libp2p/js-libp2p-multistream-select/commit/cf9133a00b73c9e6d7576b57d2dccd9e87ccd01e)) + +## [3.1.3](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.2...v3.1.3) (2023-03-31) + + +### Trivial Changes + +* replace err-code with CodeError ([#36](https://github.com/libp2p/js-libp2p-multistream-select/issues/36)) ([fc2aefd](https://github.com/libp2p/js-libp2p-multistream-select/commit/fc2aefdec0db9a2b39fe8259881cf3a2693027cb)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1861a94](https://github.com/libp2p/js-libp2p-multistream-select/commit/1861a945fd8fef3d407591632d92f080d07e0bed)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([0f312c0](https://github.com/libp2p/js-libp2p-multistream-select/commit/0f312c08f3760f188304074088060f3d701e5815)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6a277a6](https://github.com/libp2p/js-libp2p-multistream-select/commit/6a277a6efdcbd3afef72335699d3a61e4bbea609)) + + +### Dependencies + +* bump it-merge from 2.0.1 to 3.0.0 ([#51](https://github.com/libp2p/js-libp2p-multistream-select/issues/51)) ([129166b](https://github.com/libp2p/js-libp2p-multistream-select/commit/129166ba5366d29d20e2629ce1f542c57cc864ba)) +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#50](https://github.com/libp2p/js-libp2p-multistream-select/issues/50)) ([d8420a0](https://github.com/libp2p/js-libp2p-multistream-select/commit/d8420a03207be7ee3472c4bb7a4f3cc0912758a1)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.1...v3.1.2) (2022-12-16) + + +### Trivial Changes + +* log invalid buffer ([#30](https://github.com/libp2p/js-libp2p-multistream-select/issues/30)) ([1fce957](https://github.com/libp2p/js-libp2p-multistream-select/commit/1fce9579eefe32a81b9805edc6a348f37605ac7f)) +* update it-* deps ([#31](https://github.com/libp2p/js-libp2p-multistream-select/issues/31)) ([3caf904](https://github.com/libp2p/js-libp2p-multistream-select/commit/3caf904c20aab7dc4ca61f40420b18e84bbd2c49)) + + +### Documentation + +* publish api docs ([#35](https://github.com/libp2p/js-libp2p-multistream-select/issues/35)) ([c4c978a](https://github.com/libp2p/js-libp2p-multistream-select/commit/c4c978ac1eb84667d5568c5f68a6678cf460380f)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.1.0...v3.1.1) (2022-10-31) + + +### Bug Fixes + +* set min and max protocol length ([#21](https://github.com/libp2p/js-libp2p-multistream-select/issues/21)) ([ae42f76](https://github.com/libp2p/js-libp2p-multistream-select/commit/ae42f7623b557d33208c12c69d7f01e49f478fdb)) + + +### Trivial Changes + +* update to handshake 4.1.2 ([#28](https://github.com/libp2p/js-libp2p-multistream-select/issues/28)) ([53883b1](https://github.com/libp2p/js-libp2p-multistream-select/commit/53883b1c6215584043f4dd6e97e2d10adb890af6)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v3.0.0...v3.1.0) (2022-10-12) + + +### Features + +* add lazy select ([#18](https://github.com/libp2p/js-libp2p-multistream-select/issues/18)) ([d3bff7c](https://github.com/libp2p/js-libp2p-multistream-select/commit/d3bff7cc3cd5afe6ebc1355241030868ec0aa572)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ba9ea12](https://github.com/libp2p/js-libp2p-multistream-select/commit/ba9ea12b2b55602bbeb6c9227976419851496783)) + + +### Dependencies + +* bump uint8arrays from 3.x.x to 4.x.x ([#22](https://github.com/libp2p/js-libp2p-multistream-select/issues/22)) ([cfb887b](https://github.com/libp2p/js-libp2p-multistream-select/commit/cfb887b9bc01f8234838049c59866064db97bdf5)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.2...v3.0.0) (2022-08-06) + + +### ⚠ BREAKING CHANGES + +* the single-method Listener and Dialer classes have been removed and their methods exported instead + +### Bug Fixes + +* support Duplex and Duplex ([#17](https://github.com/libp2p/js-libp2p-multistream-select/issues/17)) ([6e96c89](https://github.com/libp2p/js-libp2p-multistream-select/commit/6e96c89b68a77ea5192e91cab5547e78f5b078fd)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.1...v2.0.2) (2022-07-31) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-multistream-select/issues/14)) ([4d4ef28](https://github.com/libp2p/js-libp2p-multistream-select/commit/4d4ef28af8cb8d0f57e06d9ae161ba31e2c5e814)) + + +### Dependencies + +* update it-length-prefixed deps to support no-copy ops ([#16](https://github.com/libp2p/js-libp2p-multistream-select/issues/16)) ([2946064](https://github.com/libp2p/js-libp2p-multistream-select/commit/2946064a8993b4ec70ebfd3e5a34d86db1ee7fe6)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-multistream-select/compare/v2.0.0...v2.0.1) (2022-06-17) + + +### Trivial Changes + +* update deps ([#9](https://github.com/libp2p/js-libp2p-multistream-select/issues/9)) ([dc5ddc1](https://github.com/libp2p/js-libp2p-multistream-select/commit/dc5ddc1b93da82a98e5acddc25a8e41c6eb67044)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-multistream-select/compare/v1.0.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* updates to single-issue libp2p interfaces and ls has been removed + +### Features + +* update interfaces, remove ls ([#3](https://github.com/libp2p/js-libp2p-multistream-select/issues/3)) ([1e6f3cd](https://github.com/libp2p/js-libp2p-multistream-select/commit/1e6f3cdffee6683786349142349a50872fa8fd17)), closes [#2](https://github.com/libp2p/js-libp2p-multistream-select/issues/2) + +## [@libp2p/multistream-select-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.5...@libp2p/multistream-select-v1.0.6) (2022-05-24) + + +### Bug Fixes + +* chunk data in mock muxer ([#218](https://github.com/libp2p/js-libp2p-interfaces/issues/218)) ([14604f6](https://github.com/libp2p/js-libp2p-interfaces/commit/14604f69a858bf8c16ce118420c5e49f3f5331ea)) + +## [@libp2p/multistream-select-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.4...@libp2p/multistream-select-v1.0.5) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/multistream-select-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.3...@libp2p/multistream-select-v1.0.4) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/multistream-select-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.2...@libp2p/multistream-select-v1.0.3) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/multistream-select-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.1...@libp2p/multistream-select-v1.0.2) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/multistream-select-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/multistream-select-v1.0.0...@libp2p/multistream-select-v1.0.1) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## @libp2p/multistream-select-v1.0.0 (2022-02-17) + + +### Bug Fixes + +* add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) diff --git a/packages/multistream-select/LICENSE b/packages/multistream-select/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/multistream-select/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/multistream-select/LICENSE-APACHE b/packages/multistream-select/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/multistream-select/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/multistream-select/LICENSE-MIT b/packages/multistream-select/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/multistream-select/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/multistream-select/README.md b/packages/multistream-select/README.md new file mode 100644 index 0000000000..e28849ffbe --- /dev/null +++ b/packages/multistream-select/README.md @@ -0,0 +1,68 @@ +# @libp2p/multistream-select + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> JavaScript implementation of multistream-select + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Background + +### What is `multistream-select`? + +TLDR; multistream-select is protocol multiplexing per connection/stream. [Full spec here](https://github.com/multiformats/multistream-select) + +### Select a protocol flow + +The caller will send "interactive" messages, expecting for some acknowledgement from the callee, which will "select" the handler for the desired and supported protocol: + + < /multistream-select/0.3.0 # i speak multistream-select/0.3.0 + > /multistream-select/0.3.0 # ok, let's speak multistream-select/0.3.0 + > /ipfs-dht/0.2.3 # i want to speak ipfs-dht/0.2.3 + < na # ipfs-dht/0.2.3 is not available + > /ipfs-dht/0.1.9 # What about ipfs-dht/0.1.9 ? + < /ipfs-dht/0.1.9 # ok let's speak ipfs-dht/0.1.9 -- in a sense acts as an ACK + > + > + > + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/multistream-select/package.json b/packages/multistream-select/package.json new file mode 100644 index 0000000000..156907a437 --- /dev/null +++ b/packages/multistream-select/package.json @@ -0,0 +1,82 @@ +{ + "name": "@libp2p/multistream-select", + "version": "3.1.9", + "description": "JavaScript implementation of multistream-select", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/multistream-select#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "ipfs", + "libp2p", + "multistream", + "protocol", + "stream" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "abortable-iterator": "^5.0.1", + "it-first": "^3.0.1", + "it-handshake": "^4.1.3", + "it-length-prefixed": "^9.0.1", + "it-merge": "^3.0.0", + "it-pipe": "^3.0.1", + "it-pushable": "^3.1.3", + "it-reader": "^6.0.1", + "it-stream-types": "^2.0.1", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "iso-random-stream": "^2.0.2", + "it-all": "^3.0.1", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "p-timeout": "^6.0.0", + "varint": "^6.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/multistream-select/src/constants.ts b/packages/multistream-select/src/constants.ts new file mode 100644 index 0000000000..ce5dbb521e --- /dev/null +++ b/packages/multistream-select/src/constants.ts @@ -0,0 +1,6 @@ + +export const PROTOCOL_ID = '/multistream/1.0.0' + +// Conforming to go-libp2p +// See https://github.com/multiformats/go-multistream/blob/master/multistream.go#L297 +export const MAX_PROTOCOL_LENGTH = 1024 diff --git a/packages/multistream-select/src/handle.ts b/packages/multistream-select/src/handle.ts new file mode 100644 index 0000000000..eaf8331f6b --- /dev/null +++ b/packages/multistream-select/src/handle.ts @@ -0,0 +1,92 @@ +import { logger } from '@libp2p/logger' +import { handshake } from 'it-handshake' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PROTOCOL_ID } from './constants.js' +import * as multistream from './multistream.js' +import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:mss:handle') + +/** + * Handle multistream protocol selections for the given list of protocols. + * + * Note that after a protocol is handled `listener` can no longer be used. + * + * @param stream - A duplex iterable stream to listen on + * @param protocols - A list of protocols (or single protocol) that this listener is able to speak. + * @param options - an options object containing an AbortSignal and an optional boolean `writeBytes` - if this is true, `Uint8Array`s will be written into `duplex`, otherwise `Uint8ArrayList`s will + * @returns A stream for the selected protocol and the protocol that was selected from the list of protocols provided to `select` + * @example + * + * ```js + * import { pipe } from 'it-pipe' + * import * as mss from '@libp2p/multistream-select' + * import { Mplex } from '@libp2p/mplex' + * + * const muxer = new Mplex({ + * async onStream (muxedStream) { + * // mss.handle(handledProtocols) + * // Returns selected stream and protocol + * const { stream, protocol } = await mss.handle(muxedStream, [ + * '/ipfs-dht/1.0.0', + * '/ipfs-bitswap/1.0.0' + * ]) + * + * // Typically here we'd call the handler function that was registered in + * // libp2p for the given protocol: + * // e.g. handlers[protocol].handler(stream) + * // + * // If protocol was /ipfs-dht/1.0.0 it might do something like this: + * // try { + * // await pipe( + * // dhtStream, + * // source => (async function * () { + * // for await (const chunk of source) + * // // Incoming DHT data -> process and yield to respond + * // })(), + * // dhtStream + * // ) + * // } catch (err) { + * // // Error in stream + * // } + * } + * }) + * ``` + */ +export async function handle (stream: Duplex, Source>, protocols: string | string[], options: ByteArrayInit): Promise> +export async function handle (stream: Duplex, Source>, protocols: string | string[], options?: ByteListInit): Promise> +export async function handle (stream: any, protocols: string | string[], options?: MultistreamSelectInit): Promise> { + protocols = Array.isArray(protocols) ? protocols : [protocols] + const { writer, reader, rest, stream: shakeStream } = handshake(stream) + + while (true) { + const protocol = await multistream.readString(reader, options) + log.trace('read "%s"', protocol) + + if (protocol === PROTOCOL_ID) { + log.trace('respond with "%s" for "%s"', PROTOCOL_ID, protocol) + multistream.write(writer, uint8ArrayFromString(PROTOCOL_ID), options) + continue + } + + if (protocols.includes(protocol)) { + multistream.write(writer, uint8ArrayFromString(protocol), options) + log.trace('respond with "%s" for "%s"', protocol, protocol) + rest() + return { stream: shakeStream, protocol } + } + + if (protocol === 'ls') { + // \n\n\n + multistream.write(writer, new Uint8ArrayList(...protocols.map(p => multistream.encode(uint8ArrayFromString(p)))), options) + // multistream.writeAll(writer, protocols.map(p => uint8ArrayFromString(p))) + log.trace('respond with "%s" for %s', protocols, protocol) + continue + } + + multistream.write(writer, uint8ArrayFromString('na'), options) + log('respond with "na" for "%s"', protocol) + } +} diff --git a/packages/multistream-select/src/index.ts b/packages/multistream-select/src/index.ts new file mode 100644 index 0000000000..651fd77cce --- /dev/null +++ b/packages/multistream-select/src/index.ts @@ -0,0 +1,25 @@ +import { PROTOCOL_ID } from './constants.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Duplex, Source } from 'it-stream-types' + +export { PROTOCOL_ID } + +export interface ProtocolStream> { + stream: Duplex, Source, RSink> + protocol: string +} + +export interface ByteArrayInit extends AbortOptions { + writeBytes: true +} + +export interface ByteListInit extends AbortOptions { + writeBytes?: false +} + +export interface MultistreamSelectInit extends AbortOptions { + writeBytes?: boolean +} + +export { select, lazySelect } from './select.js' +export { handle } from './handle.js' diff --git a/packages/multistream-select/src/multistream.ts b/packages/multistream-select/src/multistream.ts new file mode 100644 index 0000000000..65eda32839 --- /dev/null +++ b/packages/multistream-select/src/multistream.ts @@ -0,0 +1,100 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import first from 'it-first' +import * as lp from 'it-length-prefixed' +import { pipe } from 'it-pipe' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { MAX_PROTOCOL_LENGTH } from './constants.js' +import type { MultistreamSelectInit } from '.' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Pushable } from 'it-pushable' +import type { Reader } from 'it-reader' +import type { Source } from 'it-stream-types' + +const log = logger('libp2p:mss') + +const NewLine = uint8ArrayFromString('\n') + +export function encode (buffer: Uint8Array | Uint8ArrayList): Uint8ArrayList { + const list = new Uint8ArrayList(buffer, NewLine) + + return lp.encode.single(list) +} + +/** + * `write` encodes and writes a single buffer + */ +export function write (writer: Pushable, buffer: Uint8Array | Uint8ArrayList, options: MultistreamSelectInit = {}): void { + const encoded = encode(buffer) + + if (options.writeBytes === true) { + writer.push(encoded.subarray()) + } else { + writer.push(encoded) + } +} + +/** + * `writeAll` behaves like `write`, except it encodes an array of items as a single write + */ +export function writeAll (writer: Pushable, buffers: Uint8Array[], options: MultistreamSelectInit = {}): void { + const list = new Uint8ArrayList() + + for (const buf of buffers) { + list.append(encode(buf)) + } + + if (options.writeBytes === true) { + writer.push(list.subarray()) + } else { + writer.push(list) + } +} + +export async function read (reader: Reader, options?: AbortOptions): Promise { + let byteLength = 1 // Read single byte chunks until the length is known + const varByteSource = { // No return impl - we want the reader to remain readable + [Symbol.asyncIterator]: () => varByteSource, + next: async () => reader.next(byteLength) + } + + let input: Source = varByteSource + + // If we have been passed an abort signal, wrap the input source in an abortable + // iterator that will throw if the operation is aborted + if (options?.signal != null) { + input = abortableSource(varByteSource, options.signal) + } + + // Once the length has been parsed, read chunk for that length + const onLength = (l: number): void => { + byteLength = l + } + + const buf = await pipe( + input, + (source) => lp.decode(source, { onLength, maxDataLength: MAX_PROTOCOL_LENGTH }), + async (source) => first(source) + ) + + if (buf == null || buf.length === 0) { + throw new CodeError('no buffer returned', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + } + + if (buf.get(buf.byteLength - 1) !== NewLine[0]) { + log.error('Invalid mss message - missing newline - %s', buf.subarray()) + throw new CodeError('missing newline', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + } + + return buf.sublist(0, -1) // Remove newline +} + +export async function readString (reader: Reader, options?: AbortOptions): Promise { + const buf = await read(reader, options) + + return uint8ArrayToString(buf.subarray()) +} diff --git a/packages/multistream-select/src/select.ts b/packages/multistream-select/src/select.ts new file mode 100644 index 0000000000..71dafed647 --- /dev/null +++ b/packages/multistream-select/src/select.ts @@ -0,0 +1,161 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { handshake } from 'it-handshake' +import merge from 'it-merge' +import { pushable } from 'it-pushable' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as multistream from './multistream.js' +import { PROTOCOL_ID } from './index.js' +import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js' +import type { Duplex, Source } from 'it-stream-types' + +const log = logger('libp2p:mss:select') + +/** + * Negotiate a protocol to use from a list of protocols. + * + * @param stream - A duplex iterable stream to dial on + * @param protocols - A list of protocols (or single protocol) to negotiate with. Protocols are attempted in order until a match is made. + * @param options - An options object containing an AbortSignal and an optional boolean `writeBytes` - if this is true, `Uint8Array`s will be written into `duplex`, otherwise `Uint8ArrayList`s will + * @returns A stream for the selected protocol and the protocol that was selected from the list of protocols provided to `select`. + * @example + * + * ```js + * import { pipe } from 'it-pipe' + * import * as mss from '@libp2p/multistream-select' + * import { Mplex } from '@libp2p/mplex' + * + * const muxer = new Mplex() + * const muxedStream = muxer.newStream() + * + * // mss.select(protocol(s)) + * // Select from one of the passed protocols (in priority order) + * // Returns selected stream and protocol + * const { stream: dhtStream, protocol } = await mss.select(muxedStream, [ + * // This might just be different versions of DHT, but could be different impls + * '/ipfs-dht/2.0.0', // Most of the time this will probably just be one item. + * '/ipfs-dht/1.0.0' + * ]) + * + * // Typically this stream will be passed back to the caller of libp2p.dialProtocol + * // + * // ...it might then do something like this: + * // try { + * // await pipe( + * // [uint8ArrayFromString('Some DHT data')] + * // dhtStream, + * // async source => { + * // for await (const chunk of source) + * // // DHT response data + * // } + * // ) + * // } catch (err) { + * // // Error in stream + * // } + * ``` + */ +export async function select (stream: Duplex, Source>, protocols: string | string[], options: ByteArrayInit): Promise> +export async function select (stream: Duplex, Source>, protocols: string | string[], options?: ByteListInit): Promise> +export async function select (stream: any, protocols: string | string[], options: MultistreamSelectInit = {}): Promise> { + protocols = Array.isArray(protocols) ? [...protocols] : [protocols] + const { reader, writer, rest, stream: shakeStream } = handshake(stream) + + const protocol = protocols.shift() + + if (protocol == null) { + throw new Error('At least one protocol must be specified') + } + + log.trace('select: write ["%s", "%s"]', PROTOCOL_ID, protocol) + const p1 = uint8ArrayFromString(PROTOCOL_ID) + const p2 = uint8ArrayFromString(protocol) + multistream.writeAll(writer, [p1, p2], options) + + let response = await multistream.readString(reader, options) + log.trace('select: read "%s"', response) + + // Read the protocol response if we got the protocolId in return + if (response === PROTOCOL_ID) { + response = await multistream.readString(reader, options) + log.trace('select: read "%s"', response) + } + + // We're done + if (response === protocol) { + rest() + return { stream: shakeStream, protocol } + } + + // We haven't gotten a valid ack, try the other protocols + for (const protocol of protocols) { + log.trace('select: write "%s"', protocol) + multistream.write(writer, uint8ArrayFromString(protocol), options) + const response = await multistream.readString(reader, options) + log.trace('select: read "%s" for "%s"', response, protocol) + + if (response === protocol) { + rest() // End our writer so others can start writing to stream + return { stream: shakeStream, protocol } + } + } + + rest() + throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL') +} + +/** + * Lazily negotiates a protocol. + * + * It *does not* block writes waiting for the other end to respond. Instead, it + * simply assumes the negotiation went successfully and starts writing data. + * + * Use when it is known that the receiver supports the desired protocol. + */ +export function lazySelect (stream: Duplex, Source>, protocol: string): ProtocolStream +export function lazySelect (stream: Duplex, Source>, protocol: string): ProtocolStream +export function lazySelect (stream: Duplex, protocol: string): ProtocolStream { + // This is a signal to write the multistream headers if the consumer tries to + // read from the source + const negotiateTrigger = pushable() + let negotiated = false + return { + stream: { + sink: async source => { + await stream.sink((async function * () { + let first = true + for await (const chunk of merge(source, negotiateTrigger)) { + if (first) { + first = false + negotiated = true + negotiateTrigger.end() + const p1 = uint8ArrayFromString(PROTOCOL_ID) + const p2 = uint8ArrayFromString(protocol) + const list = new Uint8ArrayList(multistream.encode(p1), multistream.encode(p2)) + if (chunk.length > 0) list.append(chunk) + yield * list + } else { + yield chunk + } + } + })()) + }, + source: (async function * () { + if (!negotiated) negotiateTrigger.push(new Uint8Array()) + const byteReader = reader(stream.source) + let response = await multistream.readString(byteReader) + if (response === PROTOCOL_ID) { + response = await multistream.readString(byteReader) + } + if (response !== protocol) { + throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL') + } + for await (const chunk of byteReader) { + yield * chunk + } + })() + }, + protocol + } +} diff --git a/packages/multistream-select/test/dialer.spec.ts b/packages/multistream-select/test/dialer.spec.ts new file mode 100644 index 0000000000..fe77684dfb --- /dev/null +++ b/packages/multistream-select/test/dialer.spec.ts @@ -0,0 +1,139 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import { pair } from 'it-pair' +import { pipe } from 'it-pipe' +import { reader } from 'it-reader' +import pTimeout from 'p-timeout' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as mss from '../src/index.js' +import * as Multistream from '../src/multistream.js' + +describe('Dialer', () => { + describe('dialer.select', () => { + it('should select from single protocol', async () => { + const protocol = '/echo/1.0.0' + const duplex = pair() + + const selection = await mss.select(duplex, protocol, { + writeBytes: true + }) + expect(selection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should fail to select twice', async () => { + const protocol = '/echo/1.0.0' + const protocol2 = '/echo/2.0.0' + const duplex = pair() + + const selection = await mss.select(duplex, protocol, { + writeBytes: true + }) + expect(selection.protocol).to.equal(protocol) + + // A second select will timeout + await pTimeout(mss.select(duplex, protocol2, { + writeBytes: true + }), { + milliseconds: 1e3 + }) + .then(() => expect.fail('should have timed out'), (err) => { + expect(err).to.exist() + }) + }) + + it('should select from multiple protocols', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const stream = pair() + const duplex = { + sink: stream.sink, + source: (async function * () { + const source = reader(stream.source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(source) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Echo it back + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + + // Reject protocols until selectedProtocol appears + while (true) { + msg = await Multistream.readString(source) + if (msg === selectedProtocol) { + yield Multistream.encode(uint8ArrayFromString(selectedProtocol)) + break + } else { + yield Multistream.encode(uint8ArrayFromString('na')) + } + } + + // Rest is data + yield * source + })() + } + + const selection = await mss.select(duplex, protocols) + expect(protocols).to.have.length(2) + expect(selection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should throw if protocol selection fails', async () => { + const protocol = ['/echo/2.0.0', '/echo/1.0.0'] + const stream = pair() + const duplex = { + sink: stream.sink, + source: (async function * () { + const source = reader(stream.source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(source) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Echo it back + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + + // Reject all protocols + while (true) { + msg = await Multistream.readString(source) + yield Multistream.encode(uint8ArrayFromString('na')) + } + })() + } + + await expect(mss.select(duplex, protocol)).to.eventually.be.rejected().with.property('code', 'ERR_UNSUPPORTED_PROTOCOL') + }) + }) + + describe('dialer.lazySelect', () => { + it('should lazily select a single protocol', async () => { + const protocol = '/echo/1.0.0' + const duplex = pair() + + const selection = mss.lazySelect(duplex, protocol) + expect(selection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await pipe(input, selection.stream, async (source) => all(source)) + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + }) +}) diff --git a/packages/multistream-select/test/integration.spec.ts b/packages/multistream-select/test/integration.spec.ts new file mode 100644 index 0000000000..e455e02ae1 --- /dev/null +++ b/packages/multistream-select/test/integration.spec.ts @@ -0,0 +1,120 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import { duplexPair } from 'it-pair/duplex' +import { pipe } from 'it-pipe' +import { Uint8ArrayList } from 'uint8arraylist' +import * as mss from '../src/index.js' + +describe('Dialer and Listener integration', () => { + it('should handle and select', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [dialerSelection, listenerSelection] = await Promise.all([ + mss.select(pair[0], protocols), + mss.handle(pair[1], selectedProtocol) + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle, ls and select', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [listenerSelection, dialerSelection] = await Promise.all([ + mss.handle(pair[1], selectedProtocol), + (async () => mss.select(pair[0], selectedProtocol))() + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle and select with Uint8Array streams', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const selectedProtocol = protocols[protocols.length - 1] + const pair = duplexPair() + + const [dialerSelection, listenerSelection] = await Promise.all([ + mss.select(pair[0], protocols), + mss.handle(pair[1], selectedProtocol) + ]) + + expect(dialerSelection.protocol).to.equal(selectedProtocol) + expect(listenerSelection.protocol).to.equal(selectedProtocol) + + // Ensure stream is usable after selection + const input = [randomBytes(10), randomBytes(64), randomBytes(3)] + const output = await Promise.all([ + pipe(input, dialerSelection.stream, async (source) => all(source)), + pipe(listenerSelection.stream, listenerSelection.stream) + ]) + expect(new Uint8ArrayList(...output[0]).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle and lazySelect', async () => { + const protocol = '/echo/1.0.0' + const pair = duplexPair() + + const dialerSelection = mss.lazySelect(pair[0], protocol) + expect(dialerSelection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + // Since the stream is lazy, we need to write to it before handling + const dialerOutPromise = pipe(input, dialerSelection.stream, async source => all(source)) + + const listenerSelection = await mss.handle(pair[1], protocol) + expect(listenerSelection.protocol).to.equal(protocol) + + await pipe(listenerSelection.stream, listenerSelection.stream) + + const dialerOut = await dialerOutPromise + expect(new Uint8ArrayList(...dialerOut).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should abort an unhandled lazySelect', async () => { + const protocol = '/echo/1.0.0' + const pair = duplexPair() + + const dialerSelection = mss.lazySelect(pair[0], protocol) + expect(dialerSelection.protocol).to.equal(protocol) + + // Ensure stream is usable after selection + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + // Since the stream is lazy, we need to write to it before handling + const dialerResultPromise = pipe(input, dialerSelection.stream, async source => all(source)) + + // The error message from this varies depending on how much data got + // written when the dialer receives the `na` response and closes the + // stream, so we just assert that this rejects. + await expect(mss.handle(pair[1], '/unhandled/1.0.0')).to.eventually.be.rejected() + + // Dialer should fail to negotiate the single protocol + await expect(dialerResultPromise).to.eventually.be.rejected() + .with.property('code', 'ERR_UNSUPPORTED_PROTOCOL') + }) +}) diff --git a/packages/multistream-select/test/listener.spec.ts b/packages/multistream-select/test/listener.spec.ts new file mode 100644 index 0000000000..eb9a8b9f40 --- /dev/null +++ b/packages/multistream-select/test/listener.spec.ts @@ -0,0 +1,154 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import * as Lp from 'it-length-prefixed' +import map from 'it-map' +import { pipe } from 'it-pipe' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as mss from '../src/index.js' +import * as Multistream from '../src/multistream.js' +import type { Duplex, Source } from 'it-stream-types' + +describe('Listener', () => { + describe('listener.handle', () => { + it('should handle a protocol', async () => { + const protocol = '/echo/1.0.0' + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be protocol + msg = await Multistream.readString(read) + expect(msg).to.equal(protocol) + + // Rest is data + output = await all(read) + }, + source: (async function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + yield Multistream.encode(uint8ArrayFromString(protocol)) + yield * input + })() + } + + const selection = await mss.handle(duplex, protocol) + expect(selection.protocol).to.equal(protocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should reject unhandled protocols', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const handledProtocols = ['/test/1.0.0', protocols[protocols.length - 1]] + const handledProtocol = protocols[protocols.length - 1] + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be na + msg = await Multistream.readString(read) + expect(msg).to.equal('na') + + // Third message will be handledProtocol + msg = await Multistream.readString(read) + expect(msg).to.equal(handledProtocol) + + // Rest is data + output = await all(read) + }, + source: (function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + for (const protocol of protocols) { + yield Multistream.encode(uint8ArrayFromString(protocol)) + } + yield * input + })() + } + + const selection = await mss.handle(duplex, handledProtocols) + expect(selection.protocol).to.equal(handledProtocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + + it('should handle ls', async () => { + const protocols = ['/echo/2.0.0', '/echo/1.0.0'] + const handledProtocols = ['/test/1.0.0', protocols[protocols.length - 1]] + const handledProtocol = protocols[protocols.length - 1] + const input = [new Uint8ArrayList(randomBytes(10), randomBytes(64), randomBytes(3))] + let output: Uint8ArrayList[] = [] + + const duplex: Duplex, Source> = { + sink: async source => { + const read = reader(source) + let msg: string + + // First message will be multistream-select header + msg = await Multistream.readString(read) + expect(msg).to.equal(mss.PROTOCOL_ID) + + // Second message will be ls response + const buf = await Multistream.read(read) + + const protocolsReader = reader([buf]) + + // Decode each of the protocols from the reader + const lsProtocols = await pipe( + protocolsReader, + (source) => Lp.decode(source), + // Stringify and remove the newline + (source) => map(source, (buf) => uint8ArrayToString(buf.subarray()).trim()), + async (source) => all(source) + ) + + expect(lsProtocols).to.deep.equal(handledProtocols) + + // Third message will be handledProtocol + msg = await Multistream.readString(read) + expect(msg).to.equal(handledProtocol) + + // Rest is data + output = await all(read) + }, + source: (function * () { + yield Multistream.encode(uint8ArrayFromString(mss.PROTOCOL_ID)) + yield Multistream.encode(uint8ArrayFromString('ls')) + yield Multistream.encode(uint8ArrayFromString(handledProtocol)) + yield * input + })() + } + + const selection = await mss.handle(duplex, handledProtocols) + expect(selection.protocol).to.equal(handledProtocol) + + await pipe(selection.stream, selection.stream) + + expect(new Uint8ArrayList(...output).slice()).to.eql(new Uint8ArrayList(...input).slice()) + }) + }) +}) diff --git a/packages/multistream-select/test/multistream.spec.ts b/packages/multistream-select/test/multistream.spec.ts new file mode 100644 index 0000000000..e02d88b9f8 --- /dev/null +++ b/packages/multistream-select/test/multistream.spec.ts @@ -0,0 +1,132 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pushable } from 'it-pushable' +import { reader } from 'it-reader' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as Varint from 'varint' +import * as Multistream from '../src/multistream.js' + +describe('Multistream', () => { + describe('Multistream.encode', () => { + it('should encode data Buffer as a multistream-select message', () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + const output = Multistream.encode(input) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ]) + + expect(output.slice()).to.eql(expected) + }) + + it('should encode data Uint8ArrayList as a multistream-select message', () => { + const input = new Uint8ArrayList(uint8ArrayFromString('TEST'), uint8ArrayFromString(`${Date.now()}`)) + const output = Multistream.encode(input.slice()) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input.slice(), + uint8ArrayFromString('\n') + ]) + + expect(output.slice()).to.eql(expected) + }) + }) + + describe('Multistream.write', () => { + it('should encode and write a multistream-select message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + const writer = pushable() + + Multistream.write(writer, input) + + const expected = uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ]) + + writer.end() + + const output = await all(writer) + expect(output.length).to.equal(1) + expect(output[0].subarray()).to.equalBytes(expected) + }) + }) + + describe('Multistream.read', () => { + it('should decode a multistream-select message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ])]) + + const output = await Multistream.read(source) + expect(output.subarray()).to.equalBytes(input) + }) + + it('should throw for non-newline delimited message', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + }) + + it('should throw for a large message', async () => { + const input = new Uint8Array(10000) + input[input.length - 1] = '\n'.charCodeAt(0) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_MSG_DATA_TOO_LONG') + }) + + it('should throw for a 0-length message', async () => { + const input = new Uint8Array(0) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length)), + input + ])]) + + await expect(Multistream.read(source)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE') + }) + + it('should be abortable', async () => { + const input = uint8ArrayFromString(`TEST${Date.now()}`) + + const source = reader([uint8ArrayConcat([ + Uint8Array.from(Varint.encode(input.length + 1)), // +1 to include newline + input, + uint8ArrayFromString('\n') + ])]) + + const controller = new AbortController() + controller.abort() + + await expect(Multistream.read(source, { + signal: controller.signal + })).to.eventually.be.rejected().with.property('code', 'ABORT_ERR') + }) + }) +}) diff --git a/packages/multistream-select/tsconfig.json b/packages/multistream-select/tsconfig.json new file mode 100644 index 0000000000..4f2b857b3b --- /dev/null +++ b/packages/multistream-select/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/peer-collections/CHANGELOG.md b/packages/peer-collections/CHANGELOG.md new file mode 100644 index 0000000000..2c070f5975 --- /dev/null +++ b/packages/peer-collections/CHANGELOG.md @@ -0,0 +1,114 @@ +## [3.0.2](https://github.com/libp2p/js-libp2p-peer-collections/compare/v3.0.1...v3.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([ceeba09](https://github.com/libp2p/js-libp2p-peer-collections/commit/ceeba0909f8f5d2d9b239761530a45dcbc119f85)) +* Update .github/workflows/stale.yml [skip ci] ([9190f64](https://github.com/libp2p/js-libp2p-peer-collections/commit/9190f64e66f11090b63b2a7c6ee5fe1c5ed5327c)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#36](https://github.com/libp2p/js-libp2p-peer-collections/issues/36)) ([9fa3de6](https://github.com/libp2p/js-libp2p-peer-collections/commit/9fa3de6d85dbe1ade54fda86b597ed9ffe6d71d5)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-peer-collections/compare/v3.0.0...v3.0.1) (2023-03-24) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([aece586](https://github.com/libp2p/js-libp2p-peer-collections/commit/aece5866e1acd0e2ec189ecb189b04a864b2627b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7cea5fc](https://github.com/libp2p/js-libp2p-peer-collections/commit/7cea5fc5b11f7f2326d5268119e7f02b18745352)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([67007a2](https://github.com/libp2p/js-libp2p-peer-collections/commit/67007a29585ce2d7a6396edbed85d8428cd10413)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.8 ([#28](https://github.com/libp2p/js-libp2p-peer-collections/issues/28)) ([17b6e75](https://github.com/libp2p/js-libp2p-peer-collections/commit/17b6e75131932f47d592d61dc517bc0439bfa318)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.2...v3.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update to multiformats 11 (#15) + +### Bug Fixes + +* update to multiformats 11 ([#15](https://github.com/libp2p/js-libp2p-peer-collections/issues/15)) ([a503803](https://github.com/libp2p/js-libp2p-peer-collections/commit/a5038039b48f165b3c7c4f5e114038cd71583fe3)) + +## [2.2.2](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.1...v2.2.2) (2022-12-16) + + +### Documentation + +* document mapIterable method ([5f2bc2c](https://github.com/libp2p/js-libp2p-peer-collections/commit/5f2bc2c22fbd1b53a715d28733dce6eb2daa4dab)) + +## [2.2.1](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.2.0...v2.2.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#14](https://github.com/libp2p/js-libp2p-peer-collections/issues/14)) ([f11b89a](https://github.com/libp2p/js-libp2p-peer-collections/commit/f11b89a3dec6431a34cfbbbde7c5e0e2878d440f)) + +## [2.2.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.1.0...v2.2.0) (2022-09-29) + + +### Features + +* add set operations - intersection, difference, union ([#9](https://github.com/libp2p/js-libp2p-peer-collections/issues/9)) ([896ee1f](https://github.com/libp2p/js-libp2p-peer-collections/commit/896ee1f537bfbdf7104b188edd7e858d02293a44)) + +## [2.1.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v2.0.0...v2.1.0) (2022-09-29) + + +### Features + +* accept Iterable in PeerList constructor ([#8](https://github.com/libp2p/js-libp2p-peer-collections/issues/8)) ([5596ede](https://github.com/libp2p/js-libp2p-peer-collections/commit/5596ede224a387181610353167d0dcfe27e2949f)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([615eacf](https://github.com/libp2p/js-libp2p-peer-collections/commit/615eacf6be0f3381756203521b28e1e44de58001)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-collections/compare/v1.0.3...v2.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* uses new peer-id interface + +### Bug Fixes + +* update deps ([#5](https://github.com/libp2p/js-libp2p-peer-collections/issues/5)) ([89e975a](https://github.com/libp2p/js-libp2p-peer-collections/commit/89e975aece4bbe6f9783349b223d6dac3e1d3c78)) + +## [@libp2p/peer-collections-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.2...@libp2p/peer-collections-v1.0.3) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-collections-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.1...@libp2p/peer-collections-v1.0.2) (2022-04-19) + + +### Bug Fixes + +* move dev deps to prod ([#195](https://github.com/libp2p/js-libp2p-interfaces/issues/195)) ([3e1ffc7](https://github.com/libp2p/js-libp2p-interfaces/commit/3e1ffc7b174e74be483943ad4e5fcab823ae3f6d)) + +## [@libp2p/peer-collections-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-collections-v1.0.0...@libp2p/peer-collections-v1.0.1) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## @libp2p/peer-collections-v1.0.0 (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) diff --git a/packages/peer-collections/LICENSE b/packages/peer-collections/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-collections/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-collections/LICENSE-APACHE b/packages/peer-collections/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-collections/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-collections/LICENSE-MIT b/packages/peer-collections/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-collections/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-collections/README.md b/packages/peer-collections/README.md new file mode 100644 index 0000000000..9292ed7402 --- /dev/null +++ b/packages/peer-collections/README.md @@ -0,0 +1,52 @@ +# @libp2p/peer-collections + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Stores values against a peer id + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +We can't use PeerIds as collection keys because collection keys are compared using same-value-zero equality, so this is just a group of collections that stringifies PeerIds before storing them. + +PeerIds cache stringified versions of themselves so this should be a cheap operation. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-collections/package.json b/packages/peer-collections/package.json new file mode 100644 index 0000000000..b2b2990ab3 --- /dev/null +++ b/packages/peer-collections/package.json @@ -0,0 +1,61 @@ +{ + "name": "@libp2p/peer-collections", + "version": "3.0.2", + "description": "Stores values against a peer id", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-collections#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id": "^2.0.0" + }, + "devDependencies": { + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-collections/src/index.ts b/packages/peer-collections/src/index.ts new file mode 100644 index 0000000000..db2767e619 --- /dev/null +++ b/packages/peer-collections/src/index.ts @@ -0,0 +1,3 @@ +export { PeerMap } from './map.js' +export { PeerSet } from './set.js' +export { PeerList } from './list.js' diff --git a/packages/peer-collections/src/list.ts b/packages/peer-collections/src/list.ts new file mode 100644 index 0000000000..562ef50a5f --- /dev/null +++ b/packages/peer-collections/src/list.ts @@ -0,0 +1,154 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as list entries because list entries are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerList } from '@libp2p/peer-collections' + * + * const list = peerList() + * list.push(peerId) + * ``` + */ +export class PeerList { + private readonly list: string[] + + constructor (list?: PeerList | Iterable) { + this.list = [] + + if (list != null) { + for (const value of list) { + this.list.push(value.toString()) + } + } + } + + [Symbol.iterator] (): IterableIterator { + return mapIterable<[number, string], PeerId>( + this.list.entries(), + (val) => { + return peerIdFromString(val[1]) + } + ) + } + + concat (list: PeerList): PeerList { + const output = new PeerList(this) + + for (const value of list) { + output.push(value) + } + + return output + } + + entries (): IterableIterator<[number, PeerId]> { + return mapIterable<[number, string], [number, PeerId]>( + this.list.entries(), + (val) => { + return [val[0], peerIdFromString(val[1])] + } + ) + } + + every (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): boolean { + return this.list.every((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + } + + filter (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): PeerList { + const output = new PeerList() + + this.list.forEach((str, index) => { + const peerId = peerIdFromString(str) + + if (predicate(peerId, index, this)) { + output.push(peerId) + } + }) + + return output + } + + find (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): PeerId | undefined { + const str = this.list.find((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + findIndex (predicate: (peerId: PeerId, index: number, arr: PeerList) => boolean): number { + return this.list.findIndex((str, index) => { + return predicate(peerIdFromString(str), index, this) + }) + } + + forEach (predicate: (peerId: PeerId, index: number, arr: PeerList) => void): void { + this.list.forEach((str, index) => { + predicate(peerIdFromString(str), index, this) + }) + } + + includes (peerId: PeerId): boolean { + return this.list.includes(peerId.toString()) + } + + indexOf (peerId: PeerId): number { + return this.list.indexOf(peerId.toString()) + } + + pop (): PeerId | undefined { + const str = this.list.pop() + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + push (...peerIds: PeerId[]): void { + for (const peerId of peerIds) { + this.list.push(peerId.toString()) + } + } + + shift (): PeerId | undefined { + const str = this.list.shift() + + if (str == null) { + return undefined + } + + return peerIdFromString(str) + } + + unshift (...peerIds: PeerId[]): number { + let len = this.list.length + + for (let i = peerIds.length - 1; i > -1; i--) { + len = this.list.unshift(peerIds[i].toString()) + } + + return len + } + + get length (): number { + return this.list.length + } +} diff --git a/packages/peer-collections/src/map.ts b/packages/peer-collections/src/map.ts new file mode 100644 index 0000000000..aa54fe8465 --- /dev/null +++ b/packages/peer-collections/src/map.ts @@ -0,0 +1,90 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as map keys because map keys are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerMap } from '@libp2p/peer-collections' + * + * const map = peerMap() + * map.set(peerId, 'value') + * ``` + */ +export class PeerMap { + private readonly map: Map + + constructor (map?: PeerMap) { + this.map = new Map() + + if (map != null) { + for (const [key, value] of map.entries()) { + this.map.set(key.toString(), value) + } + } + } + + [Symbol.iterator] (): IterableIterator<[PeerId, T]> { + return this.entries() + } + + clear (): void { + this.map.clear() + } + + delete (peer: PeerId): void { + this.map.delete(peer.toString()) + } + + entries (): IterableIterator<[PeerId, T]> { + return mapIterable<[string, T], [PeerId, T]>( + this.map.entries(), + (val) => { + return [peerIdFromString(val[0]), val[1]] + } + ) + } + + forEach (fn: (value: T, key: PeerId, map: PeerMap) => void): void { + this.map.forEach((value, key) => { + fn(value, peerIdFromString(key), this) + }) + } + + get (peer: PeerId): T | undefined { + return this.map.get(peer.toString()) + } + + has (peer: PeerId): boolean { + return this.map.has(peer.toString()) + } + + set (peer: PeerId, value: T): void { + this.map.set(peer.toString(), value) + } + + keys (): IterableIterator { + return mapIterable( + this.map.keys(), + (val) => { + return peerIdFromString(val) + } + ) + } + + values (): IterableIterator { + return this.map.values() + } + + get size (): number { + return this.map.size + } +} diff --git a/packages/peer-collections/src/set.ts b/packages/peer-collections/src/set.ts new file mode 100644 index 0000000000..b238c3db65 --- /dev/null +++ b/packages/peer-collections/src/set.ts @@ -0,0 +1,124 @@ +import { peerIdFromString } from '@libp2p/peer-id' +import { mapIterable } from './util.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +/** + * We can't use PeerIds as set entries because set entries are + * compared using same-value-zero equality, so this is just + * a map that stringifies the PeerIds before storing them. + * + * PeerIds cache stringified versions of themselves so this + * should be a cheap operation. + * + * @example + * + * ```JavaScript + * import { peerSet } from '@libp2p/peer-collections' + * + * const set = peerSet() + * set.add(peerId) + * ``` + */ +export class PeerSet { + private readonly set: Set + + constructor (set?: PeerSet | Iterable) { + this.set = new Set() + + if (set != null) { + for (const key of set) { + this.set.add(key.toString()) + } + } + } + + get size (): number { + return this.set.size + } + + [Symbol.iterator] (): IterableIterator { + return this.values() + } + + add (peer: PeerId): void { + this.set.add(peer.toString()) + } + + clear (): void { + this.set.clear() + } + + delete (peer: PeerId): void { + this.set.delete(peer.toString()) + } + + entries (): IterableIterator<[PeerId, PeerId]> { + return mapIterable<[string, string], [PeerId, PeerId]>( + this.set.entries(), + (val) => { + const peerId = peerIdFromString(val[0]) + + return [peerId, peerId] + } + ) + } + + forEach (predicate: (peerId: PeerId, index: PeerId, set: PeerSet) => void): void { + this.set.forEach((str) => { + const id = peerIdFromString(str) + + predicate(id, id, this) + }) + } + + has (peer: PeerId): boolean { + return this.set.has(peer.toString()) + } + + values (): IterableIterator { + return mapIterable( + this.set.values(), + (val) => { + return peerIdFromString(val) + } + ) + } + + intersection (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of other) { + if (this.has(peerId)) { + output.add(peerId) + } + } + + return output + } + + difference (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of this) { + if (!other.has(peerId)) { + output.add(peerId) + } + } + + return output + } + + union (other: PeerSet): PeerSet { + const output = new PeerSet() + + for (const peerId of other) { + output.add(peerId) + } + + for (const peerId of this) { + output.add(peerId) + } + + return output + } +} diff --git a/packages/peer-collections/src/util.ts b/packages/peer-collections/src/util.ts new file mode 100644 index 0000000000..8a79a32c0f --- /dev/null +++ b/packages/peer-collections/src/util.ts @@ -0,0 +1,31 @@ + +/** + * Calls the passed map function on every entry of the passed iterable iterator + */ +export function mapIterable (iter: IterableIterator, map: (val: T) => R): IterableIterator { + const iterator: IterableIterator = { + [Symbol.iterator]: () => { + return iterator + }, + next: () => { + const next = iter.next() + const val = next.value + + if (next.done === true || val == null) { + const result: IteratorReturnResult = { + done: true, + value: undefined + } + + return result + } + + return { + done: false, + value: map(val) + } + } + } + + return iterator +} diff --git a/packages/peer-collections/test/list.spec.ts b/packages/peer-collections/test/list.spec.ts new file mode 100644 index 0000000000..665a5dcfb4 --- /dev/null +++ b/packages/peer-collections/test/list.spec.ts @@ -0,0 +1,35 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerList } from '../src/index.js' + +describe('peer-list', () => { + it('should return a list', async () => { + const list = new PeerList() + const peer = await createEd25519PeerId() + + list.push(peer) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(list.indexOf(peer2)).to.equal(0) + }) + + it('should create a list with PeerList contents', async () => { + const list1 = new PeerList() + const peer = await createEd25519PeerId() + + list1.push(peer) + + const list2 = new PeerList(list1) + + expect(list2.indexOf(peer)).to.equal(0) + }) + + it('should create a list with Array contents', async () => { + const peer = await createEd25519PeerId() + const list = new PeerList([peer]) + + expect(list.indexOf(peer)).to.equal(0) + }) +}) diff --git a/packages/peer-collections/test/map.spec.ts b/packages/peer-collections/test/map.spec.ts new file mode 100644 index 0000000000..b501b73254 --- /dev/null +++ b/packages/peer-collections/test/map.spec.ts @@ -0,0 +1,30 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerMap } from '../src/index.js' + +describe('peer-map', () => { + it('should return a map', async () => { + const map = new PeerMap() + const value = 5 + const peer = await createEd25519PeerId() + + map.set(peer, value) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(map.get(peer2)).to.equal(value) + }) + + it('should create a map with contents', async () => { + const map1 = new PeerMap() + const value = 5 + const peer = await createEd25519PeerId() + + map1.set(peer, value) + + const map2 = new PeerMap(map1) + + expect(map2.get(peer)).to.equal(value) + }) +}) diff --git a/packages/peer-collections/test/set.spec.ts b/packages/peer-collections/test/set.spec.ts new file mode 100644 index 0000000000..27e12fab6b --- /dev/null +++ b/packages/peer-collections/test/set.spec.ts @@ -0,0 +1,110 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { PeerSet } from '../src/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('peer-set', () => { + it('should return a set', async () => { + const set = new PeerSet() + const peer = await createEd25519PeerId() + + set.add(peer) + + const peer2 = peerIdFromBytes(peer.toBytes()) + + expect(set.has(peer2)).to.be.true() + }) + + it('should create a set with PeerSet contents', async () => { + const set1 = new PeerSet() + const peer = await createEd25519PeerId() + + set1.add(peer) + + const set2 = new PeerSet(set1) + + expect(set2.has(peer)).to.be.true() + }) + + it('should create a set with Array contents', async () => { + const peer = await createEd25519PeerId() + const set = new PeerSet([peer]) + + expect(set.has(peer)).to.be.true() + }) + + it('should create a set with Set contents', async () => { + const peer = await createEd25519PeerId() + const s = new Set() + s.add(peer) + const set = new PeerSet(s) + + expect(set.has(peer)).to.be.true() + }) + + it('should return intersection', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.intersection(s2)).to.have.property('size', 1) + expect(s1.intersection(s2).has(peer1)).to.be.true() + expect(s1.intersection(s2).has(peer2)).to.be.false() + + expect(s2.intersection(s1)).to.have.property('size', 1) + expect(s2.intersection(s1).has(peer1)).to.be.true() + expect(s2.intersection(s1).has(peer2)).to.be.false() + + expect(s1.intersection(s1)).to.have.property('size', 1) + expect(s1.intersection(s1).has(peer1)).to.be.true() + expect(s1.intersection(s1).has(peer2)).to.be.false() + + expect(s2.intersection(s2)).to.have.property('size', 2) + expect(s2.intersection(s2).has(peer1)).to.be.true() + expect(s2.intersection(s2).has(peer2)).to.be.true() + }) + + it('should return difference', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.difference(s2)).to.have.property('size', 0) + + expect(s2.difference(s1)).to.have.property('size', 1) + expect(s2.difference(s1).has(peer1)).to.be.false() + expect(s2.difference(s1).has(peer2)).to.be.true() + + expect(s1.difference(s1)).to.have.property('size', 0) + expect(s2.difference(s2)).to.have.property('size', 0) + }) + + it('should return union', async () => { + const peer1 = await createEd25519PeerId() + const peer2 = await createEd25519PeerId() + + const s1 = new PeerSet([peer1]) + const s2 = new PeerSet([peer1, peer2]) + + expect(s1.union(s2)).to.have.property('size', 2) + expect(s1.union(s2).has(peer1)).to.be.true() + expect(s1.union(s2).has(peer2)).to.be.true() + + expect(s2.union(s1)).to.have.property('size', 2) + expect(s2.union(s1).has(peer1)).to.be.true() + expect(s2.union(s1).has(peer2)).to.be.true() + + expect(s1.union(s1)).to.have.property('size', 1) + expect(s1.union(s1).has(peer1)).to.be.true() + expect(s1.union(s1).has(peer2)).to.be.false() + + expect(s2.union(s2)).to.have.property('size', 2) + expect(s2.union(s2).has(peer1)).to.be.true() + expect(s2.union(s2).has(peer2)).to.be.true() + }) +}) diff --git a/packages/peer-collections/tsconfig.json b/packages/peer-collections/tsconfig.json new file mode 100644 index 0000000000..6a53317368 --- /dev/null +++ b/packages/peer-collections/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/peer-discovery-bootstrap/CHANGELOG.md b/packages/peer-discovery-bootstrap/CHANGELOG.md new file mode 100644 index 0000000000..a74abf2fa2 --- /dev/null +++ b/packages/peer-discovery-bootstrap/CHANGELOG.md @@ -0,0 +1,477 @@ +## [8.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v7.0.1...v8.0.0) (2023-05-04) + + +### ⚠ BREAKING CHANGES + +* update @libp2p/interface-peer-discovery to 2.0.0 (#176) + +### Dependencies + +* update @libp2p/interface-peer-discovery to 2.0.0 ([#176](https://github.com/libp2p/js-libp2p-bootstrap/issues/176)) ([1954e75](https://github.com/libp2p/js-libp2p-bootstrap/commit/1954e75fa4b1e6b3b42f885f663f989fd0e422ab)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v7.0.0...v7.0.1) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#175](https://github.com/libp2p/js-libp2p-bootstrap/issues/175)) ([e8d7ed7](https://github.com/libp2p/js-libp2p-bootstrap/commit/e8d7ed76e60b64c2e5a554cd10173a1176e5e2b1)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.3...v7.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 (#171) + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#171](https://github.com/libp2p/js-libp2p-bootstrap/issues/171)) ([9f36bb2](https://github.com/libp2p/js-libp2p-bootstrap/commit/9f36bb2d1f966b46d73b50a590f0141894ef511f)) + +## [6.0.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.2...v6.0.3) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#170](https://github.com/libp2p/js-libp2p-bootstrap/issues/170)) ([5c15878](https://github.com/libp2p/js-libp2p-bootstrap/commit/5c158783314b9fdc9f69608cb8ebe180732c4242)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.1...v6.0.2) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#169](https://github.com/libp2p/js-libp2p-bootstrap/issues/169)) ([3532982](https://github.com/libp2p/js-libp2p-bootstrap/commit/35329826d9e90ec6c323d883e5838bd347dea71c)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v6.0.0...v6.0.1) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([2771c45](https://github.com/libp2p/js-libp2p-bootstrap/commit/2771c45b036be498416cebb4ecd017a5f57ed102)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([bdc51df](https://github.com/libp2p/js-libp2p-bootstrap/commit/bdc51df6aefc35fd8a55c1d134a4b5f3b6b2c7c2)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([93061bb](https://github.com/libp2p/js-libp2p-bootstrap/commit/93061bbf78a2033b2e67b4fd2fb53bf827ed409e)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#163](https://github.com/libp2p/js-libp2p-bootstrap/issues/163)) ([81ae1f2](https://github.com/libp2p/js-libp2p-bootstrap/commit/81ae1f246ae21b842ab002ad4dfb3107950fbaa6)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.2...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to v11 (#153) + +### Bug Fixes + +* update multiformats to v11 ([#153](https://github.com/libp2p/js-libp2p-bootstrap/issues/153)) ([dca6305](https://github.com/libp2p/js-libp2p-bootstrap/commit/dca63051ef102e79ae712c8a786cb26fb42870d3)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.1...v5.0.2) (2022-12-16) + + +### Documentation + +* publish typedoc api docs ([#152](https://github.com/libp2p/js-libp2p-bootstrap/issues/152)) ([b0ff483](https://github.com/libp2p/js-libp2p-bootstrap/commit/b0ff4832751ef74eb012d5058d04e8b087894fe8)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v5.0.0...v5.0.1) (2022-12-01) + + +### Documentation + +* update readme ([#149](https://github.com/libp2p/js-libp2p-bootstrap/issues/149)) ([aba592f](https://github.com/libp2p/js-libp2p-bootstrap/commit/aba592f1c3e9e8687357aa9513de56ef6b22f812)), closes [#147](https://github.com/libp2p/js-libp2p-bootstrap/issues/147) + +## [5.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#144](https://github.com/libp2p/js-libp2p-bootstrap/issues/144)) ([772acc1](https://github.com/libp2p/js-libp2p-bootstrap/commit/772acc14a7eaab369bf7d885a96188853d353ed0)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v3.0.0...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#143) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#143](https://github.com/libp2p/js-libp2p-bootstrap/issues/143)) ([96f2a1e](https://github.com/libp2p/js-libp2p-bootstrap/commit/96f2a1ee7b93c7b74c7070db680e2738f9c2e516)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v2.0.1...v3.0.0) (2022-09-23) + + +### ⚠ BREAKING CHANGES + +* the `interval` option has been renamed `timeout` and +peers are now only discovered once + +### Bug Fixes + +* only discover bootstrap peers once and tag them on discovery ([#142](https://github.com/libp2p/js-libp2p-bootstrap/issues/142)) ([cd41d94](https://github.com/libp2p/js-libp2p-bootstrap/commit/cd41d94fa0fc1d84592448c6e9eccd65ad5e80b1)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v2.0.0...v2.0.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([fed165c](https://github.com/libp2p/js-libp2p-bootstrap/commit/fed165ca3396bb7a9b330b4dbe3a90d4af9cc703)) + + +### Dependencies + +* update @multiformats/multaddr to 11.0.0 ([#141](https://github.com/libp2p/js-libp2p-bootstrap/issues/141)) ([1c9079c](https://github.com/libp2p/js-libp2p-bootstrap/commit/1c9079c57ff8b12b97b1f6f3b3596c16f145846b)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.6...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#137](https://github.com/libp2p/js-libp2p-bootstrap/issues/137)) ([aabed1d](https://github.com/libp2p/js-libp2p-bootstrap/commit/aabed1db852ed804bb8d0de94b783bf451cdea85)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.5...v1.0.6) (2022-05-23) + + +### Bug Fixes + +* update interfaces ([#132](https://github.com/libp2p/js-libp2p-bootstrap/issues/132)) ([7d2edb3](https://github.com/libp2p/js-libp2p-bootstrap/commit/7d2edb3b7dc185076dbbbd7cec6edb5ee7b288ec)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.4...v1.0.5) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#129](https://github.com/libp2p/js-libp2p-bootstrap/issues/129)) ([31de4e1](https://github.com/libp2p/js-libp2p-bootstrap/commit/31de4e18fcaf93f987b1e9e1fcf834680c06c216)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.3...v1.0.4) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#128](https://github.com/libp2p/js-libp2p-bootstrap/issues/128)) ([a07f0e8](https://github.com/libp2p/js-libp2p-bootstrap/commit/a07f0e8a10d9fea533e299c0fb7284355989b043)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.2...v1.0.3) (2022-04-08) + + +### Trivial Changes + +* update aegir ([#127](https://github.com/libp2p/js-libp2p-bootstrap/issues/127)) ([c8e4f01](https://github.com/libp2p/js-libp2p-bootstrap/commit/c8e4f01da4fb6c2a19c1ad6bab40e316d23a9258)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.1...v1.0.2) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#123](https://github.com/libp2p/js-libp2p-bootstrap/issues/123)) ([4b7a52a](https://github.com/libp2p/js-libp2p-bootstrap/commit/4b7a52a374086c2ee874b1eb5d83d7a391885671)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v1.0.0...v1.0.1) (2022-03-17) + + +### Bug Fixes + +* update interfaces ([#121](https://github.com/libp2p/js-libp2p-bootstrap/issues/121)) ([4b5e757](https://github.com/libp2p/js-libp2p-bootstrap/commit/4b5e7576e67f6550d5e18f16afc121553936230b)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.14.0...v1.0.0) (2022-02-06) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +Co-authored-by: Alan Shaw + +### Features + +* convert to typescript ([#119](https://github.com/libp2p/js-libp2p-bootstrap/issues/119)) ([e4a0326](https://github.com/libp2p/js-libp2p-bootstrap/commit/e4a0326b6de88d36b752fa7143814da464252ce0)) + +# [0.14.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.13.0...v0.14.0) (2021-12-02) + + +### chore + +* update peer id, interfaces, etc ([#115](https://github.com/libp2p/js-libp2p-bootstrap/issues/115)) ([f7b8ce0](https://github.com/libp2p/js-libp2p-bootstrap/commit/f7b8ce0cea136f7258bc57ccdb294d9cf40ba811)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.3...v0.13.0) (2021-07-08) + + +### chore + +* update deps ([#114](https://github.com/libp2p/js-libp2p-bootstrap/issues/114)) ([597144f](https://github.com/libp2p/js-libp2p-bootstrap/commit/597144f9c0e0a9674c5e90595d516d191b83a11f)) + + +### BREAKING CHANGES + +* uses new peer-id, multiaddr and friends + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.2...v0.12.3) (2021-04-13) + + +### Bug Fixes + +* build ([#113](https://github.com/libp2p/js-libp2p-bootstrap/issues/113)) ([aeab2bf](https://github.com/libp2p/js-libp2p-bootstrap/commit/aeab2bf46dfd5d7026e9e2b06be9c0b88bd75de1)) + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.12.1...v0.12.2) (2021-02-08) + + +### Features + +* add types and update deps ([#111](https://github.com/libp2p/js-libp2p-bootstrap/issues/111)) ([269b807](https://github.com/libp2p/js-libp2p-bootstrap/commit/269b80782c4640dbbb7d66de0345703086c03f24)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.11.0...v0.12.1) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#106](https://github.com/libp2p/js-libp2p-bootstrap/issues/106)) ([b59b7ad](https://github.com/libp2p/js-libp2p-bootstrap/commit/b59b7ad)) + + +### BREAKING CHANGES + +* - The deps of this module have Uint8Array properties + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.11.0...v0.12.0) (2020-08-10) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#106](https://github.com/libp2p/js-libp2p-bootstrap/issues/106)) ([b59b7ad](https://github.com/libp2p/js-libp2p-bootstrap/commit/b59b7ad)) + + +### BREAKING CHANGES + +* - The deps of this module have Uint8Array properties + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.4...v0.11.0) (2020-04-21) + + +### Chores + +* peer-discovery not using peer-info ([8a99f1b](https://github.com/libp2p/js-libp2p-bootstrap/commit/8a99f1b)) + + +### BREAKING CHANGES + +* peer event emits an object with id and multiaddr instead of a peer-info + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.3...v0.10.4) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#99](https://github.com/libp2p/js-libp2p-bootstrap/issues/99)) ([29b8aa6](https://github.com/libp2p/js-libp2p-bootstrap/commit/29b8aa6)) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.2...v0.10.3) (2019-11-28) + + +### Bug Fixes + +* validate list ([#97](https://github.com/libp2p/js-libp2p-bootstrap/issues/97)) ([5041f28](https://github.com/libp2p/js-libp2p-bootstrap/commit/5041f28)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.1...v0.10.2) (2019-08-01) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.10.0...v0.10.1) (2019-07-31) + + +### Bug Fixes + +* use callback in start from js-libp2p ([#93](https://github.com/libp2p/js-libp2p-bootstrap/issues/93)) ([74c305d](https://github.com/libp2p/js-libp2p-bootstrap/commit/74c305d)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.7...v0.10.0) (2019-07-15) + + +### Code Refactoring + +* callbacks -> async/await ([#89](https://github.com/libp2p/js-libp2p-bootstrap/issues/89)) ([77cfc28](https://github.com/libp2p/js-libp2p-bootstrap/commit/77cfc28)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + + + + +## [0.9.7](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.6...v0.9.7) (2019-01-10) + + + + +## [0.9.6](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.5...v0.9.6) (2019-01-04) + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.4...v0.9.5) (2019-01-03) + + +### Bug Fixes + +* discover peers faster ([#86](https://github.com/libp2p/js-libp2p-bootstrap/issues/86)) ([63a6d10](https://github.com/libp2p/js-libp2p-bootstrap/commit/63a6d10)), closes [#85](https://github.com/libp2p/js-libp2p-bootstrap/issues/85) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.3...v0.9.4) (2018-11-26) + + +### Bug Fixes + +* rename railing -> bootstrap ([#81](https://github.com/libp2p/js-libp2p-bootstrap/issues/81)) ([bda0dc8](https://github.com/libp2p/js-libp2p-bootstrap/commit/bda0dc8)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-bootstrap/compare/v0.9.2...v0.9.3) (2018-07-02) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-railing/compare/v0.9.1...v0.9.2) (2018-06-29) + + +### Bug Fixes + +* name of property and make it stop properly ([#77](https://github.com/libp2p/js-libp2p-railing/issues/77)) ([8f9bef6](https://github.com/libp2p/js-libp2p-railing/commit/8f9bef6)) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.9.0...v0.9.1) (2018-06-05) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.8.1...v0.9.0) (2018-06-05) + + +### Features + +* (BREAKING CHANGE) constructor takes options. + add tag, update deps and fix tests ([27f9aed](https://github.com/libp2p/js-libp2p-railing/commit/27f9aed)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.8.0...v0.8.1) (2018-04-12) + + +### Bug Fixes + +* add more error handling for malformed bootstrap multiaddr ([#74](https://github.com/libp2p/js-libp2p-railing/issues/74)) ([f65e1ba](https://github.com/libp2p/js-libp2p-railing/commit/f65e1ba)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.7.1...v0.8.0) (2018-04-05) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.7.0...v0.7.1) (2017-09-08) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.6.1...v0.7.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#70](https://github.com/libp2p/js-libp2p-railing/issues/70)) ([34064b2](https://github.com/libp2p/js-libp2p-railing/commit/34064b2)) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.6.0...v0.6.1) (2017-07-23) + + +### Features + +* emit peers every 10 secs ([598fd94](https://github.com/libp2p/js-libp2p-railing/commit/598fd94)) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.2...v0.6.0) (2017-07-22) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.1...v0.5.2) (2017-07-08) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-railing/compare/v0.5.0...v0.5.1) (2017-05-19) + + +### Bug Fixes + +* use async/setImmediate ([0c6f754](https://github.com/libp2p/js-libp2p-railing/commit/0c6f754)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-railing/compare/v0.4.3...v0.5.0) (2017-03-30) + + +### Features + +* update to new peer-info ([a6254d8](https://github.com/libp2p/js-libp2p-railing/commit/a6254d8)) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-railing/compare/v0.4.2...v0.4.3) (2017-03-23) + + +### Bug Fixes + +* multiaddr parsing ([#53](https://github.com/libp2p/js-libp2p-railing/issues/53)) ([7d13ea6](https://github.com/libp2p/js-libp2p-railing/commit/7d13ea6)) + + + + +## [0.4.2](https://github.com/libp2p/js-ipfs-railing/compare/v0.4.1...v0.4.2) (2017-03-21) diff --git a/packages/peer-discovery-bootstrap/LICENSE b/packages/peer-discovery-bootstrap/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-discovery-bootstrap/LICENSE-APACHE b/packages/peer-discovery-bootstrap/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-discovery-bootstrap/LICENSE-MIT b/packages/peer-discovery-bootstrap/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-discovery-bootstrap/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-discovery-bootstrap/README.md b/packages/peer-discovery-bootstrap/README.md new file mode 100644 index 0000000000..c48965d55c --- /dev/null +++ b/packages/peer-discovery-bootstrap/README.md @@ -0,0 +1,103 @@ +# @libp2p/bootstrap + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Peer discovery via a list of bootstrap peers + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Usage + +The configured bootstrap peers will be discovered after the configured timeout. This will ensure +there are some peers in the peer store for the node to use to discover other peers. + +They will be tagged with a tag with the name `'bootstrap'` tag, the value `50` and it will expire +after two minutes which means the nodes connections may be closed if the maximum number of +connections is reached. + +Clients that need constant connections to bootstrap nodes (e.g. browsers) can set the TTL to `Infinity`. + +```JavaScript +import { createLibp2p } from 'libp2p' +import { bootstrap } from '@libp2p/bootstrap' +import { tcp } from 'libp2p/tcp' +import { noise } from '@libp2p/noise' +import { mplex } from '@libp2p/mplex' + +let options = { + transports: [ + tcp() + ], + streamMuxers: [ + mplex() + ], + connectionEncryption: [ + noise() + ], + peerDiscovery: [ + bootstrap({ + list: [ // a list of bootstrap peer multiaddrs to connect to on node startup + "/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", + "/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa" + ], + timeout: 1000, // in ms, + tagName: 'bootstrap', + tagValue: 50, + tagTTL: 120000 // in ms + }) + ] +} + +async function start () { + let libp2p = await createLibp2p(options) + + libp2p.on('peer:discovery', function (peerId) { + console.log('found peer: ', peerId.toB58String()) + }) + + await libp2p.start() + +} + +start() +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-discovery-bootstrap/package.json b/packages/peer-discovery-bootstrap/package.json new file mode 100644 index 0000000000..792fcabd24 --- /dev/null +++ b/packages/peer-discovery-bootstrap/package.json @@ -0,0 +1,69 @@ +{ + "name": "@libp2p/bootstrap", + "version": "8.0.0", + "description": "Peer discovery via a list of bootstrap peers", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-bootstrap#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3" + }, + "devDependencies": { + "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "aegir": "^39.0.10", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-discovery-bootstrap/src/index.ts b/packages/peer-discovery-bootstrap/src/index.ts new file mode 100644 index 0000000000..e283223b21 --- /dev/null +++ b/packages/peer-discovery-bootstrap/src/index.ts @@ -0,0 +1,164 @@ +import { peerDiscovery } from '@libp2p/interface-peer-discovery' +import { EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { P2P } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { PeerStore } from '@libp2p/interface-peer-store' +import type { Startable } from '@libp2p/interfaces/dist/src/startable' + +const log = logger('libp2p:bootstrap') + +const DEFAULT_BOOTSTRAP_TAG_NAME = 'bootstrap' +const DEFAULT_BOOTSTRAP_TAG_VALUE = 50 +const DEFAULT_BOOTSTRAP_TAG_TTL = 120000 +const DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT = 1000 + +export interface BootstrapInit { + /** + * The list of peer addresses in multi-address format + */ + list: string[] + + /** + * How long to wait before discovering bootstrap nodes + */ + timeout?: number + + /** + * Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap') + */ + tagName?: string + + /** + * The bootstrap peer tag will have this value (default: 50) + */ + tagValue?: number + + /** + * Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes) + */ + tagTTL?: number +} + +export interface BootstrapComponents { + peerStore: PeerStore +} + +/** + * Emits 'peer' events on a regular interval for each peer in the provided list. + */ +class Bootstrap extends EventEmitter implements PeerDiscovery, Startable { + static tag = 'bootstrap' + + private timer?: ReturnType + private readonly list: PeerInfo[] + private readonly timeout: number + private readonly components: BootstrapComponents + private readonly _init: BootstrapInit + + constructor (components: BootstrapComponents, options: BootstrapInit = { list: [] }) { + if (options.list == null || options.list.length === 0) { + throw new Error('Bootstrap requires a list of peer addresses') + } + super() + + this.components = components + this.timeout = options.timeout ?? DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT + this.list = [] + + for (const candidate of options.list) { + if (!P2P.matches(candidate)) { + log.error('Invalid multiaddr') + continue + } + + const ma = multiaddr(candidate) + const peerIdStr = ma.getPeerId() + + if (peerIdStr == null) { + log.error('Invalid bootstrap multiaddr without peer id') + continue + } + + const peerData: PeerInfo = { + id: peerIdFromString(peerIdStr), + multiaddrs: [ma], + protocols: [] + } + + this.list.push(peerData) + } + + this._init = options + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/bootstrap' + + isStarted (): boolean { + return Boolean(this.timer) + } + + /** + * Start emitting events + */ + start (): void { + if (this.isStarted()) { + return + } + + log('Starting bootstrap node discovery, discovering peers after %s ms', this.timeout) + this.timer = setTimeout(() => { + void this._discoverBootstrapPeers() + .catch(err => { + log.error(err) + }) + }, this.timeout) + } + + /** + * Emit each address in the list as a PeerInfo + */ + async _discoverBootstrapPeers (): Promise { + if (this.timer == null) { + return + } + + for (const peerData of this.list) { + await this.components.peerStore.merge(peerData.id, { + tags: { + [this._init.tagName ?? DEFAULT_BOOTSTRAP_TAG_NAME]: { + value: this._init.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE, + ttl: this._init.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL + } + } + }) + + // check we are still running + if (this.timer == null) { + return + } + + this.safeDispatchEvent('peer', { detail: peerData }) + } + } + + /** + * Stop emitting events + */ + stop (): void { + if (this.timer != null) { + clearTimeout(this.timer) + } + + this.timer = undefined + } +} + +export function bootstrap (init: BootstrapInit): (components: BootstrapComponents) => PeerDiscovery { + return (components: BootstrapComponents) => new Bootstrap(components, init) +} diff --git a/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts b/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts new file mode 100644 index 0000000000..4213385204 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/bootstrap.spec.ts @@ -0,0 +1,125 @@ +/* eslint-env mocha */ + +import { isPeerId } from '@libp2p/interface-peer-id' +import { start, stop } from '@libp2p/interfaces/startable' +import { peerIdFromString } from '@libp2p/peer-id' +import { IPFS } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { type StubbedInstance, stubInterface } from 'sinon-ts' +import { bootstrap, type BootstrapComponents } from '../src/index.js' +import peerList from './fixtures/default-peers.js' +import partialValidPeerList from './fixtures/some-invalid-peers.js' +import type { PeerStore } from '@libp2p/interface-peer-store' + +describe('bootstrap', () => { + let components: BootstrapComponents + let peerStore: StubbedInstance + + beforeEach(async () => { + peerStore = stubInterface() + + components = { + peerStore + } + }) + + it('should throw if no peer list is provided', () => { + // @ts-expect-error missing args + expect(() => bootstrap()()) + .to.throw('Bootstrap requires a list of peer addresses') + }) + + it('should discover bootstrap peers', async function () { + this.timeout(5 * 1000) + const r = bootstrap({ + list: peerList, + timeout: 100 + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', resolve, { + once: true + }) + }) + await start(r) + + await p + await stop(r) + }) + + it('should tag bootstrap peers', async function () { + this.timeout(5 * 1000) + + const tagName = 'tag-tag' + const tagValue = 10 + const tagTTL = 50 + + const r = bootstrap({ + list: peerList, + timeout: 100, + tagName, + tagValue, + tagTTL + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', resolve, { + once: true + }) + }) + await start(r) + + await p + + const bootstrapper0ma = multiaddr(peerList[0]) + const bootstrapper0PeerIdStr = bootstrapper0ma.getPeerId() + + if (bootstrapper0PeerIdStr == null) { + throw new Error('bootstrapper had no PeerID') + } + + const bootstrapper0PeerId = peerIdFromString(bootstrapper0PeerIdStr) + + expect(peerStore.merge).to.have.property('called', true) + + const call = peerStore.merge.getCall(0) + expect(call).to.have.deep.nested.property('args[0]', bootstrapper0PeerId) + expect(call).to.have.deep.nested.property('args[1]', { + tags: { + [tagName]: { + value: tagValue, + ttl: tagTTL + } + } + }) + + await stop(r) + }) + + it('should not fail on malformed peers in peer list', async function () { + this.timeout(5 * 1000) + + const r = bootstrap({ + list: partialValidPeerList, + timeout: 100 + })(components) + + const p = new Promise((resolve) => { + r.addEventListener('peer', (evt) => { + const { id, multiaddrs } = evt.detail + + expect(id).to.exist() + expect(isPeerId(id)).to.be.true() + expect(multiaddrs.length).to.eq(1) + expect(IPFS.matches(multiaddrs[0].toString())).equals(true) + resolve() + }) + }) + + await start(r) + + await p + await stop(r) + }) +}) diff --git a/packages/peer-discovery-bootstrap/test/compliance.spec.ts b/packages/peer-discovery-bootstrap/test/compliance.spec.ts new file mode 100644 index 0000000000..7afc7dba12 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/compliance.spec.ts @@ -0,0 +1,23 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-peer-discovery-compliance-tests' +import { stubInterface } from 'sinon-ts' +import { bootstrap } from '../src/index.js' +import peerList from './fixtures/default-peers.js' +import type { PeerStore } from '@libp2p/interface-peer-store' + +describe('compliance tests', () => { + tests({ + async setup () { + const components = { + peerStore: stubInterface() + } + + return bootstrap({ + list: peerList, + timeout: 100 + })(components) + }, + async teardown () {} + }) +}) diff --git a/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts b/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts new file mode 100644 index 0000000000..3b611d6463 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/fixtures/default-peers.ts @@ -0,0 +1,10 @@ +const peers: string[] = [ + '/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', + '/dnsaddr/bootstrap.libp2p.io/ipfs/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp' +] + +export default peers diff --git a/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts b/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts new file mode 100644 index 0000000000..2449b3a737 --- /dev/null +++ b/packages/peer-discovery-bootstrap/test/fixtures/some-invalid-peers.ts @@ -0,0 +1,9 @@ +const peers: string[] = [ + // @ts-expect-error this is an invalid peer + null, + '/ip4/104.236.151.122/tcp/4001/ipfs/malformed-peer-id', + '/ip4/bad.ip.addr/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', + '/ip4/104.236.151.122/tcp/4001/ipfs/QmSoLju6m7xTh3DuokvT3886QRYqxAzb1kShaanJgW36yx' +] + +export default peers diff --git a/packages/peer-discovery-bootstrap/tsconfig.json b/packages/peer-discovery-bootstrap/tsconfig.json new file mode 100644 index 0000000000..954f3f631c --- /dev/null +++ b/packages/peer-discovery-bootstrap/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-discovery-compliance-tests" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/peer-discovery-mdns/.aegir.js b/packages/peer-discovery-mdns/.aegir.js new file mode 100644 index 0000000000..4c82ade3d7 --- /dev/null +++ b/packages/peer-discovery-mdns/.aegir.js @@ -0,0 +1,8 @@ +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + config: { + platform: 'node' + } + } +} diff --git a/packages/peer-discovery-mdns/CHANGELOG.md b/packages/peer-discovery-mdns/CHANGELOG.md new file mode 100644 index 0000000000..faba3af6eb --- /dev/null +++ b/packages/peer-discovery-mdns/CHANGELOG.md @@ -0,0 +1,500 @@ +## [8.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.5...v8.0.0) (2023-05-04) + + +### ⚠ BREAKING CHANGES + +* update @libp2p/interface-peer-discovery to 2.0.0 (#197) + +### Dependencies + +* update @libp2p/interface-peer-discovery to 2.0.0 ([#197](https://github.com/libp2p/js-libp2p-mdns/issues/197)) ([e8172af](https://github.com/libp2p/js-libp2p-mdns/commit/e8172af8b9856a934327195238b00e5fbba436a4)) + +## [7.0.5](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.4...v7.0.5) (2023-05-04) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#196](https://github.com/libp2p/js-libp2p-mdns/issues/196)) ([1db5304](https://github.com/libp2p/js-libp2p-mdns/commit/1db530413904d24cfb446513769f45454ad82428)) + +## [7.0.4](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.3...v7.0.4) (2023-04-24) + + +### Dependencies + +* **dev:** bump @libp2p/interface-address-manager from 2.0.5 to 3.0.0 ([#193](https://github.com/libp2p/js-libp2p-mdns/issues/193)) ([5044392](https://github.com/libp2p/js-libp2p-mdns/commit/5044392191b4ea3149c2e4c6c75120eaa1441fde)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.2...v7.0.3) (2023-04-07) + + +### Bug Fixes + +* do not append peer id to advertised addresses ([#192](https://github.com/libp2p/js-libp2p-mdns/issues/192)) ([d1ee623](https://github.com/libp2p/js-libp2p-mdns/commit/d1ee6236f678166e87d0784f041ad5588801fdf2)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.1...v7.0.2) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#191](https://github.com/libp2p/js-libp2p-mdns/issues/191)) ([25e353b](https://github.com/libp2p/js-libp2p-mdns/commit/25e353b1b1a5261ceb484acde924d8f007238326)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v7.0.0...v7.0.1) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5e82b6d](https://github.com/libp2p/js-libp2p-mdns/commit/5e82b6d1e81934588bf70bdf8663672cf36bbb18)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#190](https://github.com/libp2p/js-libp2p-mdns/issues/190)) ([6b4882f](https://github.com/libp2p/js-libp2p-mdns/commit/6b4882f58f8e9528ca405534afb9792c8988b339)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v6.0.0...v7.0.0) (2023-03-07) + + +### ⚠ BREAKING CHANGES + +* service name now defaults to `_p2p._udp.local` and no +longer uses A and AAA records -> replaced by TXT records + +Added random peer name option + +### Features + +* update to latest spec, added peer name, announces all multiaddrs ([#157](https://github.com/libp2p/js-libp2p-mdns/issues/157)) ([5edcc16](https://github.com/libp2p/js-libp2p-mdns/commit/5edcc16d119ebd2b644f85a29596fdcd33617bd0)), closes [#101](https://github.com/libp2p/js-libp2p-mdns/issues/101) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([28c668e](https://github.com/libp2p/js-libp2p-mdns/commit/28c668e9eee0906d4a05a27d824f1c293e702940)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9dccd84](https://github.com/libp2p/js-libp2p-mdns/commit/9dccd84725704e2b3b6b7b2aee16829ca416904f)) +* upgrade aegir to `38.1.2` ([#182](https://github.com/libp2p/js-libp2p-mdns/issues/182)) ([f86328c](https://github.com/libp2p/js-libp2p-mdns/commit/f86328c5cdb4c5a83ee0c941feba3b6ef8e5c016)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v5.1.1...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to v11.x.x (#178) + +### Bug Fixes + +* update multiformats to v11.x.x ([#178](https://github.com/libp2p/js-libp2p-mdns/issues/178)) ([e48b9b1](https://github.com/libp2p/js-libp2p-mdns/commit/e48b9b199a1a76893f888368b4a027df9ac0c4cf)) + +## [5.1.1](https://github.com/libp2p/js-libp2p-mdns/compare/v5.1.0...v5.1.1) (2022-12-16) + + +### Documentation + +* publish typedocs ([#176](https://github.com/libp2p/js-libp2p-mdns/issues/176)) ([7f3a41b](https://github.com/libp2p/js-libp2p-mdns/commit/7f3a41b59a85f618e0af8af9cbda77961edab334)) + + +### Trivial Changes + +* remove lockfile ([3c1b399](https://github.com/libp2p/js-libp2p-mdns/commit/3c1b39962442869f6f00a748d86d60f3aa204684)) + +## [5.1.0](https://github.com/libp2p/js-libp2p-mdns/compare/v5.0.0...v5.1.0) (2022-10-27) + + +### Features + +* add support for custom DNS server IP ([#142](https://github.com/libp2p/js-libp2p-mdns/issues/142)) ([3b6c7db](https://github.com/libp2p/js-libp2p-mdns/commit/3b6c7dbb0e6cfd11d1394ac3e62509926346dbf2)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#146](https://github.com/libp2p/js-libp2p-mdns/issues/146)) ([36d68fc](https://github.com/libp2p/js-libp2p-mdns/commit/36d68fc819316ec7f7a215a38310d90130770e0f)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v3.0.1...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#143) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#143](https://github.com/libp2p/js-libp2p-mdns/issues/143)) ([a6c3f22](https://github.com/libp2p/js-libp2p-mdns/commit/a6c3f22a68c9ea6e5431d3a34e16f67e1e4b9cff)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v3.0.0...v3.0.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cfb0b5c](https://github.com/libp2p/js-libp2p-mdns/commit/cfb0b5cb007b18e5a508d2b11856bbdf895c72d8)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#140](https://github.com/libp2p/js-libp2p-mdns/issues/140)) ([931be6b](https://github.com/libp2p/js-libp2p-mdns/commit/931be6b3fce395ba2e66e9b811b6fb85b7d40083)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v2.0.1...v3.0.0) (2022-07-01) + + +### ⚠ BREAKING CHANGES + +* **deps:** uses components with single-issue interfaces + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> + +### Trivial Changes + +* **deps:** bump @libp2p/components from 1.0.0 to 2.0.1 ([#136](https://github.com/libp2p/js-libp2p-mdns/issues/136)) ([5c6d17b](https://github.com/libp2p/js-libp2p-mdns/commit/5c6d17bce713d9f404d01c08d5732e871c2151b1)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v2.0.0...v2.0.1) (2022-06-27) + + +### Bug Fixes + +* add @types/multicast-dns as dependency ([#135](https://github.com/libp2p/js-libp2p-mdns/issues/135)) ([8c855a1](https://github.com/libp2p/js-libp2p-mdns/commit/8c855a12336a341d3d53d6c15823c0be4a11b75e)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.7...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#131](https://github.com/libp2p/js-libp2p-mdns/issues/131)) ([c9d1e62](https://github.com/libp2p/js-libp2p-mdns/commit/c9d1e62381712678e7b4869a058cf60db3b40af6)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.6...v1.0.7) (2022-05-23) + + +### Bug Fixes + +* update deps ([#127](https://github.com/libp2p/js-libp2p-mdns/issues/127)) ([1f8654e](https://github.com/libp2p/js-libp2p-mdns/commit/1f8654e9387e5987714142e79038a16d5a1f94ac)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.5...v1.0.6) (2022-05-06) + + +### Bug Fixes + +* update interfaces ([#124](https://github.com/libp2p/js-libp2p-mdns/issues/124)) ([bbb0c62](https://github.com/libp2p/js-libp2p-mdns/commit/bbb0c62e0456044383a684ac8a271136360ee565)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.4...v1.0.5) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#123](https://github.com/libp2p/js-libp2p-mdns/issues/123)) ([d2692a1](https://github.com/libp2p/js-libp2p-mdns/commit/d2692a101965d233922d6b66d640928b1bd9ab74)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.3...v1.0.4) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#122](https://github.com/libp2p/js-libp2p-mdns/issues/122)) ([37b689f](https://github.com/libp2p/js-libp2p-mdns/commit/37b689fb7e22887b050a6177bf05f9fc304563f9)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.2...v1.0.3) (2022-03-24) + + +### Bug Fixes + +* update interfaces ([#117](https://github.com/libp2p/js-libp2p-mdns/issues/117)) ([d454b94](https://github.com/libp2p/js-libp2p-mdns/commit/d454b94738ba3caf1b2e3da7cd43dd8f1863ed6d)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.1...v1.0.2) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#116](https://github.com/libp2p/js-libp2p-mdns/issues/116)) ([30ec23a](https://github.com/libp2p/js-libp2p-mdns/commit/30ec23a5bc2e983fe01e0e47e46ecedf4c0eab5d)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-mdns/compare/v1.0.0...v1.0.1) (2022-02-12) + + +### Bug Fixes + +* update to latest interfaces ([#114](https://github.com/libp2p/js-libp2p-mdns/issues/114)) ([4322c1e](https://github.com/libp2p/js-libp2p-mdns/commit/4322c1e8462cb3dde16435efc23303923bbc7d86)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.18.0...v1.0.0) (2022-02-11) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#113](https://github.com/libp2p/js-libp2p-mdns/issues/113)) ([296791e](https://github.com/libp2p/js-libp2p-mdns/commit/296791ec3364199ea5a2de6ee6fec0aadf318392)) + +# [0.18.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.17.0...v0.18.0) (2021-12-02) + + +### chore + +* update to new peer-id ([#102](https://github.com/libp2p/js-libp2p-mdns/issues/102)) ([d88eda5](https://github.com/libp2p/js-libp2p-mdns/commit/d88eda5fca9a589ecba519be89150f25a36271e6)) + + +### BREAKING CHANGES + +* requires node 15+ + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.16.0...v0.17.0) (2021-07-08) + + +### chore + +* update deps ([#100](https://github.com/libp2p/js-libp2p-mdns/issues/100)) ([0b974bc](https://github.com/libp2p/js-libp2p-mdns/commit/0b974bc9e0d110303e2d15a173447ec5631d15f9)) + + +### BREAKING CHANGES + +* uses then new multiaddr and friends + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.3...v0.16.0) (2021-04-13) + + +### Bug Fixes + +* actually check tcp multiaddrs ([#94](https://github.com/libp2p/js-libp2p-mdns/issues/94)) ([9f45f73](https://github.com/libp2p/js-libp2p-mdns/commit/9f45f731e91f225016d11cc3471bd0874e4b5490)) +* ensure event handlers are removed on MulticastDNS.stop ([#96](https://github.com/libp2p/js-libp2p-mdns/issues/96)) ([9fea1f6](https://github.com/libp2p/js-libp2p-mdns/commit/9fea1f6eb9d68b8e7c145e8b615ac504e0031b0e)) + + +### chore + +* peer-discovery not using peer-info ([#90](https://github.com/libp2p/js-libp2p-mdns/issues/90)) ([fca175e](https://github.com/libp2p/js-libp2p-mdns/commit/fca175e6bc706be07a14b81ef3b3c8143ce97a0a)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + +* chore: add tests for peer-discovery interface + +* chore: apply suggestions from code review + +Co-Authored-By: Jacob Heun + +* chore: update readme with peerData and peerId + +Co-authored-by: Jacob Heun + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.3...v0.15.0) (2020-08-11) + + +### Chores + +* upgrade deps ([#97](https://github.com/libp2p/js-libp2p-mdns/issues/97)) ([3cf0e75](https://github.com/libp2p/js-libp2p-mdns/commit/3cf0e75)) + + +### BREAKING CHANGES + +* - All deps of this module now use Uint8Arrays instead of node Buffers + +* chore: address pr comments + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.2...v0.14.3) (2020-08-07) + + +### Bug Fixes + +* ensure event handlers are removed on MulticastDNS.stop ([#96](https://github.com/libp2p/js-libp2p-mdns/issues/96)) ([9fea1f6](https://github.com/libp2p/js-libp2p-mdns/commit/9fea1f6)) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.1...v0.14.2) (2020-07-02) + + +### Bug Fixes + +* actually check tcp multiaddrs ([#94](https://github.com/libp2p/js-libp2p-mdns/issues/94)) ([9f45f73](https://github.com/libp2p/js-libp2p-mdns/commit/9f45f73)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.14.0...v0.14.1) (2020-04-29) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.3...v0.14.0) (2020-04-23) + + +### Chores + +* peer-discovery not using peer-info ([#90](https://github.com/libp2p/js-libp2p-mdns/issues/90)) ([fca175e](https://github.com/libp2p/js-libp2p-mdns/commit/fca175e)) + + +### BREAKING CHANGES + +* peer event emitted with id and multiaddrs properties instead of peer-info + +* chore: add tests for peer-discovery interface + +* chore: apply suggestions from code review + +Co-Authored-By: Jacob Heun + +* chore: update readme with peerData and peerId + +Co-authored-by: Jacob Heun + + + + +## [0.13.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.2...v0.13.3) (2020-02-17) + + +### Bug Fixes + +* remove use of assert module ([#87](https://github.com/libp2p/js-libp2p-mdns/issues/87)) ([e362b04](https://github.com/libp2p/js-libp2p-mdns/commit/e362b04)) + + + + +## [0.13.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.1...v0.13.2) (2020-02-02) + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.13.0...v0.13.1) (2020-01-17) + + +### Bug Fixes + +* do not emit empty peer info objects ([#85](https://github.com/libp2p/js-libp2p-mdns/issues/85)) ([a88483c](https://github.com/libp2p/js-libp2p-mdns/commit/a88483c)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.3...v0.13.0) (2019-09-27) + + +### Code Refactoring + +* callbacks -> async / await ([#78](https://github.com/libp2p/js-libp2p-mdns/issues/78)) ([46d78eb](https://github.com/libp2p/js-libp2p-mdns/commit/46d78eb)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + +* chore: update CI file +* test: add compliance tests + + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.2...v0.12.3) (2019-05-09) + + +### Features + +* compatibility with go-libp2p-mdns ([#80](https://github.com/libp2p/js-libp2p-mdns/issues/80)) ([c6d1d49](https://github.com/libp2p/js-libp2p-mdns/commit/c6d1d49)) + + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.1...v0.12.2) (2019-01-04) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.12.0...v0.12.1) (2018-11-26) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.11.0...v0.12.0) (2018-06-05) + + +### Features + +* (BREAKING CHANGE) update constructor. add tag ([d3eeb6e](https://github.com/libp2p/js-libp2p-mdns/commit/d3eeb6e)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.2...v0.11.0) (2018-04-05) + + +### Features + +* service names ([#68](https://github.com/libp2p/js-libp2p-mdns/issues/68)) ([fa8fe22](https://github.com/libp2p/js-libp2p-mdns/commit/fa8fe22)) +* Use latest multicast-dns and dns-packet ([#69](https://github.com/libp2p/js-libp2p-mdns/issues/69)) ([cb69f2f](https://github.com/libp2p/js-libp2p-mdns/commit/cb69f2f)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.2...v0.10.0) (2018-04-05) + + +### Features + +* service names ([#68](https://github.com/libp2p/js-libp2p-mdns/issues/68)) ([fa8fe22](https://github.com/libp2p/js-libp2p-mdns/commit/fa8fe22)) +* Use latest multicast-dns and dns-packet ([#69](https://github.com/libp2p/js-libp2p-mdns/issues/69)) ([cb69f2f](https://github.com/libp2p/js-libp2p-mdns/commit/cb69f2f)) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.1...v0.9.2) (2018-01-30) + + +### Bug Fixes + +* Clear interval when stopping ([#63](https://github.com/libp2p/js-libp2p-mdns/issues/63)) ([1d586c3](https://github.com/libp2p/js-libp2p-mdns/commit/1d586c3)) +* update deps for [#64](https://github.com/libp2p/js-libp2p-mdns/issues/64) ([#66](https://github.com/libp2p/js-libp2p-mdns/issues/66)) ([d4ed3b3](https://github.com/libp2p/js-libp2p-mdns/commit/d4ed3b3)) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.9.0...v0.9.1) (2017-09-08) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.8.0...v0.9.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#61](https://github.com/libp2p/js-libp2p-mdns/issues/61)) ([36ed2a1](https://github.com/libp2p/js-libp2p-mdns/commit/36ed2a1)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.7.1...v0.8.0) (2017-07-22) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-mdns/compare/v0.7.0...v0.7.1) (2017-07-09) + + +### Bug Fixes + +* support optional no options ([dd53646](https://github.com/libp2p/js-libp2p-mdns/commit/dd53646)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-mdns/compare/v0.6.2...v0.7.0) (2017-03-30) + + +### Features + +* update to that new peer-info everyone is talking about ([3fd3602](https://github.com/libp2p/js-libp2p-mdns/commit/3fd3602)) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-mdns/compare/v0.6.1...v0.6.2) (2017-03-21) diff --git a/packages/peer-discovery-mdns/LICENSE b/packages/peer-discovery-mdns/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-discovery-mdns/LICENSE-APACHE b/packages/peer-discovery-mdns/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-discovery-mdns/LICENSE-MIT b/packages/peer-discovery-mdns/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-discovery-mdns/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-discovery-mdns/README.md b/packages/peer-discovery-mdns/README.md new file mode 100644 index 0000000000..69be7f9185 --- /dev/null +++ b/packages/peer-discovery-mdns/README.md @@ -0,0 +1,113 @@ +# @libp2p/mdns + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Node.js libp2p mDNS discovery implementation for peer discovery + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [MDNS messages](#mdns-messages) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/mdns +``` + +## Usage + +```Typescript +import { mdns } from '@libp2p/mdns' + +const options = { + peerDiscovery: [ + mdns() + ] +} + +async function start () { + const libp2p = await createLibp2p(options) + + libp2p.on('peer:discovery', function (peerId) { + console.log('found peer: ', peerId.toB58String()) + }) + + await libp2p.start() +} + +``` + +- options + - `peerName` - Peer name to announce (should not be peeer id), default random string + - `multiaddrs` - multiaddrs to announce + - `broadcast` - (true/false) announce our presence through mDNS, default `false` + - `interval` - query interval, default 10 \* 1000 (10 seconds) + - `serviceTag` - name of the service announce , default 'ipfs.local\` + +## MDNS messages + +A query is sent to discover the IPFS nodes on the local network + +```js +{ + type: 'query', + questions: [ { name: 'ipfs.local', type: 'PTR' } ] +} +``` + +When a query is detected, each IPFS node sends an answer about itself + +```js + [ { name: 'ipfs.local', + type: 'PTR', + class: 'IN', + ttl: 120, + data: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local' }, + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + type: 'SRV', + class: 'IN', + ttl: 120, + data: + { priority: 10, + weight: 1, + port: '20002', + target: 'LAPTOP-G5LJ7VN9' } }, + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + type: 'TXT', + class: 'IN', + ttl: 120, + data: ['QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK'] }, + { name: 'LAPTOP-G5LJ7VN9', + type: 'A', + class: 'IN', + ttl: 120, + data: '127.0.0.1' }, + { name: 'LAPTOP-G5LJ7VN9', + type: 'AAAA', + class: 'IN', + ttl: 120, + data: '::1' } ] +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-discovery-mdns/package.json b/packages/peer-discovery-mdns/package.json new file mode 100644 index 0000000000..7c2969d741 --- /dev/null +++ b/packages/peer-discovery-mdns/package.json @@ -0,0 +1,69 @@ +{ + "name": "@libp2p/mdns", + "version": "8.0.0", + "description": "Node.js libp2p mDNS discovery implementation for peer discovery", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-mdns#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-discovery": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "@types/multicast-dns": "^7.2.1", + "dns-packet": "^5.4.0", + "multicast-dns": "^7.2.5" + }, + "devDependencies": { + "@libp2p/interface-address-manager": "^3.0.0", + "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "aegir": "^39.0.10", + "p-wait-for": "^5.0.0", + "ts-sinon": "^2.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-discovery-mdns/src/index.ts b/packages/peer-discovery-mdns/src/index.ts new file mode 100644 index 0000000000..1d49e45555 --- /dev/null +++ b/packages/peer-discovery-mdns/src/index.ts @@ -0,0 +1,175 @@ +import { peerDiscovery } from '@libp2p/interface-peer-discovery' +import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import multicastDNS from 'multicast-dns' +import * as query from './query.js' +import { stringGen } from './utils.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' +import type { PeerInfo } from '@libp2p/interface-peer-info' + +const log = logger('libp2p:mdns') + +export interface MulticastDNSInit { + broadcast?: boolean + interval?: number + serviceTag?: string + peerName?: string + port?: number + ip?: string +} + +export interface MulticastDNSComponents { + addressManager: AddressManager +} + +class MulticastDNS extends EventEmitter implements PeerDiscovery { + public mdns?: multicastDNS.MulticastDNS + + private readonly broadcast: boolean + private readonly interval: number + private readonly serviceTag: string + private readonly peerName: string + private readonly port: number + private readonly ip: string + private _queryInterval: ReturnType | null + private readonly components: MulticastDNSComponents + + constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { + super() + + this.broadcast = init.broadcast !== false + this.interval = init.interval ?? (1e3 * 10) + this.serviceTag = init.serviceTag ?? '_p2p._udp.local' + this.ip = init.ip ?? '224.0.0.251' + this.peerName = init.peerName ?? stringGen(63) + // 63 is dns label limit + if (this.peerName.length >= 64) { + throw new Error('Peer name should be less than 64 chars long') + } + this.port = init.port ?? 5353 + this.components = components + this._queryInterval = null + this._onPeer = this._onPeer.bind(this) + this._onMdnsQuery = this._onMdnsQuery.bind(this) + this._onMdnsResponse = this._onMdnsResponse.bind(this) + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/mdns' + + isStarted (): boolean { + return Boolean(this.mdns) + } + + /** + * Start sending queries to the LAN. + * + * @returns {void} + */ + async start (): Promise { + if (this.mdns != null) { + return + } + + this.mdns = multicastDNS({ port: this.port, ip: this.ip }) + this.mdns.on('query', this._onMdnsQuery) + this.mdns.on('response', this._onMdnsResponse) + + this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) + } + + _onMdnsQuery (event: multicastDNS.QueryPacket): void { + if (this.mdns == null) { + return + } + + log.trace('received incoming mDNS query') + query.gotQuery( + event, + this.mdns, + this.peerName, + this.components.addressManager.getAddresses(), + this.serviceTag, + this.broadcast) + } + + _onMdnsResponse (event: multicastDNS.ResponsePacket): void { + log.trace('received mDNS query response') + + try { + const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) + + if (foundPeer != null) { + log('discovered peer in mDNS query response %p', foundPeer.id) + + this.dispatchEvent(new CustomEvent('peer', { + detail: foundPeer + })) + } + } catch (err) { + log.error('Error processing peer response', err) + } + } + + _onPeer (evt: CustomEvent): void { + if (this.mdns == null) { + return + } + + this.dispatchEvent(new CustomEvent('peer', { + detail: evt.detail + })) + } + + /** + * Stop sending queries to the LAN. + * + * @returns {Promise} + */ + async stop (): Promise { + if (this.mdns == null) { + return + } + + this.mdns.removeListener('query', this._onMdnsQuery) + this.mdns.removeListener('response', this._onMdnsResponse) + + if (this._queryInterval != null) { + clearInterval(this._queryInterval) + this._queryInterval = null + } + + await new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) + + this.mdns = undefined + } +} + +export function mdns (init: MulticastDNSInit = {}): (components: MulticastDNSComponents) => PeerDiscovery { + return (components: MulticastDNSComponents) => new MulticastDNS(components, init) +} + +/* for reference + + [ { name: '_p2p._udp.local', + type: 'PTR', + class: 1, + ttl: 120, + data: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local' }, + + { name: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local', + type: 'TXT', + class: 1, + ttl: 120, + data: 'dnsaddr=/ip4/127.0.0.1/tcp/80/p2p/QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' }, +] + +*/ diff --git a/packages/peer-discovery-mdns/src/query.ts b/packages/peer-discovery-mdns/src/query.ts new file mode 100644 index 0000000000..da0e854087 --- /dev/null +++ b/packages/peer-discovery-mdns/src/query.ts @@ -0,0 +1,111 @@ +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Answer, StringAnswer, TxtAnswer } from 'dns-packet' +import type { MulticastDNS, QueryPacket, ResponsePacket } from 'multicast-dns' + +const log = logger('libp2p:mdns:query') + +export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number): NodeJS.Timer { + const query = (): void => { + log('query', serviceTag) + + mdns.query({ + questions: [{ + name: serviceTag, + type: 'PTR' + }] + }) + } + + // Immediately start a query, then do it every interval. + query() + return setInterval(query, interval) +} + +export function gotResponse (rsp: ResponsePacket, localPeerName: string, serviceTag: string): PeerInfo | undefined { + if (rsp.answers == null) { + return + } + + let answerPTR: StringAnswer | undefined + const txtAnswers: TxtAnswer[] = [] + + rsp.answers.forEach((answer) => { + switch (answer.type) { + case 'PTR': answerPTR = answer; break + case 'TXT': txtAnswers.push(answer); break + default: break + } + }) + + if (answerPTR == null || + answerPTR?.name !== serviceTag || + txtAnswers.length === 0 || + answerPTR.data.startsWith(localPeerName)) { + return + } + + try { + const multiaddrs: Multiaddr[] = txtAnswers + .flatMap((a) => a.data) + .filter(answerData => answerData.toString().startsWith('dnsaddr=')) + .map((answerData) => { + return multiaddr(answerData.toString().substring('dnsaddr='.length)) + }) + + const peerId = multiaddrs[0].getPeerId() + if (peerId == null) { + throw new Error("Multiaddr doesn't contain PeerId") + } + log('peer found %p', peerId) + + return { + id: peerIdFromString(peerId), + multiaddrs, + protocols: [] + } + } catch (e) { + log.error('failed to parse mdns response', e) + } +} + +export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerName: string, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean): void { + if (!broadcast) { + log('not responding to mDNS query as broadcast mode is false') + return + } + + if (multiaddrs.length === 0) { + return + } + + if (qry.questions[0] != null && qry.questions[0].name === serviceTag) { + const answers: Answer[] = [] + + answers.push({ + name: serviceTag, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerName + '.' + serviceTag + }) + + multiaddrs.forEach((addr) => { + // spec mandates multiaddr contains peer id + if (addr.getPeerId() != null) { + answers.push({ + name: peerName + '.' + serviceTag, + type: 'TXT', + class: 'IN', + ttl: 120, + data: 'dnsaddr=' + addr.toString() + }) + } + }) + + log('responding to query') + mdns.respond(answers) + } +} diff --git a/packages/peer-discovery-mdns/src/utils.ts b/packages/peer-discovery-mdns/src/utils.ts new file mode 100644 index 0000000000..c88c951102 --- /dev/null +++ b/packages/peer-discovery-mdns/src/utils.ts @@ -0,0 +1,9 @@ +export function stringGen (len: number): string { + let text = '' + + const charset = 'abcdefghijklmnopqrstuvwxyz0123456789' + + for (let i = 0; i < len; i++) { text += charset.charAt(Math.floor(Math.random() * charset.length)) } + + return text +} diff --git a/packages/peer-discovery-mdns/test/compliance.spec.ts b/packages/peer-discovery-mdns/test/compliance.spec.ts new file mode 100644 index 0000000000..e9b00b62f9 --- /dev/null +++ b/packages/peer-discovery-mdns/test/compliance.spec.ts @@ -0,0 +1,53 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-peer-discovery-compliance-tests' +import { CustomEvent } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { stubInterface } from 'ts-sinon' +import { mdns } from '../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerDiscovery } from '@libp2p/interface-peer-discovery' + +let discovery: PeerDiscovery + +describe('compliance tests', () => { + let intervalId: ReturnType + + tests({ + async setup () { + const peerId1 = await createEd25519PeerId() + const peerId2 = await createEd25519PeerId() + + const addressManager = stubInterface() + addressManager.getAddresses.returns([ + multiaddr(`/ip4/127.0.0.1/tcp/13921/p2p/${peerId1.toString()}`) + ]) + + discovery = mdns({ + broadcast: false, + port: 50001 + })({ + addressManager + }) + + // Trigger discovery + const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' + + // @ts-expect-error not a PeerDiscovery field + intervalId = setInterval(() => discovery._onPeer(new CustomEvent('peer', { + detail: { + id: peerId2, + multiaddrs: [multiaddr(maStr)], + protocols: [] + } + })), 1000) + + return discovery + }, + async teardown () { + clearInterval(intervalId) + await new Promise(resolve => setTimeout(resolve, 10)) + } + }) +}) diff --git a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts new file mode 100644 index 0000000000..62613717e3 --- /dev/null +++ b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts @@ -0,0 +1,222 @@ +/* eslint-env mocha */ + +import { start, stop } from '@libp2p/interfaces/startable' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import pWaitFor from 'p-wait-for' +import { stubInterface } from 'ts-sinon' +import { mdns, type MulticastDNSComponents } from './../src/index.js' +import type { AddressManager } from '@libp2p/interface-address-manager' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerInfo } from '@libp2p/interface-peer-info' +import type { Multiaddr } from '@multiformats/multiaddr' + +function getComponents (peerId: PeerId, multiaddrs: Multiaddr[]): MulticastDNSComponents { + const addressManager = stubInterface() + addressManager.getAddresses.returns(multiaddrs.map(ma => ma.encapsulate(`/p2p/${peerId.toString()}`))) + + return { addressManager } +} + +describe('MulticastDNS', () => { + let pA: PeerId + let aMultiaddrs: Multiaddr[] + let pB: PeerId + let bMultiaddrs: Multiaddr[] + let cMultiaddrs: Multiaddr[] + let pD: PeerId + let dMultiaddrs: Multiaddr[] + + before(async function () { + this.timeout(80 * 1000) + + ;[pA, pB, pD] = await Promise.all([ + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId() + ]) + + aMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/20001'), + multiaddr('/dns4/webrtc-star.discovery.libp2p.io/tcp/443/wss/p2p-webrtc-star'), + multiaddr('/dns4/discovery.libp2p.io/tcp/8443') + ] + + bMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/20002'), + multiaddr('/ip6/::1/tcp/20002'), + multiaddr('/dnsaddr/discovery.libp2p.io') + ] + + cMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/20003'), + multiaddr('/ip4/127.0.0.1/tcp/30003/ws'), + multiaddr('/dns4/discovery.libp2p.io') + ] + + dMultiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/30003/ws') + ] + }) + + it('find another peer', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50001 + })(getComponents(pA, aMultiaddrs)) + + const mdnsB = mdns({ + port: 50001 // port must be the same + })(getComponents(pB, bMultiaddrs)) + + await start(mdnsA, mdnsB) + + const { detail: { id } } = await new Promise>((resolve) => { + mdnsA.addEventListener('peer', resolve, { + once: true + }) + }) + + expect(pB.toString()).to.eql(id.toString()) + + await stop(mdnsA, mdnsB) + }) + + it('announces all multiaddresses', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50003 + })(getComponents(pA, aMultiaddrs)) + const mdnsB = mdns({ + port: 50003 // port must be the same + })(getComponents(pB, cMultiaddrs)) + const mdnsD = mdns({ + port: 50003 // port must be the same + })(getComponents(pD, dMultiaddrs)) + + await start(mdnsA, mdnsB, mdnsD) + + const peers = new Map() + const expectedPeer = pB.toString() + + const foundPeer = (evt: CustomEvent): Map => peers.set(evt.detail.id.toString(), evt.detail) + mdnsA.addEventListener('peer', foundPeer) + + await pWaitFor(() => peers.has(expectedPeer)) + mdnsA.removeEventListener('peer', foundPeer) + + expect(peers.get(expectedPeer).multiaddrs.length).to.equal(3) + + await stop(mdnsA, mdnsB, mdnsD) + }) + + it('doesn\'t emit peers after stop', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + port: 50004 // port must be the same + })(getComponents(pA, aMultiaddrs)) + + const mdnsC = mdns({ + port: 50004 + })(getComponents(pD, dMultiaddrs)) + + await start(mdnsA) + await new Promise((resolve) => setTimeout(resolve, 1000)) + await stop(mdnsA) + await start(mdnsC) + + mdnsC.addEventListener('peer', () => { + throw new Error('Should not receive new peer.') + }, { + once: true + }) + + await new Promise((resolve) => setTimeout(resolve, 5000)) + await stop(mdnsC) + }) + + it('should start and stop with go-libp2p-mdns compat', async () => { + const mdnsA = mdns({ + port: 50004 + })(getComponents(pA, aMultiaddrs)) + + await start(mdnsA) + await stop(mdnsA) + }) + + it('should not emit undefined peer ids', async () => { + const mdnsA = mdns({ + port: 50004 + })(getComponents(pA, aMultiaddrs)) + await start(mdnsA) + + await new Promise((resolve, reject) => { + mdnsA.addEventListener('peer', (evt) => { + if (evt.detail == null) { + reject(new Error('peerData was not set')) + } + }) + + // @ts-expect-error not a PeerDiscovery field + if (mdnsA.mdns == null) { + reject(new Error('mdns property was not set')) + return + } + + // @ts-expect-error not a PeerDiscovery field + mdnsA.mdns.on('response', () => { + // query.gotResponse is async - we'll bail from that method when + // comparing the senders PeerId to our own but it'll happen later + // so allow enough time for the test to have failed if we emit + // empty peerData objects + setTimeout(() => { + resolve() + }, 100) + }) + + // this will cause us to respond to ourselves + // @ts-expect-error not a PeerDiscovery field + mdnsA.mdns.query({ + questions: [{ + name: 'localhost', + type: 'A' + }] + }) + }) + + await stop(mdnsA) + }) + + it('find another peer with different udp4 address', async function () { + this.timeout(40 * 1000) + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50005, + ip: '224.0.0.252' + })(getComponents(pA, aMultiaddrs)) + + const mdnsB = mdns({ + port: 50005, // port must be the same + ip: '224.0.0.252' // ip must be the same + })(getComponents(pB, bMultiaddrs)) + + await start(mdnsA, mdnsB) + + const { detail: { id } } = await new Promise>((resolve) => { + mdnsA.addEventListener('peer', resolve, { + once: true + }) + }) + + expect(pB.toString()).to.eql(id.toString()) + + await stop(mdnsA, mdnsB) + }) +}) diff --git a/packages/peer-discovery-mdns/tsconfig.json b/packages/peer-discovery-mdns/tsconfig.json new file mode 100644 index 0000000000..f89978dd99 --- /dev/null +++ b/packages/peer-discovery-mdns/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-address-manager" + }, + { + "path": "../interface-peer-discovery" + }, + { + "path": "../interface-peer-discovery-compliance-tests" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-info" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/peer-id-factory/CHANGELOG.md b/packages/peer-id-factory/CHANGELOG.md new file mode 100644 index 0000000000..c674ac0dac --- /dev/null +++ b/packages/peer-id-factory/CHANGELOG.md @@ -0,0 +1,214 @@ +## [@libp2p/peer-id-factory-v2.0.3](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.2...@libp2p/peer-id-factory-v2.0.3) (2023-03-20) + + +### Documentation + +* update README.md ([#59](https://github.com/libp2p/js-libp2p-peer-id/issues/59)) ([aba6483](https://github.com/libp2p/js-libp2p-peer-id/commit/aba6483dad028ee5c24bfc01135b77568666cfd3)) + +## [@libp2p/peer-id-factory-v2.0.2](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.1...@libp2p/peer-id-factory-v2.0.2) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#49](https://github.com/libp2p/js-libp2p-peer-id/issues/49)) ([48037ee](https://github.com/libp2p/js-libp2p-peer-id/commit/48037ee53d4c07a3750bbeeb40727cbaf3c23946)) + +## [@libp2p/peer-id-factory-v2.0.1](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v2.0.0...@libp2p/peer-id-factory-v2.0.1) (2023-01-18) + + +### Dependencies + +* bump aegir from 37.12.1 to 38.1.0 ([#46](https://github.com/libp2p/js-libp2p-peer-id/issues/46)) ([ba54f6a](https://github.com/libp2p/js-libp2p-peer-id/commit/ba54f6a4a35de20528d4c60a2a532c553b9a9a34)) + +## [@libp2p/peer-id-factory-v2.0.0](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.20...@libp2p/peer-id-factory-v2.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 (#41) + +### Trivial Changes + +* remove lerna ([#43](https://github.com/libp2p/js-libp2p-peer-id/issues/43)) ([d458051](https://github.com/libp2p/js-libp2p-peer-id/commit/d458051bfcb7ff83c42ed26e1c12ac3d07bee492)) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 ([#41](https://github.com/libp2p/js-libp2p-peer-id/issues/41)) ([2aa0f79](https://github.com/libp2p/js-libp2p-peer-id/commit/2aa0f799789b52758651c115b3a021ca67e5c407)) +* update sibling dependencies ([fa59f28](https://github.com/libp2p/js-libp2p-peer-id/commit/fa59f289f543ecafa5c763cb0dba08eb0c24e8d8)) + +## [@libp2p/peer-id-factory-v1.0.20](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.19...@libp2p/peer-id-factory-v1.0.20) (2022-12-16) + + +### Documentation + +* publish api docs for all modules ([#39](https://github.com/libp2p/js-libp2p-peer-id/issues/39)) ([861957a](https://github.com/libp2p/js-libp2p-peer-id/commit/861957add8610498bf095d82dd51af0082eae4b5)) + +## [@libp2p/peer-id-factory-v1.0.19](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.18...@libp2p/peer-id-factory-v1.0.19) (2022-10-12) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#28](https://github.com/libp2p/js-libp2p-peer-id/issues/28)) ([e270265](https://github.com/libp2p/js-libp2p-peer-id/commit/e27026508b3684e6cb2eb896de19c161dbd21d45)) + +## [@libp2p/peer-id-factory-v1.0.18](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.17...@libp2p/peer-id-factory-v1.0.18) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#22](https://github.com/libp2p/js-libp2p-peer-id/issues/22)) ([f86d87a](https://github.com/libp2p/js-libp2p-peer-id/commit/f86d87a547e3f1dc13c3aa9816770964830fda6d)) + +## [@libp2p/peer-id-factory-v1.0.17](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.16...@libp2p/peer-id-factory-v1.0.17) (2022-07-31) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-peer-id/issues/14)) ([5c3918c](https://github.com/libp2p/js-libp2p-peer-id/commit/5c3918c61d8346ed1d49094bb592f8c872b7de57)) + + +### Dependencies + +* update protons and uint8arraylist ([#17](https://github.com/libp2p/js-libp2p-peer-id/issues/17)) ([90588f5](https://github.com/libp2p/js-libp2p-peer-id/commit/90588f599fe9522a0ccfd43f1405425031c5175d)) + +## [@libp2p/peer-id-factory-v1.0.16](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.15...@libp2p/peer-id-factory-v1.0.16) (2022-07-18) + + +### Trivial Changes + +* update sibling dependencies [skip ci] ([4ecbbe0](https://github.com/libp2p/js-libp2p-peer-id/commit/4ecbbe0247dd664a172008ff9255f0f79c04ffb9)) + +## [@libp2p/peer-id-factory-v1.0.15](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.14...@libp2p/peer-id-factory-v1.0.15) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-keys from 0.0.1 to 1.0.2 ([#4](https://github.com/libp2p/js-libp2p-peer-id/issues/4)) ([8aff9ce](https://github.com/libp2p/js-libp2p-peer-id/commit/8aff9ce5b8556f438fd2b7389f8070ae7d217a84)) + +## [@libp2p/peer-id-factory-v1.0.14](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.13...@libp2p/peer-id-factory-v1.0.14) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/crypto from 0.22.14 to 1.0.0 ([#3](https://github.com/libp2p/js-libp2p-peer-id/issues/3)) ([21ff018](https://github.com/libp2p/js-libp2p-peer-id/commit/21ff018a634f58bdd4b9b943e84ae03b1f1ef69d)) + +## [@libp2p/peer-id-factory-v1.0.13](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.12...@libp2p/peer-id-factory-v1.0.13) (2022-06-14) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-peer-id from 0.0.1 to 1.0.0 ([#2](https://github.com/libp2p/js-libp2p-peer-id/issues/2)) ([69c0c49](https://github.com/libp2p/js-libp2p-peer-id/commit/69c0c495ab04d5b97de27d3ef20e1ee78d5b0056)) + +## [@libp2p/peer-id-factory-v1.0.12](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-factory-v1.0.11...@libp2p/peer-id-factory-v1.0.12) (2022-06-10) + + +### Trivial Changes + +* update interface deps ([#1](https://github.com/libp2p/js-libp2p-peer-id/issues/1)) ([3cf652d](https://github.com/libp2p/js-libp2p-peer-id/commit/3cf652d50ede0d876da46dcb0b1de387126e272a)) + +## [@libp2p/peer-id-factory-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.10...@libp2p/peer-id-factory-v1.0.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-id-factory-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.9...@libp2p/peer-id-factory-v1.0.10) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-id-factory-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.8...@libp2p/peer-id-factory-v1.0.9) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-id-factory-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.7...@libp2p/peer-id-factory-v1.0.8) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/peer-id-factory-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.6...@libp2p/peer-id-factory-v1.0.7) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-id-factory-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.5...@libp2p/peer-id-factory-v1.0.6) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-id-factory-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.4...@libp2p/peer-id-factory-v1.0.5) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-id-factory-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.3...@libp2p/peer-id-factory-v1.0.4) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/peer-id-factory-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.2...@libp2p/peer-id-factory-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/peer-id-factory-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-factory-v1.0.1...@libp2p/peer-id-factory-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-peer-id-factory@0.2.0...libp2p-peer-id-factory@0.2.1) (2022-01-02) + +**Note:** Version bump only for package libp2p-peer-id-factory + + + + + +# 0.2.0 (2022-01-02) + + +### Features + +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) diff --git a/packages/peer-id-factory/LICENSE b/packages/peer-id-factory/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-id-factory/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-id-factory/LICENSE-APACHE b/packages/peer-id-factory/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-id-factory/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-id-factory/LICENSE-MIT b/packages/peer-id-factory/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-id-factory/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-id-factory/README.md b/packages/peer-id-factory/README.md new file mode 100644 index 0000000000..7c353cd96b --- /dev/null +++ b/packages/peer-id-factory/README.md @@ -0,0 +1,68 @@ +# @libp2p/peer-id-factory + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Create PeerId instances + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Generate, import, and export PeerIDs, for use with [IPFS](https://github.com/ipfs/ipfs). + +A Peer ID is the SHA-256 [multihash](https://github.com/multiformats/multihash) of a public key. + +The public key is a base64 encoded string of a protobuf containing an RSA DER buffer. This uses a node buffer to pass the base64 encoded public key protobuf to the multihash for ID generation. + +## Example + +```JavaScript +import { createEd25519PeerId } from '@libp2p/peer-id-factory' + +const peerId = await createEd25519PeerId() +console.log(id.toString()) +``` + +```bash +12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-id-factory/package.json b/packages/peer-id-factory/package.json new file mode 100644 index 0000000000..8d03095bc8 --- /dev/null +++ b/packages/peer-id-factory/package.json @@ -0,0 +1,71 @@ +{ + "name": "@libp2p/peer-id-factory", + "version": "2.0.3", + "description": "Create PeerId instances", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id-factory#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "proto.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "generate": "protons src/proto.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-keys": "^1.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-id-factory/src/index.ts b/packages/peer-id-factory/src/index.ts new file mode 100644 index 0000000000..f3b66da887 --- /dev/null +++ b/packages/peer-id-factory/src/index.ts @@ -0,0 +1,91 @@ +import { generateKeyPair, marshalPrivateKey, unmarshalPrivateKey, marshalPublicKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { peerIdFromKeys, peerIdFromBytes } from '@libp2p/peer-id' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { PeerIdProto } from './proto.js' +import type { PublicKey, PrivateKey } from '@libp2p/interface-keys' +import type { RSAPeerId, Ed25519PeerId, Secp256k1PeerId, PeerId } from '@libp2p/interface-peer-id' + +export const createEd25519PeerId = async (): Promise => { + const key = await generateKeyPair('Ed25519') + const id = await createFromPrivKey(key) + + if (id.type === 'Ed25519') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export const createSecp256k1PeerId = async (): Promise => { + const key = await generateKeyPair('secp256k1') + const id = await createFromPrivKey(key) + + if (id.type === 'secp256k1') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export const createRSAPeerId = async (opts?: { bits: number }): Promise => { + const key = await generateKeyPair('RSA', opts?.bits ?? 2048) + const id = await createFromPrivKey(key) + + if (id.type === 'RSA') { + return id + } + + throw new Error(`Generated unexpected PeerId type "${id.type}"`) +} + +export async function createFromPubKey (publicKey: PublicKey): Promise { + return peerIdFromKeys(marshalPublicKey(publicKey)) +} + +export async function createFromPrivKey (privateKey: PrivateKey): Promise { + return peerIdFromKeys(marshalPublicKey(privateKey.public), marshalPrivateKey(privateKey)) +} + +export function exportToProtobuf (peerId: RSAPeerId | Ed25519PeerId | Secp256k1PeerId, excludePrivateKey?: boolean): Uint8Array { + return PeerIdProto.encode({ + id: peerId.multihash.bytes, + pubKey: peerId.publicKey, + privKey: excludePrivateKey === true || peerId.privateKey == null ? undefined : peerId.privateKey + }) +} + +export async function createFromProtobuf (buf: Uint8Array): Promise { + const { + id, + privKey, + pubKey + } = PeerIdProto.decode(buf) + + return createFromParts( + id ?? new Uint8Array(0), + privKey, + pubKey + ) +} + +export async function createFromJSON (obj: { id: string, privKey?: string, pubKey?: string }): Promise { + return createFromParts( + uint8ArrayFromString(obj.id, 'base58btc'), + obj.privKey != null ? uint8ArrayFromString(obj.privKey, 'base64pad') : undefined, + obj.pubKey != null ? uint8ArrayFromString(obj.pubKey, 'base64pad') : undefined + ) +} + +async function createFromParts (multihash: Uint8Array, privKey?: Uint8Array, pubKey?: Uint8Array): Promise { + if (privKey != null) { + const key = await unmarshalPrivateKey(privKey) + + return createFromPrivKey(key) + } else if (pubKey != null) { + const key = unmarshalPublicKey(pubKey) + + return createFromPubKey(key) + } + + return peerIdFromBytes(multihash) +} diff --git a/packages/peer-id-factory/src/proto.proto b/packages/peer-id-factory/src/proto.proto new file mode 100644 index 0000000000..39f8868c76 --- /dev/null +++ b/packages/peer-id-factory/src/proto.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message PeerIdProto { + optional bytes id = 1; + optional bytes pubKey = 2; + optional bytes privKey = 3; +} diff --git a/packages/peer-id-factory/src/proto.ts b/packages/peer-id-factory/src/proto.ts new file mode 100644 index 0000000000..bcd0a25d21 --- /dev/null +++ b/packages/peer-id-factory/src/proto.ts @@ -0,0 +1,83 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerIdProto { + id?: Uint8Array + pubKey?: Uint8Array + privKey?: Uint8Array +} + +export namespace PeerIdProto { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.id != null) { + w.uint32(10) + w.bytes(obj.id) + } + + if (obj.pubKey != null) { + w.uint32(18) + w.bytes(obj.pubKey) + } + + if (obj.privKey != null) { + w.uint32(26) + w.bytes(obj.privKey) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.id = reader.bytes() + break + case 2: + obj.pubKey = reader.bytes() + break + case 3: + obj.privKey = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PeerIdProto.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PeerIdProto => { + return decodeMessage(buf, PeerIdProto.codec()) + } +} diff --git a/packages/peer-id-factory/test/fixtures/go-private-key.ts b/packages/peer-id-factory/test/fixtures/go-private-key.ts new file mode 100644 index 0000000000..5acd665de1 --- /dev/null +++ b/packages/peer-id-factory/test/fixtures/go-private-key.ts @@ -0,0 +1,5 @@ + +export default { + id: 'QmRLoXS3E73psYaUsma1VSbboTa2J8Z9kso1tpiGLk9WQ4', + privKey: 'CAASpwkwggSjAgEAAoIBAQDWBEbO8kc6a5kEks09CKPQargY3p0DCmCczoCT52/RYFqxvH9dI+s+u4ZAvF9aLWOBvFomL7jHZODPxKDrbiNCmyEbViNgZYK+PNbwh0V3ZGbB27X3q8yZtLvYA8dhcNkz/2SHBarSoC4QLA5MXUuSWtVaYMY3MzMnzBF57Jc9Ase7NvHOIUI90M7aN5izP7hxPXpZ+shiN+TyjM8mFxYONG7ZSsY3IxUhtrU5MRzFX+tp1o/gb/aa51mHf7AL3N02j5ABiYbCK97Rbwr03hsBcwgMxoDPJmP3WZ+D5yyPcOIIF1Vd7+4/f7FQJnIw3xr9/jvaFbPyDCVbBOhr9oyxAgMBAAECggEALlrgx2Q8v0+c5hux7p1XdgYXd/OHyKfPw0cLHH4NfylCm6q7X34vLvhJHO5wLMUV/3y/ffPqLu4Pr5DkVfoWExAsvJIMuY1jIzdkStbR2glaJHUlVc7VUxmNcj1nSxi5QwT3TjORC2v8bi5Mroeqnbmk6p15cW1akC0oP+NZ4rG48+WFHRqsBaBusdSOVfA+IiZUqSd1ILysJ1w7aVN3EC7jLjDG43i+P/2BcEHy8TVClGOknJL341bHe3UPdEpmeu6k6aHGlDI4blUMXahCIUh0IdZuj+Vi/TxQME9+3bKIOjQb8RCNm3U3j/uz5gs9SyTjBuYIib9Scj/jDbLh0QKBgQDfLr3go3Q/AR0jb12QjGALJz1lc9ZRX2RQJkqqmYkZwOlHHyl+YJgqOZiO80fUkN0sJ29CmKecXU4gXuHir913Fdceei1ScBSsvZpWtBLhEZXKrRJYq8U0atKUFQADDMGutyB/uGCNeNwR6VcJezHPICvHxQfmWlWHA5VIOEtRPQKBgQD1fID76SkIpF/EaJMnN2alXWWnzKhUBUPGpQtbpwgSfaCBiZ4vr3NQwKBntOOB5QwHmifNZMoqaFQLzC4B/uyTNUcQMQQ6arYav7WQXqXTmW6poTsjUSuSOPx1swsHlYX09SmUwWDfd94XF9UOU0KUfA2/c85ixzNlV5ejkFA4hQKBgEvP3uQN4hD82d8Nl2TgqkdfnvV1cdnWY4buWvK0kOPUqelk5n1tZoMBaZc1gLLuOpMjGiIvJNByyXUpheWxA7POEXLi4b5dIEjFZ0YIiVk21gEw5UiFoMl7d+ihcY2Xqbslrb507SdhZLAY6V3pITRQo06K2XIgQWlJiE4uATepAoGBALZ2vEiBnYZW5vfN4tKbUyhGq3B1pggNgbr8odyV4mIcDlk6OOGov0WeZ5ut0AyUesSLyFnaOIoc0ZuTP/8rxBwG1bMrO8FP39sx83pDX25P9PkQZixyALjGsp+pXOFeOhtAvo9azO5M4j638Bydtjc3neBX62dwOLtyx7tDYN0hAoGAVLmr3w7XMVHTfEuCSzKHyRrOaN2PAuSX31QAji1PwlwVKMylVrb8rRvBOpTicA/wXPX9Q5O/yjegqhqLT/LXAm9ziFzy5b9/9SzXPukKebXXbvc0FOmcsrcxtijlPyUzf9fKM1ShiwqqsgM9eNyZ9GWUJw2GFATCWW7pl7rtnWk=' +} diff --git a/packages/peer-id-factory/test/fixtures/sample-id.ts b/packages/peer-id-factory/test/fixtures/sample-id.ts new file mode 100644 index 0000000000..79f0645c85 --- /dev/null +++ b/packages/peer-id-factory/test/fixtures/sample-id.ts @@ -0,0 +1,7 @@ + +export default { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=', + marshalled: '0a22122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a912ab02080012a60230820122300d06092a864886f70d01010105000382010f003082010a0282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100011aab09080012a609308204a20201000282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100010282010066d8eefdb70abca14fcf49a41e2689729c9ccbd4932a9868ae9093f37b2b055422e7d09d154e8c8fe68bff1b749023cc562809c3c3f7fd808427d27ead2f01b28584fb159412c26fb57a13eefccf1da02d337722d4765ddf4d8ccf5f86812f04a5dc7eec5e69f345c014b0d49c42f33b329fb6f58666659f49e0e7b25c1538d90bff5540cf02b2ec27ba864e12c5113b976344d8e9254873b30865357fbf19cd560a4a74b9020f58ac68ce0264ce5c36ca34a37fa88a2b010d5ba2fcc6a02c31de21886ad40a14ec72542c8ed4fb09613ec93be9196e105645113e2fb97ea693c447d6dd2c5c6cd6de42aca734efc87ec2e52bd394b53f52635e4ebca64dfe9102818100de2bc011d75dfbdccce26fabb3a631b380d44ccdd60db84c568f1cb1033cf9dcd011ef3acf1ef5ef7c8aa30d270b27835c44ed9375d85701f66838f547e64e0f24728b04f2ae5d9a56968a24080c84358efe3dde794bcafe6be32eb2b31a8183658dbe566d54e037c7207698a6f656db20596937a4996958cb40bdc9f13587eb02818100d20a1cc8b64a965f5d4236cb49f73272504db423b2eba720493601b582dbf3dd93144029f73f1c47b50ccdf67d4fd2649262cfa304a3eea12c982edd70c1ed74fe5a602f8ae4296537fe6d4ccadd2dbde27d59ca8787ab737006dbfdf5e95054ffa384960e299690f92e09bfbc8ffff6ca25e4d1afd3d9fdfacca32e66fba3a90281802e81ec10100c6d87d81fe28e87e9d767a3254dfa9cbf7c800672a8e7e92c9f8578ccf84e504343ea6120c8671d70395247436a943ecc0dd2ac593eeb21a4f55c381dfe3a07ef364af3ab49b9a731af8f62a29822f533478820df8acbffb021c276c4c83e615eae1d1f030db080eafa5d9e94f8f09bf53d57481d025dbeaf9d070281802edb0aa8cbe1bfc1ee7003013eb2e29215cfffcba6f2630a14caf37ea67ea2dc5f1f39612342f4f01a378d0adbd19ec1c8d63a33c7a93a66c22800ec6d6715adefc0018d1992e4992bf09a397357fc084c2a628987ca8038f458d362c8251042a5f4b873311d9df521615fd362214d9ca463e7b3cf619753cd4b316bfc954e610281806beec9501236f93a79f99999c60e1fbbd81c4b35d83006484ed0e09da5d212aa4d05d0fc5bcb6d8314e297644a62c88f5760fd42f303e226c4a11a6db213004f5979ebad9356733695b826d71eb664590a200431b71c65cd754e0c0160b28989728a7201a4fa68009652ce918b9966cc5a1dbcf91252e80417e8a1eb2b5a36bb' +} diff --git a/packages/peer-id-factory/test/index.spec.ts b/packages/peer-id-factory/test/index.spec.ts new file mode 100644 index 0000000000..aa32dd2574 --- /dev/null +++ b/packages/peer-id-factory/test/index.spec.ts @@ -0,0 +1,313 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import util from 'util' +import { keys } from '@libp2p/crypto' +import { peerIdFromString, peerIdFromBytes, peerIdFromCID, createPeerId } from '@libp2p/peer-id' +import { expect } from 'aegir/chai' +import { base16 } from 'multiformats/bases/base16' +import { base36 } from 'multiformats/bases/base36' +import { base58btc } from 'multiformats/bases/base58' +import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' +import { identity } from 'multiformats/hashes/identity' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import * as PeerIdFactory from '../src/index.js' +import goId from './fixtures/go-private-key.js' +import testId from './fixtures/sample-id.js' + +const LIBP2P_KEY_CODE = 0x72 +const RAW_CODE = 0x55 + +const testIdBytes = base16.decode(`f${testId.id}`) +const testIdDigest = Digest.decode(testIdBytes) +const testIdB58String = base58btc.encode(testIdBytes).substring(1) +const testIdB36String = base36.encode(testIdBytes) +const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) +const testIdCIDString = testIdCID.toString() + +describe('PeerId', () => { + it('create an id without \'new\'', () => { + // @ts-expect-error missing args + expect(() => createPeerId()).to.throw(Error) + }) + + it('create a new id', async () => { + const id = await PeerIdFactory.createEd25519PeerId() + expect(id.toString().length).to.equal(52) + }) + + it('can be created for a secp256k1 key', async () => { + const id = await PeerIdFactory.createSecp256k1PeerId() + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('can get the public key from a Secp256k1 key', async () => { + const original = await PeerIdFactory.createSecp256k1PeerId() + const newId = peerIdFromString(original.toString()) + expect(original.publicKey).to.equalBytes(newId.publicKey) + }) + + it('recreate from a Uint8Array', () => { + const id = peerIdFromBytes(testIdBytes) + expect(testId.id).to.equal(uint8ArrayToString(id.multihash.bytes, 'base16')) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from a B58 String', () => { + const id = peerIdFromString(testIdB58String) + expect(testIdB58String).to.equal(id.toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CID object', () => { + const id = peerIdFromCID(testIdCID) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from Base58 String (CIDv0)', () => { + const id = peerIdFromCID(CID.parse(testIdB58String)) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from Base36 String', () => { + const id = peerIdFromString(testIdB36String) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CIDv1 Base32 (libp2p-key multicodec)', () => { + const cid = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) + const id = peerIdFromCID(cid) + expect(cid.toString()).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('recreate from CID Uint8Array', () => { + const id = peerIdFromBytes(testIdCID.bytes) + expect(testIdCIDString).to.equal(id.toCID().toString()) + expect(testIdBytes).to.equalBytes(id.multihash.bytes) + }) + + it('throws on invalid CID multicodec', () => { + // only libp2p and dag-pb are supported + const invalidCID = CID.createV1(RAW_CODE, testIdDigest) + expect(() => { + peerIdFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('throws on invalid multihash value', () => { + // using function code 0x50 that does not represent valid hash function + // https://github.com/multiformats/js-multihash/blob/b85999d5768bf06f1b0f16b926ef2cb6d9c14265/src/constants.js#L345 + const invalidMultihash = uint8ArrayToString(Uint8Array.from([0x50, 0x1, 0x0]), 'base58btc') + expect(() => { + peerIdFromString(invalidMultihash) + }).to.throw(/Non-base32hexpadupper character/i) + }) + + it('throws on invalid CID object', () => { + const invalidCID = {} + expect(() => { + // @ts-expect-error invalid cid is invalid type + peerIdFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('recreate from a Public Key', async () => { + const id = await PeerIdFactory.createFromPubKey(keys.unmarshalPublicKey(uint8ArrayFromString(testId.pubKey, 'base64pad'))) + + expect(testIdB58String).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.multihash.bytes) + }) + + it('recreate from a Private Key', async () => { + const id = await PeerIdFactory.createFromPrivKey(await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad'))) + expect(testIdB58String).to.equal(id.toString()) + + const encoded = await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad')) + const id2 = await PeerIdFactory.createFromPrivKey(encoded) + + if (id.type !== 'RSA') { + throw new Error('Wrong key type found') + } + + expect(testIdB58String).to.equal(id2.toString()) + expect(id.publicKey).to.deep.equal(id2.publicKey) + }) + + it('recreate from Protobuf', async () => { + const id = await PeerIdFactory.createFromProtobuf(uint8ArrayFromString(testId.marshalled, 'base16')) + expect(testIdB58String).to.equal(id.toString()) + + const key = await keys.unmarshalPrivateKey(uint8ArrayFromString(testId.privKey, 'base64pad')) + const id2 = await PeerIdFactory.createFromPrivKey(key) + + expect(testIdB58String).to.equal(id2.toString()) + expect(id.publicKey).to.equalBytes(id2.publicKey) + expect(uint8ArrayToString(PeerIdFactory.exportToProtobuf(id).subarray(), 'base16')).to.equal(testId.marshalled) + }) + + it('recreate from embedded ed25519 key', async () => { + const key = '12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + + if (id.publicKey == null) { + throw new Error('No pubic key found on Ed25519 key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('recreate from embedded secp256k1 key', async () => { + const key = '16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + + if (id.publicKey == null) { + throw new Error('No pubic key found on secp256k1 key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('recreate from string key', async () => { + const key = 'QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC' + const id = peerIdFromString(key) + expect(id.toString()).to.equal(key) + }) + + it('can be created from a secp256k1 public key', async () => { + const privKey = await keys.generateKeyPair('secp256k1', 256) + const id = await PeerIdFactory.createFromPubKey(privKey.public) + + if (id.publicKey == null) { + throw new Error('No public key found on peer id created from secp256k1 public key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('can be created from a Secp256k1 private key', async () => { + const privKey = await keys.generateKeyPair('secp256k1', 256) + const id = await PeerIdFactory.createFromPrivKey(privKey) + + if (id.publicKey == null) { + throw new Error('No public key found on peer id created from secp256k1 private key') + } + + const expB58 = base58btc.encode((identity.digest(id.publicKey)).bytes).slice(1) + expect(id.toString()).to.equal(expB58) + }) + + it('Compare generated ID with one created from PubKey', async () => { + const id1 = await PeerIdFactory.createSecp256k1PeerId() + const id2 = await PeerIdFactory.createFromPubKey(keys.unmarshalPublicKey(id1.publicKey)) + + expect(id1.multihash.bytes).to.equalBytes(id2.multihash.bytes) + }) + + it('Works with default options', async function () { + const id = await PeerIdFactory.createEd25519PeerId() + expect(id.toString().length).to.equal(52) + }) + + it('Non-default # of bits', async function () { + const shortId = await PeerIdFactory.createRSAPeerId({ bits: 512 }) + const longId = await PeerIdFactory.createRSAPeerId({ bits: 1024 }) + + if (longId.privateKey == null) { + throw new Error('No private key found on peer id') + } + + expect(shortId.privateKey).to.have.property('length').that.is.lessThan(longId.privateKey.length) + }) + + it('equals', async () => { + const ids = await Promise.all([ + PeerIdFactory.createEd25519PeerId(), + PeerIdFactory.createEd25519PeerId() + ]) + + expect(ids[0].equals(ids[0])).to.equal(true) + expect(ids[0].equals(ids[1])).to.equal(false) + expect(ids[0].equals(ids[0].multihash.bytes)).to.equal(true) + expect(ids[0].equals(ids[1].multihash.bytes)).to.equal(false) + }) + + describe('fromJSON', () => { + it('full node', async () => { + const id = await PeerIdFactory.createEd25519PeerId() + const other = await PeerIdFactory.createFromJSON({ + id: id.toString(), + privKey: id.privateKey != null ? uint8ArrayToString(id.privateKey, 'base64pad') : undefined, + pubKey: uint8ArrayToString(id.publicKey, 'base64pad') + }) + expect(id.toString()).to.equal(other.toString()) + expect(id.privateKey).to.equalBytes(other.privateKey) + expect(id.publicKey).to.equalBytes(other.publicKey) + }) + + it('only id', async () => { + const key = await keys.generateKeyPair('RSA', 1024) + const digest = await key.public.hash() + const id = peerIdFromBytes(digest) + expect(id.privateKey).to.not.exist() + expect(id.publicKey).to.not.exist() + const other = await PeerIdFactory.createFromJSON({ + id: id.toString(), + privKey: id.privateKey != null ? uint8ArrayToString(id.privateKey, 'base64pad') : undefined, + pubKey: id.publicKey != null ? uint8ArrayToString(id.publicKey, 'base64pad') : undefined + }) + expect(id.toString()).to.equal(other.toString()) + }) + + it('go interop', async () => { + const id = await PeerIdFactory.createFromJSON(goId) + expect(id.toString()).to.eql(goId.id) + }) + }) + + it('keys are equal after one is stringified', async () => { + const peerId = await PeerIdFactory.createEd25519PeerId() + const peerId1 = peerIdFromString(peerId.toString()) + const peerId2 = peerIdFromString(peerId.toString()) + + expect(peerId1).to.deep.equal(peerId2) + + peerId1.toString() + + expect(peerId1).to.deep.equal(peerId2) + }) + + describe('returns error instead of crashing', () => { + const garbage = [ + uint8ArrayFromString('00010203040506070809', 'base16'), + {}, null, false, undefined, true, 1, 0, + uint8ArrayFromString(''), 'aGVsbG93b3JsZA==', 'helloworld', '' + ] + + const fncs = ['createFromPubKey', 'createFromPrivKey', 'createFromJSON', 'createFromProtobuf'] + + for (const gb of garbage) { + for (const fn of fncs) { + it(`${fn} (${util.inspect(gb)})`, async () => { + try { + // @ts-expect-error cannot use a string to index PeerId + await PeerIdFactory[fn](gb) + } catch (err) { + expect(err).to.exist() + } + }) + } + } + }) +}) diff --git a/packages/peer-id-factory/tsconfig.json b/packages/peer-id-factory/tsconfig.json new file mode 100644 index 0000000000..6338201752 --- /dev/null +++ b/packages/peer-id-factory/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-keys" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/peer-id/CHANGELOG.md b/packages/peer-id/CHANGELOG.md new file mode 100644 index 0000000000..c2d9a92914 --- /dev/null +++ b/packages/peer-id/CHANGELOG.md @@ -0,0 +1,236 @@ +## [@libp2p/peer-id-v2.0.3](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.2...@libp2p/peer-id-v2.0.3) (2023-03-20) + + +### Documentation + +* update README.md ([#59](https://github.com/libp2p/js-libp2p-peer-id/issues/59)) ([aba6483](https://github.com/libp2p/js-libp2p-peer-id/commit/aba6483dad028ee5c24bfc01135b77568666cfd3)) + +## [@libp2p/peer-id-v2.0.2](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.1...@libp2p/peer-id-v2.0.2) (2023-02-23) + + +### Documentation + +* Fix example in README.md ([#54](https://github.com/libp2p/js-libp2p-peer-id/issues/54)) ([294a907](https://github.com/libp2p/js-libp2p-peer-id/commit/294a907ef7c13710045cb2224ab2398e78b353df)) + +## [@libp2p/peer-id-v2.0.1](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v2.0.0...@libp2p/peer-id-v2.0.1) (2023-01-18) + + +### Trivial Changes + +* replace err-code with CodeError ([#45](https://github.com/libp2p/js-libp2p-peer-id/issues/45)) ([06e39f0](https://github.com/libp2p/js-libp2p-peer-id/commit/06e39f0a8ca9e25cd3d055023ae61cde510183f8)), closes [#1269](https://github.com/libp2p/js-libp2p-peer-id/issues/1269) + + +### Dependencies + +* bump aegir from 37.12.1 to 38.1.0 ([#46](https://github.com/libp2p/js-libp2p-peer-id/issues/46)) ([ba54f6a](https://github.com/libp2p/js-libp2p-peer-id/commit/ba54f6a4a35de20528d4c60a2a532c553b9a9a34)) + +## [@libp2p/peer-id-v2.0.0](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.18...@libp2p/peer-id-v2.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 (#41) + +### Trivial Changes + +* remove lerna ([#43](https://github.com/libp2p/js-libp2p-peer-id/issues/43)) ([d458051](https://github.com/libp2p/js-libp2p-peer-id/commit/d458051bfcb7ff83c42ed26e1c12ac3d07bee492)) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 and @libp2p/interface-peer-id from 1.0.0 to 2.0.0 ([#41](https://github.com/libp2p/js-libp2p-peer-id/issues/41)) ([2aa0f79](https://github.com/libp2p/js-libp2p-peer-id/commit/2aa0f799789b52758651c115b3a021ca67e5c407)) + +## [@libp2p/peer-id-v1.1.18](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.17...@libp2p/peer-id-v1.1.18) (2022-12-16) + + +### Documentation + +* publish api docs for all modules ([#39](https://github.com/libp2p/js-libp2p-peer-id/issues/39)) ([861957a](https://github.com/libp2p/js-libp2p-peer-id/commit/861957add8610498bf095d82dd51af0082eae4b5)) + +## [@libp2p/peer-id-v1.1.17](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.16...@libp2p/peer-id-v1.1.17) (2022-12-08) + + +### Bug Fixes + +* human readable peer ids in console.log ([#36](https://github.com/libp2p/js-libp2p-peer-id/issues/36)) ([f80d1ea](https://github.com/libp2p/js-libp2p-peer-id/commit/f80d1ea0b6272692de69ed1d224e6cc16d9b84fb)) + + +### Trivial Changes + +* fix peer-d typo in readmes ([#31](https://github.com/libp2p/js-libp2p-peer-id/issues/31)) ([2276076](https://github.com/libp2p/js-libp2p-peer-id/commit/2276076650a0e1ecd0954be104eb7269e688b6ec)) + +## [@libp2p/peer-id-v1.1.16](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.15...@libp2p/peer-id-v1.1.16) (2022-10-12) + + +### Trivial Changes + +* update project config ([#14](https://github.com/libp2p/js-libp2p-peer-id/issues/14)) ([5c3918c](https://github.com/libp2p/js-libp2p-peer-id/commit/5c3918c61d8346ed1d49094bb592f8c872b7de57)) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#28](https://github.com/libp2p/js-libp2p-peer-id/issues/28)) ([e270265](https://github.com/libp2p/js-libp2p-peer-id/commit/e27026508b3684e6cb2eb896de19c161dbd21d45)) + +## [@libp2p/peer-id-v1.1.15](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.14...@libp2p/peer-id-v1.1.15) (2022-07-26) + + +### Bug Fixes + +* move error creation ([#11](https://github.com/libp2p/js-libp2p-peer-id/issues/11)) ([9d957fb](https://github.com/libp2p/js-libp2p-peer-id/commit/9d957fb141e30cf1064413cd649bb2c8724e935e)) + +## [@libp2p/peer-id-v1.1.14](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.13...@libp2p/peer-id-v1.1.14) (2022-07-18) + + +### Bug Fixes + +* Typo in constant name ([#7](https://github.com/libp2p/js-libp2p-peer-id/issues/7)) ([9063b65](https://github.com/libp2p/js-libp2p-peer-id/commit/9063b65ee3a29ca834588e915e490b9ec802647c)) + +## [@libp2p/peer-id-v1.1.13](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.12...@libp2p/peer-id-v1.1.13) (2022-06-14) + + +### Trivial Changes + +* **deps:** bump @libp2p/interface-peer-id from 0.0.1 to 1.0.0 ([#2](https://github.com/libp2p/js-libp2p-peer-id/issues/2)) ([69c0c49](https://github.com/libp2p/js-libp2p-peer-id/commit/69c0c495ab04d5b97de27d3ef20e1ee78d5b0056)) + +## [@libp2p/peer-id-v1.1.12](https://github.com/libp2p/js-libp2p-peer-id/compare/@libp2p/peer-id-v1.1.11...@libp2p/peer-id-v1.1.12) (2022-06-10) + + +### Trivial Changes + +* update interface deps ([#1](https://github.com/libp2p/js-libp2p-peer-id/issues/1)) ([3cf652d](https://github.com/libp2p/js-libp2p-peer-id/commit/3cf652d50ede0d876da46dcb0b1de387126e272a)) + +## [@libp2p/peer-id-v1.1.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.10...@libp2p/peer-id-v1.1.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-id-v1.1.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.9...@libp2p/peer-id-v1.1.10) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/peer-id-v1.1.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.8...@libp2p/peer-id-v1.1.9) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-id-v1.1.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.7...@libp2p/peer-id-v1.1.8) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/peer-id-v1.1.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.6...@libp2p/peer-id-v1.1.7) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-id-v1.1.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.5...@libp2p/peer-id-v1.1.6) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-id-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.4...@libp2p/peer-id-v1.1.5) (2022-02-18) + + +### Bug Fixes + +* simpler pubsub ([#172](https://github.com/libp2p/js-libp2p-interfaces/issues/172)) ([98715ed](https://github.com/libp2p/js-libp2p-interfaces/commit/98715ed73183b32e4fda3d878a462389548358d9)) + +## [@libp2p/peer-id-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.3...@libp2p/peer-id-v1.1.4) (2022-02-17) + + +### Bug Fixes + +* add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) + +## [@libp2p/peer-id-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.2...@libp2p/peer-id-v1.1.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-id-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.1...@libp2p/peer-id-v1.1.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-id-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.1.0...@libp2p/peer-id-v1.1.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/peer-id-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.4...@libp2p/peer-id-v1.1.0) (2022-02-09) + + +### Features + +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) + +## [@libp2p/peer-id-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.3...@libp2p/peer-id-v1.0.4) (2022-01-29) + + +### Bug Fixes + +* remove extra fields ([#153](https://github.com/libp2p/js-libp2p-interfaces/issues/153)) ([ccd7cf3](https://github.com/libp2p/js-libp2p-interfaces/commit/ccd7cf3f5ac71337baf516d3b0f6fc724ee0d3b4)) + +## [@libp2p/peer-id-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.2...@libp2p/peer-id-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/peer-id-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-id-v1.0.1...@libp2p/peer-id-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + + + + +# 0.2.0 (2022-01-02) + + +### Features + +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) diff --git a/packages/peer-id/LICENSE b/packages/peer-id/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-id/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-id/LICENSE-APACHE b/packages/peer-id/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-id/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-id/LICENSE-MIT b/packages/peer-id/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-id/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-id/README.md b/packages/peer-id/README.md new file mode 100644 index 0000000000..7c2c8c23bc --- /dev/null +++ b/packages/peer-id/README.md @@ -0,0 +1,62 @@ +# @libp2p/peer-id + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of @libp2p/interface-peer-id + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A basic implementation of a peer id + +## Example + +```JavaScript +import { peerIdFromString } from '@libp2p/peer-id' + +const peer = peerIdFromString('k51qzi5uqu5dkwkqm42v9j9kqcam2jiuvloi16g72i4i4amoo2m8u3ol3mqu6s') + +console.log(peer.toCid()) // CID(bafzaa...) +console.log(peer.toString()) // "12D3K..." +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-id/package.json b/packages/peer-id/package.json new file mode 100644 index 0000000000..42a9d54bcd --- /dev/null +++ b/packages/peer-id/package.json @@ -0,0 +1,62 @@ +{ + "name": "@libp2p/peer-id", + "version": "2.0.3", + "description": "Implementation of @libp2p/interface-peer-id", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "multiformats": "^11.0.2", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-id/src/index.ts b/packages/peer-id/src/index.ts new file mode 100644 index 0000000000..9ef86986c5 --- /dev/null +++ b/packages/peer-id/src/index.ts @@ -0,0 +1,272 @@ +import { type Ed25519PeerId, type PeerIdType, type RSAPeerId, type Secp256k1PeerId, symbol, type PeerId } from '@libp2p/interface-peer-id' +import { CodeError } from '@libp2p/interfaces/errors' +import { base58btc } from 'multiformats/bases/base58' +import { bases } from 'multiformats/basics' +import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' +import { identity } from 'multiformats/hashes/identity' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import type { MultibaseDecoder } from 'multiformats/bases/interface' +import type { MultihashDigest } from 'multiformats/hashes/interface' + +const inspect = Symbol.for('nodejs.util.inspect.custom') + +const baseDecoder = Object + .values(bases) + .map(codec => codec.decoder) + // @ts-expect-error https://github.com/multiformats/js-multiformats/issues/141 + .reduce((acc, curr) => acc.or(curr), bases.identity.decoder) + +// these values are from https://github.com/multiformats/multicodec/blob/master/table.csv +const LIBP2P_KEY_CODE = 0x72 + +const MARSHALLED_ED225519_PUBLIC_KEY_LENGTH = 36 +const MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH = 37 + +interface PeerIdInit { + type: PeerIdType + multihash: MultihashDigest + privateKey?: Uint8Array +} + +interface RSAPeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array + publicKey?: Uint8Array +} + +interface Ed25519PeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array +} + +interface Secp256k1PeerIdInit { + multihash: MultihashDigest + privateKey?: Uint8Array +} + +class PeerIdImpl { + public type: PeerIdType + public readonly multihash: MultihashDigest + public readonly privateKey?: Uint8Array + public readonly publicKey?: Uint8Array + private string?: string + + constructor (init: PeerIdInit) { + this.type = init.type + this.multihash = init.multihash + this.privateKey = init.privateKey + + // mark string cache as non-enumerable + Object.defineProperty(this, 'string', { + enumerable: false, + writable: true + }) + } + + get [Symbol.toStringTag] (): string { + return `PeerId(${this.toString()})` + } + + readonly [symbol] = true + + toString (): string { + if (this.string == null) { + this.string = base58btc.encode(this.multihash.bytes).slice(1) + } + + return this.string + } + + // return self-describing String representation + // in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 + toCID (): CID { + return CID.createV1(LIBP2P_KEY_CODE, this.multihash) + } + + toBytes (): Uint8Array { + return this.multihash.bytes + } + + /** + * Returns Multiaddr as a JSON string + */ + toJSON (): string { + return this.toString() + } + + /** + * Checks the equality of `this` peer against a given PeerId + */ + equals (id: PeerId | Uint8Array | string): boolean { + if (id instanceof Uint8Array) { + return uint8ArrayEquals(this.multihash.bytes, id) + } else if (typeof id === 'string') { + return peerIdFromString(id).equals(this as PeerId) + } else if (id?.multihash?.bytes != null) { + return uint8ArrayEquals(this.multihash.bytes, id.multihash.bytes) + } else { + throw new Error('not valid Id') + } + } + + /** + * Returns PeerId as a human-readable string + * https://nodejs.org/api/util.html#utilinspectcustom + * + * @example + * ```js + * import { peerIdFromString } from '@libp2p/peer-id' + * + * console.info(peerIdFromString('QmFoo')) + * // 'PeerId(QmFoo)' + * ``` + */ + [inspect] (): string { + return `PeerId(${this.toString()})` + } +} + +class RSAPeerIdImpl extends PeerIdImpl implements RSAPeerId { + public readonly type = 'RSA' + public readonly publicKey?: Uint8Array + + constructor (init: RSAPeerIdInit) { + super({ ...init, type: 'RSA' }) + + this.publicKey = init.publicKey + } +} + +class Ed25519PeerIdImpl extends PeerIdImpl implements Ed25519PeerId { + public readonly type = 'Ed25519' + public readonly publicKey: Uint8Array + + constructor (init: Ed25519PeerIdInit) { + super({ ...init, type: 'Ed25519' }) + + this.publicKey = init.multihash.digest + } +} + +class Secp256k1PeerIdImpl extends PeerIdImpl implements Secp256k1PeerId { + public readonly type = 'secp256k1' + public readonly publicKey: Uint8Array + + constructor (init: Secp256k1PeerIdInit) { + super({ ...init, type: 'secp256k1' }) + + this.publicKey = init.multihash.digest + } +} + +export function createPeerId (init: PeerIdInit): PeerId { + if (init.type === 'RSA') { + return new RSAPeerIdImpl(init) + } + + if (init.type === 'Ed25519') { + return new Ed25519PeerIdImpl(init) + } + + if (init.type === 'secp256k1') { + return new Secp256k1PeerIdImpl(init) + } + + throw new CodeError('Type must be "RSA", "Ed25519" or "secp256k1"', 'ERR_INVALID_PARAMETERS') +} + +export function peerIdFromPeerId (other: any): PeerId { + if (other.type === 'RSA') { + return new RSAPeerIdImpl(other) + } + + if (other.type === 'Ed25519') { + return new Ed25519PeerIdImpl(other) + } + + if (other.type === 'secp256k1') { + return new Secp256k1PeerIdImpl(other) + } + + throw new CodeError('Not a PeerId', 'ERR_INVALID_PARAMETERS') +} + +export function peerIdFromString (str: string, decoder?: MultibaseDecoder): PeerId { + decoder = decoder ?? baseDecoder + + if (str.charAt(0) === '1' || str.charAt(0) === 'Q') { + // identity hash ed25519/secp256k1 key or sha2-256 hash of + // rsa public key - base58btc encoded either way + const multihash = Digest.decode(base58btc.decode(`z${str}`)) + + if (str.startsWith('12D')) { + return new Ed25519PeerIdImpl({ multihash }) + } else if (str.startsWith('16U')) { + return new Secp256k1PeerIdImpl({ multihash }) + } else { + return new RSAPeerIdImpl({ multihash }) + } + } + + return peerIdFromBytes(baseDecoder.decode(str)) +} + +export function peerIdFromBytes (buf: Uint8Array): PeerId { + try { + const multihash = Digest.decode(buf) + + if (multihash.code === identity.code) { + if (multihash.digest.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash }) + } else if (multihash.digest.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash }) + } + } + + if (multihash.code === sha256.code) { + return new RSAPeerIdImpl({ multihash }) + } + } catch { + return peerIdFromCID(CID.decode(buf)) + } + + throw new Error('Supplied PeerID CID is invalid') +} + +export function peerIdFromCID (cid: CID): PeerId { + if (cid == null || cid.multihash == null || cid.version == null || (cid.version === 1 && cid.code !== LIBP2P_KEY_CODE)) { + throw new Error('Supplied PeerID CID is invalid') + } + + const multihash = cid.multihash + + if (multihash.code === sha256.code) { + return new RSAPeerIdImpl({ multihash: cid.multihash }) + } else if (multihash.code === identity.code) { + if (multihash.digest.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash: cid.multihash }) + } else if (multihash.digest.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash: cid.multihash }) + } + } + + throw new Error('Supplied PeerID CID is invalid') +} + +/** + * @param publicKey - A marshalled public key + * @param privateKey - A marshalled private key + */ +export async function peerIdFromKeys (publicKey: Uint8Array, privateKey?: Uint8Array): Promise { + if (publicKey.length === MARSHALLED_ED225519_PUBLIC_KEY_LENGTH) { + return new Ed25519PeerIdImpl({ multihash: Digest.create(identity.code, publicKey), privateKey }) + } + + if (publicKey.length === MARSHALLED_SECP256K1_PUBLIC_KEY_LENGTH) { + return new Secp256k1PeerIdImpl({ multihash: Digest.create(identity.code, publicKey), privateKey }) + } + + return new RSAPeerIdImpl({ multihash: await sha256.digest(publicKey), publicKey, privateKey }) +} diff --git a/packages/peer-id/test/index.spec.ts b/packages/peer-id/test/index.spec.ts new file mode 100644 index 0000000000..98af69f365 --- /dev/null +++ b/packages/peer-id/test/index.spec.ts @@ -0,0 +1,102 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { createPeerId, peerIdFromBytes, peerIdFromString } from '../src/index.js' + +describe('PeerId', () => { + it('create an id without \'new\'', () => { + // @ts-expect-error missing args + expect(() => createPeerId()).to.throw(Error) + }) + + it('create a new id from multihash', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(buf)).to.be.true() + }) + + it('parses a v1 CID with the libp2p-key codec', async () => { + const str = 'bafzaajaiaejca24q7uhr7adt3rtai4ixtn2r3q72kccwvwzg6wnfetwqyvrs5n2d' + const id = peerIdFromString(str) + expect(id.type).to.equal('Ed25519') + expect(id.toString()).to.equal('12D3KooWH4G2B3x5BZHH3j2ccMsBLhzR8u1uzrAQshg429xGFGPk') + expect(id.toCID().toString()).to.equal('bafzaajaiaejca24q7uhr7adt3rtai4ixtn2r3q72kccwvwzg6wnfetwqyvrs5n2d') + }) + + it('defaults to base58btc when stringifying', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.toString()).to.equal('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa') + }) + + it('turns into a CID', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.toCID().toString()).to.equal('bafzaajaiaejcda3tmul6p2537j5upxpjgz3jabbzxqrjqvhhfnthtnezvwibizjh') + }) + + it('equals a Uint8Array', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(buf)).to.be.true() + }) + + it('equals a PeerId', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id.equals(peerIdFromBytes(buf))).to.be.true() + }) + + it('parses a PeerId as Ed25519', async () => { + const id = peerIdFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa') + expect(id).to.have.property('type', 'Ed25519') + }) + + it('parses a PeerId as RSA', async () => { + const id = peerIdFromString('QmZHBBrcBtDk7yVzcNUDJBJsZnVGtPHzpTzu16J7Sk6hbp') + expect(id).to.have.property('type', 'RSA') + }) + + it('parses a PeerId as secp256k1', async () => { + const id = peerIdFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL') + expect(id).to.have.property('type', 'secp256k1') + }) + + it('decodes a PeerId as Ed25519', async () => { + const buf = uint8ArrayFromString('12D3KooWbtp1AcgweFSArD7dbKWYpAr8MZR1tofwNwLFLjeNGLWa', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'Ed25519') + }) + + it('decodes a PeerId as RSA', async () => { + const buf = uint8ArrayFromString('QmZHBBrcBtDk7yVzcNUDJBJsZnVGtPHzpTzu16J7Sk6hbp', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'RSA') + }) + + it('decodes a PeerId as secp256k1', async () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + expect(id).to.have.property('type', 'secp256k1') + }) + + it('caches toString output', async () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + + expect(id).to.have.property('string').that.is.not.ok() + + id.toString() + + expect(id).to.have.property('string').that.is.ok() + }) + + it('stringifies as JSON', () => { + const buf = uint8ArrayFromString('16Uiu2HAkxSnqYGDU5iZTQrZyAcQDQHKrZqSNPBmKFifEagS2XfrL', 'base58btc') + const id = peerIdFromBytes(buf) + + const res = JSON.parse(JSON.stringify({ id })) + + expect(res).to.have.property('id', id.toString()) + }) +}) diff --git a/packages/peer-id/tsconfig.json b/packages/peer-id/tsconfig.json new file mode 100644 index 0000000000..ba25a6ed4a --- /dev/null +++ b/packages/peer-id/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/peer-record/CHANGELOG.md b/packages/peer-record/CHANGELOG.md new file mode 100644 index 0000000000..fb59f472bf --- /dev/null +++ b/packages/peer-record/CHANGELOG.md @@ -0,0 +1,257 @@ +## [5.0.4](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.3...v5.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([10f3201](https://github.com/libp2p/js-libp2p-peer-record/commit/10f320136543cf4e41ae6bd2c1da8741ff840e47)) +* Update .github/workflows/stale.yml [skip ci] ([0bd8e9d](https://github.com/libp2p/js-libp2p-peer-record/commit/0bd8e9d32ae78dcba38904ed122bbeb0e1c6c2b6)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#64](https://github.com/libp2p/js-libp2p-peer-record/issues/64)) ([ba3ac38](https://github.com/libp2p/js-libp2p-peer-record/commit/ba3ac38c79e9449a75c0a54fefe289ee9e2c78fb)) + +## [5.0.3](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.2...v5.0.3) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#55](https://github.com/libp2p/js-libp2p-peer-record/issues/55)) ([edc67ec](https://github.com/libp2p/js-libp2p-peer-record/commit/edc67eceaaf8d1a7f1e41d26b95872860633abf0)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.1...v5.0.2) (2023-03-13) + + +### Bug Fixes + +* remove unused deps ([#52](https://github.com/libp2p/js-libp2p-peer-record/issues/52)) ([9d707bc](https://github.com/libp2p/js-libp2p-peer-record/commit/9d707bc5ea98185c36346d1d58cdd55d723dd7b3)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v5.0.0...v5.0.1) (2023-03-10) + + +### Trivial Changes + +* replace err-code with CodeError ([#42](https://github.com/libp2p/js-libp2p-peer-record/issues/42)) ([b76d07f](https://github.com/libp2p/js-libp2p-peer-record/commit/b76d07f07e7de5f65bd00a3c3c4a106fcca99f57)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([157d49d](https://github.com/libp2p/js-libp2p-peer-record/commit/157d49d80d2e6930311aac69980889174addfcdd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([920dfd3](https://github.com/libp2p/js-libp2p-peer-record/commit/920dfd323e876becae7a98493dc92cd3c40c1e73)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1ef05ef](https://github.com/libp2p/js-libp2p-peer-record/commit/1ef05ef112b5136afe0e3dea00dc3abcf92d2556)) +* upgrade `aegir` to `38.1.2` ([#48](https://github.com/libp2p/js-libp2p-peer-record/issues/48)) ([e0fd5e3](https://github.com/libp2p/js-libp2p-peer-record/commit/e0fd5e3ef3cddd82b4c65817835a783edaebfb3b)) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#45](https://github.com/libp2p/js-libp2p-peer-record/issues/45)) ([83958a5](https://github.com/libp2p/js-libp2p-peer-record/commit/83958a5b1cae2b33cbeee4bb998c790c973db776)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.5...v5.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to 11.x.x (#41) + +### Bug Fixes + +* update multiformats to 11.x.x ([#41](https://github.com/libp2p/js-libp2p-peer-record/issues/41)) ([1c1a4c6](https://github.com/libp2p/js-libp2p-peer-record/commit/1c1a4c6285cc03a7ee61d22a46d35c1b2dee5588)) + + +### Dependencies + +* update logger dep ([c9b6e92](https://github.com/libp2p/js-libp2p-peer-record/commit/c9b6e92d294e2e2f57b573cfaacea1006594c363)) + +## [4.0.5](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.4...v4.0.5) (2022-12-16) + + +### Dependencies + +* bump it-all from 1.0.6 to 2.0.0 ([#32](https://github.com/libp2p/js-libp2p-peer-record/issues/32)) ([fd1f6aa](https://github.com/libp2p/js-libp2p-peer-record/commit/fd1f6aaaeefa3991f9f9dd82342618faae92fbbc)) +* bump it-filter from 1.0.3 to 2.0.0 ([#31](https://github.com/libp2p/js-libp2p-peer-record/issues/31)) ([1369be9](https://github.com/libp2p/js-libp2p-peer-record/commit/1369be940ea2519950aeb0b6fc8969c50b2c0457)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* bump it-foreach from 0.1.1 to 1.0.0 ([#33](https://github.com/libp2p/js-libp2p-peer-record/issues/33)) ([64e2fc4](https://github.com/libp2p/js-libp2p-peer-record/commit/64e2fc4a9058b37a542e7571af9b6bf282ac907e)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* bump it-map from 1.0.6 to 2.0.0 ([#30](https://github.com/libp2p/js-libp2p-peer-record/issues/30)) ([eb307e4](https://github.com/libp2p/js-libp2p-peer-record/commit/eb307e478c8bf3a3daf1881312d6af6e18dccf14)), closes [#28](https://github.com/libp2p/js-libp2p-peer-record/issues/28) +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#36](https://github.com/libp2p/js-libp2p-peer-record/issues/36)) ([bddbdb7](https://github.com/libp2p/js-libp2p-peer-record/commit/bddbdb7fd367540be8db48d5db32378dbe087e32)) + + +### Documentation + +* publish api docs ([#39](https://github.com/libp2p/js-libp2p-peer-record/issues/39)) ([4477f4b](https://github.com/libp2p/js-libp2p-peer-record/commit/4477f4bdb7fda01fc4a17d85d3e372e81c396674)) + +## [4.0.4](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.3...v4.0.4) (2022-10-12) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#29](https://github.com/libp2p/js-libp2p-peer-record/issues/29)) ([04bf67d](https://github.com/libp2p/js-libp2p-peer-record/commit/04bf67daedbc75a9f7e6144c1bc818cd7f872b5b)) + +## [4.0.3](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.2...v4.0.3) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([11230eb](https://github.com/libp2p/js-libp2p-peer-record/commit/11230eb3a6a6e9791e47b7374e0b606be5ca2cc3)) +* update project config ([#25](https://github.com/libp2p/js-libp2p-peer-record/issues/25)) ([cca2319](https://github.com/libp2p/js-libp2p-peer-record/commit/cca231954701ed2edce6d41d4c3cd3d80d747549)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#26](https://github.com/libp2p/js-libp2p-peer-record/issues/26)) ([9b6f719](https://github.com/libp2p/js-libp2p-peer-record/commit/9b6f719628bcd2a69a23cf672e398b69b4415629)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.1...v4.0.2) (2022-08-12) + + +### Dependencies + +* bump interface-datastore from 6.1.1 to 7.0.0 ([#22](https://github.com/libp2p/js-libp2p-peer-record/issues/22)) ([f516cf0](https://github.com/libp2p/js-libp2p-peer-record/commit/f516cf0e015bbb15810bf3f121c9c911b6ffb160)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v4.0.0...v4.0.1) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#21](https://github.com/libp2p/js-libp2p-peer-record/issues/21)) ([9d2e881](https://github.com/libp2p/js-libp2p-peer-record/commit/9d2e88165271c283a6ded4837bc2681fd8cf2f0a)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v3.0.0...v4.0.0) (2022-08-03) + + +### ⚠ BREAKING CHANGES + +* update interface deps to use byte lists (#16) + +### Trivial Changes + +* update project config ([#15](https://github.com/libp2p/js-libp2p-peer-record/issues/15)) ([cd6bded](https://github.com/libp2p/js-libp2p-peer-record/commit/cd6bded2ef90a65c3026bb028879ae6c1f09c260)) + + +### Dependencies + +* update interface deps to use byte lists ([#16](https://github.com/libp2p/js-libp2p-peer-record/issues/16)) ([878f0ca](https://github.com/libp2p/js-libp2p-peer-record/commit/878f0ca0945d535c578c0285b33532bfefd80fbb)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.2...v3.0.0) (2022-06-27) + + +### ⚠ BREAKING CHANGES + +* **deps:** Bump @libp2p/utils from 2.0.0 to 3.0.0 (#10) + +### Trivial Changes + +* **deps:** Bump @libp2p/utils from 2.0.0 to 3.0.0 ([#10](https://github.com/libp2p/js-libp2p-peer-record/issues/10)) ([17bca9f](https://github.com/libp2p/js-libp2p-peer-record/commit/17bca9fa8447a501ec473f4732017e102ef01141)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.1...v2.0.2) (2022-06-17) + + +### Trivial Changes + +* update test deps ([#9](https://github.com/libp2p/js-libp2p-peer-record/issues/9)) ([6fb04c4](https://github.com/libp2p/js-libp2p-peer-record/commit/6fb04c45d0f9e7c875b3d7e4228b6d6f78180f92)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-peer-record/compare/v2.0.0...v2.0.1) (2022-06-17) + + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#6](https://github.com/libp2p/js-libp2p-peer-record/issues/6)) ([74c21d1](https://github.com/libp2p/js-libp2p-peer-record/commit/74c21d14991ed5f16c372584fc0966ba17bdfc22)) +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#7](https://github.com/libp2p/js-libp2p-peer-record/issues/7)) ([a50d068](https://github.com/libp2p/js-libp2p-peer-record/commit/a50d0685b6c4e2b5038af74f01910deb272efbb4)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-record/compare/v1.0.12...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#3](https://github.com/libp2p/js-libp2p-peer-record/issues/3)) ([3448776](https://github.com/libp2p/js-libp2p-peer-record/commit/34487765885d10fadf2b1e7727e0f1587f72ad97)) + +## [@libp2p/peer-record-v1.0.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.11...@libp2p/peer-record-v1.0.12) (2022-05-20) + + +### Bug Fixes + +* update sibling deps ([#216](https://github.com/libp2p/js-libp2p-interfaces/issues/216)) ([0ceca65](https://github.com/libp2p/js-libp2p-interfaces/commit/0ceca658901e92de554c828105b328b88a1416f8)) + +## [@libp2p/peer-record-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.10...@libp2p/peer-record-v1.0.11) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-record-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.9...@libp2p/peer-record-v1.0.10) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/peer-record-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.8...@libp2p/peer-record-v1.0.9) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-record-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.7...@libp2p/peer-record-v1.0.8) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-record-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.6...@libp2p/peer-record-v1.0.7) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/peer-record-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.5...@libp2p/peer-record-v1.0.6) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-record-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.4...@libp2p/peer-record-v1.0.5) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-record-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.3...@libp2p/peer-record-v1.0.4) (2022-02-17) + + +### Bug Fixes + +* add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) + +## [@libp2p/peer-record-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.2...@libp2p/peer-record-v1.0.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-record-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.1...@libp2p/peer-record-v1.0.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-record-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-record-v1.0.0...@libp2p/peer-record-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/peer-record-v1.0.0 (2022-02-09) + + +### Features + +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) diff --git a/packages/peer-record/LICENSE b/packages/peer-record/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-record/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-record/LICENSE-APACHE b/packages/peer-record/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-record/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-record/LICENSE-MIT b/packages/peer-record/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-record/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-record/README.md b/packages/peer-record/README.md new file mode 100644 index 0000000000..358efe226b --- /dev/null +++ b/packages/peer-record/README.md @@ -0,0 +1,187 @@ +# @libp2p/peer-record + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Used to transfer signed peer data across the network + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Libp2p nodes need to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information over its lifetime. Accordingly, libp2p nodes need to be able to verify that the data came from a specific peer and that it hasn't been tampered with. + +### Envelope + +Libp2p provides an all-purpose data container called **envelope**. It was created to enable the distribution of verifiable records, which we can prove originated from the addressed peer itself. The envelope includes a signature of the data, so that its authenticity is verified. + +This envelope stores a marshaled record implementing the [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record). These Records are designed to be serialized to bytes and placed inside of the envelopes before being shared with other peers. + +You can read further about the envelope in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217). + +## Usage + +- create an envelope with an instance of an [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record) implementation and prepare it for being exchanged: + +```js +// interface-record implementation example with the "libp2p-example" namespace +import { PeerRecord } from '@libp2p/peer-record' +import { fromString } from 'uint8arrays/from-string' + +class ExampleRecord extends PeerRecord { + constructor () { + super ('libp2p-example', fromString('0302', 'hex')) + } + + marshal () {} + + equals (other) {} +} + +ExampleRecord.createFromProtobuf = () => {} +``` + +```js +import { PeerEnvelope } from '@libp2p/peer-record' +import { ExampleRecord } from './example-record.js' + +const rec = new ExampleRecord() +const e = await PeerEnvelope.seal(rec, peerId) +const wireData = e.marshal() +``` + +- consume a received envelope (`wireData`) and transform it back to a record: + +```js +import { PeerEnvelope } from '@libp2p/peer-record' +import { ExampleRecord } from './example-record.js' + +const domain = 'libp2p-example' +let e + +try { + e = await PeerEnvelope.openAndCertify(wireData, domain) +} catch (err) {} + +const rec = ExampleRecord.createFromProtobuf(e.payload) +``` + +## Peer Record + +All libp2p nodes keep a `PeerStore`, that among other information stores a set of known addresses for each peer, which can come from a variety of sources. + +Libp2p peer records were created to enable the distribution of verifiable address records, which we can prove originated from the addressed peer itself. With such guarantees, libp2p is able to prioritize addresses based on their authenticity, with the most strict strategy being to only dial certified addresses (no strategies have been implemented at the time of writing). + +A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing. It also contains a `seqNumber` field, a timestamp per the spec, so that we can verify the most recent record. + +You can read further about the Peer Record in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217). + +### Usage + +- create a new Peer Record + +```js +import { PeerRecord } from '@libp2p/peer-record' + +const pr = new PeerRecord({ + peerId: node.peerId, + multiaddrs: node.multiaddrs +}) +``` + +- create a Peer Record from a protobuf + +```js +import { PeerRecord } from '@libp2p/peer-record' + +const pr = PeerRecord.createFromProtobuf(data) +``` + +### Libp2p Flows + +#### Self Record + +Once a libp2p node has started and is listening on a set of multiaddrs, its own peer record can be created. + +The identify service is responsible for creating the self record when the identify protocol kicks in for the first time. This record will be stored for future needs of the identify protocol when connecting with other peers. + +#### Self record Updates + +***NOT\_YET\_IMPLEMENTED*** + +While creating peer records is fairly trivial, addresses are not static and might be modified at arbitrary times. This can happen via an Address Manager API, or even through AutoRelay/AutoNAT. + +When a libp2p node changes its listen addresses, the identify service will be informed. Once that happens, the identify service creates a new self record and stores it. With the new record, the identify push/delta protocol will be used to communicate this change to the connected peers. + +#### Subsystem receiving a record + +Considering that a node can discover other peers' addresses from a variety of sources, Libp2p Peerstore can differentiate the addresses that were obtained through a signed peer record. + +Once a record is received and its signature properly validated, its envelope is stored in the AddressBook in its byte representation. The `seqNumber` remains unmarshalled so that we can quickly compare it against incoming records to determine the most recent record. + +The AddressBook Addresses will be updated with the content of the envelope with a certified property. This allows other subsystems to identify the known certified addresses of a peer. + +#### Subsystem providing a record + +Libp2p subsystems that exchange other peers information will provide the envelope that they received by those peers. As a result, other peers can verify if the envelope was really created by the addressed peer. + +When a subsystem wants to provide a record, it will get it from the AddressBook, if it exists. Other subsystems are also able to provide the self record, since it is also stored in the AddressBook. + +### Future Work + +- Persistence only considering certified addresses? +- Peers may not know their own addresses. It's often impossible to automatically infer one's own public address, and peers may need to rely on third party peers to inform them of their observed public addresses. +- A peer may inadvertently or maliciously sign an address that they do not control. In other words, a signature isn't a guarantee that a given address is valid. +- Some addresses may be ambiguous. For example, addresses on a private subnet are valid within that subnet but are useless on the public internet. +- Once all these pieces are in place, we will also need a way to prioritize addresses based on their authenticity, that is, the dialer can prioritize self-certified addresses over addresses from an unknown origin. + - Modular dialer? (taken from go PR notes) + - With the modular dialer, users should easily be able to configure precedence. With dialer v1, anything we do to prioritise dials is gonna be spaghetti and adhoc. With the modular dialer, you’d be able to specify the order of dials when instantiating the pipeline. + - Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/peer-record/package.json b/packages/peer-record/package.json new file mode 100644 index 0000000000..b3fbd458e2 --- /dev/null +++ b/packages/peer-record/package.json @@ -0,0 +1,80 @@ +{ + "name": "@libp2p/peer-record", + "version": "5.0.4", + "description": "Used to transfer signed peer data across the network", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-record#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/envelope/*.d.ts", + "src/envelope/envelope.js", + "src/peer-record/*.d.ts", + "src/peer-record/peer-record.js" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "generate": "protons src/envelope/envelope.proto src/peer-record/peer-record.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/crypto": "^1.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-record": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/multiaddr": "^12.1.3", + "protons-runtime": "^5.0.0", + "uint8-varint": "^1.0.2", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/interface-record-compliance-tests": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-record/src/envelope/envelope.proto b/packages/peer-record/src/envelope/envelope.proto new file mode 100644 index 0000000000..5b80cf504c --- /dev/null +++ b/packages/peer-record/src/envelope/envelope.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +message Envelope { + // public_key is the public key of the keypair the enclosed payload was + // signed with. + bytes public_key = 1; + + // payload_type encodes the type of payload, so that it can be deserialized + // deterministically. + bytes payload_type = 2; + + // payload is the actual payload carried inside this envelope. + bytes payload = 3; + + // signature is the signature produced by the private key corresponding to + // the enclosed public key, over the payload, prefixing a domain string for + // additional security. + bytes signature = 5; +} \ No newline at end of file diff --git a/packages/peer-record/src/envelope/envelope.ts b/packages/peer-record/src/envelope/envelope.ts new file mode 100644 index 0000000000..5a5d6e640a --- /dev/null +++ b/packages/peer-record/src/envelope/envelope.ts @@ -0,0 +1,97 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Envelope { + publicKey: Uint8Array + payloadType: Uint8Array + payload: Uint8Array + signature: Uint8Array +} + +export namespace Envelope { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.publicKey != null && obj.publicKey.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.publicKey) + } + + if ((obj.payloadType != null && obj.payloadType.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.payloadType) + } + + if ((obj.payload != null && obj.payload.byteLength > 0)) { + w.uint32(26) + w.bytes(obj.payload) + } + + if ((obj.signature != null && obj.signature.byteLength > 0)) { + w.uint32(42) + w.bytes(obj.signature) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + publicKey: new Uint8Array(0), + payloadType: new Uint8Array(0), + payload: new Uint8Array(0), + signature: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.publicKey = reader.bytes() + break + case 2: + obj.payloadType = reader.bytes() + break + case 3: + obj.payload = reader.bytes() + break + case 5: + obj.signature = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Envelope.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Envelope => { + return decodeMessage(buf, Envelope.codec()) + } +} diff --git a/packages/peer-record/src/envelope/index.ts b/packages/peer-record/src/envelope/index.ts new file mode 100644 index 0000000000..6ef0d7951d --- /dev/null +++ b/packages/peer-record/src/envelope/index.ts @@ -0,0 +1,162 @@ +import { unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interfaces/errors' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { unsigned } from 'uint8-varint' +import { Uint8ArrayList } from 'uint8arraylist' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8arraysFromString } from 'uint8arrays/from-string' +import { codes } from '../errors.js' +import { Envelope as Protobuf } from './envelope.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Record, Envelope } from '@libp2p/interface-record' + +export interface RecordEnvelopeInit { + peerId: PeerId + payloadType: Uint8Array + payload: Uint8Array + signature: Uint8Array +} + +export class RecordEnvelope implements Envelope { + /** + * Unmarshal a serialized Envelope protobuf message + */ + static createFromProtobuf = async (data: Uint8Array | Uint8ArrayList): Promise => { + const envelopeData = Protobuf.decode(data) + const peerId = await peerIdFromKeys(envelopeData.publicKey) + + return new RecordEnvelope({ + peerId, + payloadType: envelopeData.payloadType, + payload: envelopeData.payload, + signature: envelopeData.signature + }) + } + + /** + * Seal marshals the given Record, places the marshaled bytes inside an Envelope + * and signs it with the given peerId's private key + */ + static seal = async (record: Record, peerId: PeerId): Promise => { + if (peerId.privateKey == null) { + throw new Error('Missing private key') + } + + const domain = record.domain + const payloadType = record.codec + const payload = record.marshal() + const signData = formatSignaturePayload(domain, payloadType, payload) + const key = await unmarshalPrivateKey(peerId.privateKey) + const signature = await key.sign(signData.subarray()) + + return new RecordEnvelope({ + peerId, + payloadType, + payload, + signature + }) + } + + /** + * Open and certify a given marshalled envelope. + * Data is unmarshalled and the signature validated for the given domain. + */ + static openAndCertify = async (data: Uint8Array | Uint8ArrayList, domain: string): Promise => { + const envelope = await RecordEnvelope.createFromProtobuf(data) + const valid = await envelope.validate(domain) + + if (!valid) { + throw new CodeError('envelope signature is not valid for the given domain', codes.ERR_SIGNATURE_NOT_VALID) + } + + return envelope + } + + public peerId: PeerId + public payloadType: Uint8Array + public payload: Uint8Array + public signature: Uint8Array + public marshaled?: Uint8Array + + /** + * The Envelope is responsible for keeping an arbitrary signed record + * by a libp2p peer. + */ + constructor (init: RecordEnvelopeInit) { + const { peerId, payloadType, payload, signature } = init + + this.peerId = peerId + this.payloadType = payloadType + this.payload = payload + this.signature = signature + } + + /** + * Marshal the envelope content + */ + marshal (): Uint8Array { + if (this.peerId.publicKey == null) { + throw new Error('Missing public key') + } + + if (this.marshaled == null) { + this.marshaled = Protobuf.encode({ + publicKey: this.peerId.publicKey, + payloadType: this.payloadType, + payload: this.payload.subarray(), + signature: this.signature + }) + } + + return this.marshaled + } + + /** + * Verifies if the other Envelope is identical to this one + */ + equals (other: Envelope): boolean { + return uint8ArrayEquals(this.marshal(), other.marshal()) + } + + /** + * Validate envelope data signature for the given domain + */ + async validate (domain: string): Promise { + const signData = formatSignaturePayload(domain, this.payloadType, this.payload) + + if (this.peerId.publicKey == null) { + throw new Error('Missing public key') + } + + const key = unmarshalPublicKey(this.peerId.publicKey) + + return key.verify(signData.subarray(), this.signature) + } +} + +/** + * Helper function that prepares a Uint8Array to sign or verify a signature + */ +const formatSignaturePayload = (domain: string, payloadType: Uint8Array, payload: Uint8Array | Uint8ArrayList): Uint8ArrayList => { + // When signing, a peer will prepare a Uint8Array by concatenating the following: + // - The length of the domain separation string string in bytes + // - The domain separation string, encoded as UTF-8 + // - The length of the payload_type field in bytes + // - The value of the payload_type field + // - The length of the payload field in bytes + // - The value of the payload field + + const domainUint8Array = uint8arraysFromString(domain) + const domainLength = unsigned.encode(domainUint8Array.byteLength) + const payloadTypeLength = unsigned.encode(payloadType.length) + const payloadLength = unsigned.encode(payload.length) + + return new Uint8ArrayList( + domainLength, + domainUint8Array, + payloadTypeLength, + payloadType, + payloadLength, + payload + ) +} diff --git a/packages/peer-record/src/errors.ts b/packages/peer-record/src/errors.ts new file mode 100644 index 0000000000..0c09e34d60 --- /dev/null +++ b/packages/peer-record/src/errors.ts @@ -0,0 +1,4 @@ + +export const codes = { + ERR_SIGNATURE_NOT_VALID: 'ERR_SIGNATURE_NOT_VALID' +} diff --git a/packages/peer-record/src/index.ts b/packages/peer-record/src/index.ts new file mode 100644 index 0000000000..1cba7da4de --- /dev/null +++ b/packages/peer-record/src/index.ts @@ -0,0 +1,5 @@ + +export { RecordEnvelope } from './envelope/index.js' +export type { RecordEnvelopeInit } from './envelope/index.js' +export { PeerRecord } from './peer-record/index.js' +export type { PeerRecordInit } from './peer-record/index.js' diff --git a/packages/peer-record/src/peer-record/consts.ts b/packages/peer-record/src/peer-record/consts.ts new file mode 100644 index 0000000000..8f862e33b2 --- /dev/null +++ b/packages/peer-record/src/peer-record/consts.ts @@ -0,0 +1,8 @@ + +// The domain string used for peer records contained in a Envelope. +export const ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record' + +// The type hint used to identify peer records in a Envelope. +// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv +// with name "libp2p-peer-record" +export const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Uint8Array.from([3, 1]) diff --git a/packages/peer-record/src/peer-record/index.ts b/packages/peer-record/src/peer-record/index.ts new file mode 100644 index 0000000000..bd93db078e --- /dev/null +++ b/packages/peer-record/src/peer-record/index.ts @@ -0,0 +1,104 @@ +import { peerIdFromBytes } from '@libp2p/peer-id' +import { arrayEquals } from '@libp2p/utils/array-equals' +import { multiaddr } from '@multiformats/multiaddr' +import { + ENVELOPE_DOMAIN_PEER_RECORD, + ENVELOPE_PAYLOAD_TYPE_PEER_RECORD +} from './consts.js' +import { PeerRecord as Protobuf } from './peer-record.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerRecordInit { + peerId: PeerId + + /** + * Addresses of the associated peer. + */ + multiaddrs?: Multiaddr[] + + /** + * Monotonically-increasing sequence counter that's used to order PeerRecords in time. + */ + seqNumber?: bigint +} + +/** + * The PeerRecord is used for distributing peer routing records across the network. + * It contains the peer's reachable listen addresses. + */ +export class PeerRecord { + /** + * Unmarshal Peer Record Protobuf + */ + static createFromProtobuf = (buf: Uint8Array | Uint8ArrayList): PeerRecord => { + const peerRecord = Protobuf.decode(buf) + const peerId = peerIdFromBytes(peerRecord.peerId) + const multiaddrs = (peerRecord.addresses ?? []).map((a) => multiaddr(a.multiaddr)) + const seqNumber = peerRecord.seq + + return new PeerRecord({ peerId, multiaddrs, seqNumber }) + } + + static DOMAIN = ENVELOPE_DOMAIN_PEER_RECORD + static CODEC = ENVELOPE_PAYLOAD_TYPE_PEER_RECORD + + public peerId: PeerId + public multiaddrs: Multiaddr[] + public seqNumber: bigint + public domain = PeerRecord.DOMAIN + public codec = PeerRecord.CODEC + private marshaled?: Uint8Array + + constructor (init: PeerRecordInit) { + const { peerId, multiaddrs, seqNumber } = init + + this.peerId = peerId + this.multiaddrs = multiaddrs ?? [] + this.seqNumber = seqNumber ?? BigInt(Date.now()) + } + + /** + * Marshal a record to be used in an envelope + */ + marshal (): Uint8Array { + if (this.marshaled == null) { + this.marshaled = Protobuf.encode({ + peerId: this.peerId.toBytes(), + seq: BigInt(this.seqNumber), + addresses: this.multiaddrs.map((m) => ({ + multiaddr: m.bytes + })) + }) + } + + return this.marshaled + } + + /** + * Returns true if `this` record equals the `other` + */ + equals (other: unknown): boolean { + if (!(other instanceof PeerRecord)) { + return false + } + + // Validate PeerId + if (!this.peerId.equals(other.peerId)) { + return false + } + + // Validate seqNumber + if (this.seqNumber !== other.seqNumber) { + return false + } + + // Validate multiaddrs + if (!arrayEquals(this.multiaddrs, other.multiaddrs)) { + return false + } + + return true + } +} diff --git a/packages/peer-record/src/peer-record/peer-record.proto b/packages/peer-record/src/peer-record/peer-record.proto new file mode 100644 index 0000000000..6b740dc80f --- /dev/null +++ b/packages/peer-record/src/peer-record/peer-record.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +message PeerRecord { + // AddressInfo is a wrapper around a binary multiaddr. It is defined as a + // separate message to allow us to add per-address metadata in the future. + message AddressInfo { + bytes multiaddr = 1; + } + + // peer_id contains a libp2p peer id in its binary representation. + bytes peer_id = 1; + + // seq contains a monotonically-increasing sequence counter to order PeerRecords in time. + uint64 seq = 2; + + // addresses is a list of public listen addresses for the peer. + repeated AddressInfo addresses = 3; +} \ No newline at end of file diff --git a/packages/peer-record/src/peer-record/peer-record.ts b/packages/peer-record/src/peer-record/peer-record.ts new file mode 100644 index 0000000000..0e3e9814fa --- /dev/null +++ b/packages/peer-record/src/peer-record/peer-record.ts @@ -0,0 +1,147 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface PeerRecord { + peerId: Uint8Array + seq: bigint + addresses: PeerRecord.AddressInfo[] +} + +export namespace PeerRecord { + export interface AddressInfo { + multiaddr: Uint8Array + } + + export namespace AddressInfo { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.multiaddr != null && obj.multiaddr.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.multiaddr) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + multiaddr: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.multiaddr = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, AddressInfo.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): AddressInfo => { + return decodeMessage(buf, AddressInfo.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.peerId != null && obj.peerId.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.peerId) + } + + if ((obj.seq != null && obj.seq !== 0n)) { + w.uint32(16) + w.uint64(obj.seq) + } + + if (obj.addresses != null) { + for (const value of obj.addresses) { + w.uint32(26) + PeerRecord.AddressInfo.codec().encode(value, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + peerId: new Uint8Array(0), + seq: 0n, + addresses: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.peerId = reader.bytes() + break + case 2: + obj.seq = reader.uint64() + break + case 3: + obj.addresses.push(PeerRecord.AddressInfo.codec().decode(reader, reader.uint32())) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PeerRecord.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PeerRecord => { + return decodeMessage(buf, PeerRecord.codec()) + } +} diff --git a/packages/peer-record/test/envelope.spec.ts b/packages/peer-record/test/envelope.spec.ts new file mode 100644 index 0000000000..1f536288c8 --- /dev/null +++ b/packages/peer-record/test/envelope.spec.ts @@ -0,0 +1,89 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { RecordEnvelope } from '../src/envelope/index.js' +import { codes as ErrorCodes } from '../src/errors.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Record } from '@libp2p/interface-record' + +const domain = 'libp2p-testing' +const codec = uint8arrayFromString('/libp2p/testdata') + +class TestRecord implements Record { + public domain: string + public codec: Uint8Array + public data: string + + constructor (data: string) { + this.domain = domain + this.codec = codec + this.data = data + } + + marshal (): Uint8Array { + return uint8arrayFromString(this.data) + } + + equals (other: Record): boolean { + return uint8ArrayEquals(this.marshal(), other.marshal()) + } +} + +describe('Envelope', () => { + const payloadType = codec + let peerId: PeerId + let testRecord: TestRecord + + before(async () => { + peerId = await createEd25519PeerId() + testRecord = new TestRecord('test-data') + }) + + it('creates an envelope with a random key', () => { + const payload = testRecord.marshal() + const signature = uint8arrayFromString(Math.random().toString(36).substring(7)) + + const envelope = new RecordEnvelope({ + peerId, + payloadType, + payload, + signature + }) + + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.equalBytes(payloadType) + expect(envelope.payload.subarray()).to.equalBytes(payload.subarray()) + expect(envelope.signature).to.equalBytes(signature) + }) + + it('can seal a record', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.eql(payloadType) + expect(envelope.payload).to.exist() + expect(envelope.signature).to.exist() + }) + + it('can open and verify a sealed record', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + const unmarshalledEnvelope = await RecordEnvelope.openAndCertify(rawEnvelope, testRecord.domain) + expect(unmarshalledEnvelope).to.exist() + + const equals = envelope.equals(unmarshalledEnvelope) + expect(equals).to.eql(true) + }) + + it('throw on open and verify when a different domain is used', async () => { + const envelope = await RecordEnvelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + await expect(RecordEnvelope.openAndCertify(rawEnvelope, '/bad-domain')) + .to.eventually.be.rejected() + .and.to.have.property('code', ErrorCodes.ERR_SIGNATURE_NOT_VALID) + }) +}) diff --git a/packages/peer-record/test/peer-record.spec.ts b/packages/peer-record/test/peer-record.spec.ts new file mode 100644 index 0000000000..f2fa396a86 --- /dev/null +++ b/packages/peer-record/test/peer-record.spec.ts @@ -0,0 +1,156 @@ +/* eslint-env mocha */ + +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import tests from '@libp2p/interface-record-compliance-tests' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { RecordEnvelope } from '../src/envelope/index.js' +import { PeerRecord } from '../src/peer-record/index.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +describe('interface-record compliance', () => { + tests({ + async setup () { + const peerId = await createEd25519PeerId() + return new PeerRecord({ peerId }) + }, + async teardown () { + // cleanup resources created by setup() + } + }) +}) + +describe('PeerRecord', () => { + let peerId: PeerId + + before(async () => { + peerId = await createEd25519PeerId() + }) + + it('de/serializes the same as a go record', async () => { + const privKey = Uint8Array.from([8, 1, 18, 64, 133, 251, 231, 43, 96, 100, 40, 144, 4, 165, 49, 249, 103, 137, 141, 245, 49, 158, 224, 41, 146, 253, 216, 64, 33, 250, 80, 82, 67, 75, 246, 238, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196]) + const rawEnvelope = Uint8Array.from([10, 36, 8, 1, 18, 32, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196, 18, 2, 3, 1, 26, 170, 1, 10, 38, 0, 36, 8, 1, 18, 32, 17, 187, 163, 237, 23, 33, 148, 140, 239, 180, 229, 11, 10, 11, 181, 202, 216, 166, 181, 45, 199, 177, 164, 15, 79, 102, 82, 16, 92, 145, 226, 196, 16, 216, 184, 224, 191, 147, 145, 182, 151, 22, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 0, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 1, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 2, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 3, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 4, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 5, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 6, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 7, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 8, 26, 10, 10, 8, 4, 1, 2, 3, 4, 6, 0, 9, 42, 64, 177, 151, 247, 107, 159, 40, 138, 242, 180, 103, 254, 102, 111, 119, 68, 118, 40, 112, 73, 180, 36, 183, 57, 117, 200, 134, 14, 251, 2, 55, 45, 2, 106, 121, 149, 132, 84, 26, 215, 47, 38, 84, 52, 100, 133, 188, 163, 236, 227, 100, 98, 183, 209, 177, 57, 28, 141, 39, 109, 196, 171, 139, 202, 11]) + const key = await unmarshalPrivateKey(privKey) + const peerId = await peerIdFromKeys(key.public.bytes, key.bytes) + + const env = await RecordEnvelope.openAndCertify(rawEnvelope, PeerRecord.DOMAIN) + expect(peerId.equals(env.peerId)) + + const record = PeerRecord.createFromProtobuf(env.payload) + + // The payload isn't going to match because of how the protobuf encodes uint64 values + // They are marshalled correctly on both sides, but will be off by 1 value + // Signatures will still be validated + const jsEnv = await RecordEnvelope.seal(record, peerId) + expect(env.payloadType).to.eql(jsEnv.payloadType) + }) + + it('creates a peer record with peerId', () => { + const peerRecord = new PeerRecord({ peerId }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.have.lengthOf(0) + expect(peerRecord.seqNumber).to.exist() + }) + + it('creates a peer record with provided data', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.eql(multiaddrs) + expect(peerRecord.seqNumber).to.exist() + expect(peerRecord.seqNumber).to.eql(seqNumber) + }) + + it('marshals and unmarshals a peer record', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + // Marshal + const rawData = peerRecord.marshal() + expect(rawData).to.exist() + + // Unmarshal + const unmarshalPeerRecord = PeerRecord.createFromProtobuf(rawData) + expect(unmarshalPeerRecord).to.exist() + + const equals = peerRecord.equals(unmarshalPeerRecord) + expect(equals).to.eql(true) + }) + + it('equals returns false if the peer record has a different peerId', async () => { + const peerRecord0 = new PeerRecord({ peerId }) + + const peerId1 = await createEd25519PeerId() + const peerRecord1 = new PeerRecord({ peerId: peerId1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) + + it('equals returns false if the peer record has a different seqNumber', () => { + const ts0 = BigInt(Date.now()) + const peerRecord0 = new PeerRecord({ peerId, seqNumber: ts0 }) + + const ts1 = ts0 + 20n + const peerRecord1 = new PeerRecord({ peerId, seqNumber: ts1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) + + it('equals returns false if the peer record has a different multiaddrs', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const peerRecord0 = new PeerRecord({ peerId, multiaddrs }) + + const multiaddrs1 = [ + multiaddr('/ip4/127.0.0.1/tcp/2001') + ] + const peerRecord1 = new PeerRecord({ peerId, multiaddrs: multiaddrs1 }) + + const equals = peerRecord0.equals(peerRecord1) + expect(equals).to.eql(false) + }) +}) + +describe('PeerRecord inside Envelope', () => { + let peerId: PeerId + let peerRecord: PeerRecord + + before(async () => { + peerId = await createEd25519PeerId() + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = BigInt(Date.now()) + peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + }) + + it('creates an envelope with the PeerRecord and can unmarshal it', async () => { + const e = await RecordEnvelope.seal(peerRecord, peerId) + const byteE = e.marshal() + + const decodedE = await RecordEnvelope.openAndCertify(byteE, PeerRecord.DOMAIN) + expect(decodedE).to.exist() + + const decodedPeerRecord = PeerRecord.createFromProtobuf(decodedE.payload) + + const equals = peerRecord.equals(decodedPeerRecord) + expect(equals).to.eql(true) + }) +}) diff --git a/packages/peer-record/tsconfig.json b/packages/peer-record/tsconfig.json new file mode 100644 index 0000000000..27e810f549 --- /dev/null +++ b/packages/peer-record/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-record" + }, + { + "path": "../interface-record-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/peer-store/CHANGELOG.md b/packages/peer-store/CHANGELOG.md new file mode 100644 index 0000000000..539320f571 --- /dev/null +++ b/packages/peer-store/CHANGELOG.md @@ -0,0 +1,394 @@ +## [8.2.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.2.0...v8.2.1) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d38eaf8](https://github.com/libp2p/js-libp2p-peer-store/commit/d38eaf85a359761d0d1f9ad127edbe06e6fc88ca)) +* Update .github/workflows/stale.yml [skip ci] ([9fe80a2](https://github.com/libp2p/js-libp2p-peer-store/commit/9fe80a209adbed9c6491fe7be8d80585779cc47f)) + + +### Dependencies + +* **dev:** bump p-event from 5.0.1 to 6.0.0 ([#89](https://github.com/libp2p/js-libp2p-peer-store/issues/89)) ([9d96700](https://github.com/libp2p/js-libp2p-peer-store/commit/9d9670048b5e8feeac656cba92cb2e513e4a77be)) + +## [8.2.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.4...v8.2.0) (2023-06-11) + + +### Features + +* support peer queries ([#88](https://github.com/libp2p/js-libp2p-peer-store/issues/88)) ([6b780fe](https://github.com/libp2p/js-libp2p-peer-store/commit/6b780fe49e81ce82ea83326e76b23390ac966f02)) + +## [8.1.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.3...v8.1.4) (2023-06-03) + + +### Tests + +* add tests for patching and merging protocols ([#87](https://github.com/libp2p/js-libp2p-peer-store/issues/87)) ([3e51962](https://github.com/libp2p/js-libp2p-peer-store/commit/3e5196237d3416ca730696bae6ec0797958d90c7)) + +## [8.1.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.2...v8.1.3) (2023-06-03) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#85](https://github.com/libp2p/js-libp2p-peer-store/issues/85)) ([95fb9a0](https://github.com/libp2p/js-libp2p-peer-store/commit/95fb9a0c2c547483489dc071d34bd0c56dedb330)) +* move @libp2p/peer-id-factory to dependencies ([#86](https://github.com/libp2p/js-libp2p-peer-store/issues/86)) ([7152014](https://github.com/libp2p/js-libp2p-peer-store/commit/71520143e5844a2dc39a03fbcc23938c2d39687d)), closes [/github.com/libp2p/js-libp2p-peer-store/blob/master/src/utils/bytes-to-peer.ts#L2](https://github.com/libp2p//github.com/libp2p/js-libp2p-peer-store/blob/master/src/utils/bytes-to-peer.ts/issues/L2) + +## [8.1.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.1...v8.1.2) (2023-05-10) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.5 ([#81](https://github.com/libp2p/js-libp2p-peer-store/issues/81)) ([9c8c655](https://github.com/libp2p/js-libp2p-peer-store/commit/9c8c65591c5dafd46afd54646904abc12f50658d)) + +## [8.1.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.1.0...v8.1.1) (2023-05-10) + + +### Dependencies + +* bump @libp2p/interface-libp2p from 2.0.0 to 3.1.0 ([#83](https://github.com/libp2p/js-libp2p-peer-store/issues/83)) ([9a8d6c6](https://github.com/libp2p/js-libp2p-peer-store/commit/9a8d6c61d3d64dca463c74df2984a4e294424f51)) + +## [8.1.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v8.0.0...v8.1.0) (2023-05-10) + + +### Features + +* add consume peer record method ([#84](https://github.com/libp2p/js-libp2p-peer-store/issues/84)) ([dcfc803](https://github.com/libp2p/js-libp2p-peer-store/commit/dcfc8030f94d85fe13d93d12188050c5d5cd35a2)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.2...v8.0.0) (2023-04-24) + + +### ⚠ BREAKING CHANGES + +* make peerstore atomic (#75) + +### Features + +* make peerstore atomic ([#75](https://github.com/libp2p/js-libp2p-peer-store/issues/75)) ([4e89d3b](https://github.com/libp2p/js-libp2p-peer-store/commit/4e89d3bfeef0b64ccb7ccc09185a9d682ab376e3)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.1...v7.0.2) (2023-04-11) + + +### Bug Fixes + +* dispatch peer event on adding new addresses as well as set ([#74](https://github.com/libp2p/js-libp2p-peer-store/issues/74)) ([f6d7658](https://github.com/libp2p/js-libp2p-peer-store/commit/f6d76580eb463b09ab737d817d0a0e609212ed6e)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v7.0.0...v7.0.1) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#73](https://github.com/libp2p/js-libp2p-peer-store/issues/73)) ([8ef9aa1](https://github.com/libp2p/js-libp2p-peer-store/commit/8ef9aa1db5baf797e9f1afbaf896801ee0ba3ef4)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.4...v7.0.0) (2023-03-13) + + +### ⚠ BREAKING CHANGES + +* update interface-datastore to 8.x.x (#70) + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([d2b7c22](https://github.com/libp2p/js-libp2p-peer-store/commit/d2b7c229c7497c8a05bed9cb05d85e9da96e9c12)) + + +### Dependencies + +* update interface-datastore to 8.x.x ([#70](https://github.com/libp2p/js-libp2p-peer-store/issues/70)) ([864bd19](https://github.com/libp2p/js-libp2p-peer-store/commit/864bd19e711ed0d0f0a8a509c04b6b3692097232)) + +## [6.0.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.3...v6.0.4) (2023-03-02) + + +### Bug Fixes + +* remove it-pipe ([#69](https://github.com/libp2p/js-libp2p-peer-store/issues/69)) ([dcf2e8e](https://github.com/libp2p/js-libp2p-peer-store/commit/dcf2e8e851771ea4dad025c3808d25533a06e651)), closes [#44](https://github.com/libp2p/js-libp2p-peer-store/issues/44) + +## [6.0.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.2...v6.0.3) (2023-03-02) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.6 ([#66](https://github.com/libp2p/js-libp2p-peer-store/issues/66)) ([df47658](https://github.com/libp2p/js-libp2p-peer-store/commit/df47658459ef0f691fa509475cea7161ddc0e78e)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.1...v6.0.2) (2023-03-02) + + +### Bug Fixes + +* allow overwriting tags ([#68](https://github.com/libp2p/js-libp2p-peer-store/issues/68)) ([4182211](https://github.com/libp2p/js-libp2p-peer-store/commit/4182211c0bea6df9054bb0050928c68ce467ba2a)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([e1271cf](https://github.com/libp2p/js-libp2p-peer-store/commit/e1271cf951e5e3d9c6f73f0b00c086e63d89f7de)) + + +### Dependencies + +* **dev:** bump protons from 6.1.3 to 7.0.2 ([#60](https://github.com/libp2p/js-libp2p-peer-store/issues/60)) ([0b5e25f](https://github.com/libp2p/js-libp2p-peer-store/commit/0b5e25fdc454958e6d607f046bb2c7253dceb5e4)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v6.0.0...v6.0.1) (2023-02-28) + + +### Trivial Changes + +* replace err-code with CodeError ([#53](https://github.com/libp2p/js-libp2p-peer-store/issues/53)) ([e6b87d7](https://github.com/libp2p/js-libp2p-peer-store/commit/e6b87d7f67fdd7124da265a0ca6566989a17a629)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1139dc4](https://github.com/libp2p/js-libp2p-peer-store/commit/1139dc4f495dcd905344c3ed283339ea2b79f5c2)) + + +### Documentation + +* Update API link ([#65](https://github.com/libp2p/js-libp2p-peer-store/issues/65)) ([1b75110](https://github.com/libp2p/js-libp2p-peer-store/commit/1b75110ef6017b82d55134e5fd961b9c5dbfa211)), closes [#64](https://github.com/libp2p/js-libp2p-peer-store/issues/64) + +## [6.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v5.0.1...v6.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* bump multiformats from 10.0.3 to 11.0.0 (#52) + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#52](https://github.com/libp2p/js-libp2p-peer-store/issues/52)) ([0b1335c](https://github.com/libp2p/js-libp2p-peer-store/commit/0b1335c1b6f01fc8e750704f0821523fdd2d1502)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v5.0.0...v5.0.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#51](https://github.com/libp2p/js-libp2p-peer-store/issues/51)) ([149e3b9](https://github.com/libp2p/js-libp2p-peer-store/commit/149e3b94746944d0a70f5fd1ff46743e3b4ef13f)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v4.0.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#39](https://github.com/libp2p/js-libp2p-peer-store/issues/39)) ([7434179](https://github.com/libp2p/js-libp2p-peer-store/commit/74341798cc3fb70935fec26004eef2f84a0c269c)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.5...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#36) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#36](https://github.com/libp2p/js-libp2p-peer-store/issues/36)) ([8c76da7](https://github.com/libp2p/js-libp2p-peer-store/commit/8c76da7f581f29cc54a32fbe7b96b87e73370b00)) + +## [3.1.5](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.4...v3.1.5) (2022-09-21) + + +### Bug Fixes + +* do not recreate multiaddr ([#34](https://github.com/libp2p/js-libp2p-peer-store/issues/34)) ([8fbcc57](https://github.com/libp2p/js-libp2p-peer-store/commit/8fbcc57c7f8c027c612f448fcc5a062b3c891971)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#35](https://github.com/libp2p/js-libp2p-peer-store/issues/35)) ([49aa018](https://github.com/libp2p/js-libp2p-peer-store/commit/49aa0189a2e73653bd4cf0335c97721db72e67c0)) + +## [3.1.4](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.3...v3.1.4) (2022-09-20) + + +### Bug Fixes + +* wrong letter in `multiaddrs` of README.md ([#33](https://github.com/libp2p/js-libp2p-peer-store/issues/33)) ([487c059](https://github.com/libp2p/js-libp2p-peer-store/commit/487c059d4798ba6e0dab8db259863aa2cb10d880)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([7a48e4d](https://github.com/libp2p/js-libp2p-peer-store/commit/7a48e4d11e4455e71554ef1c43d1a5650da979e8)) + +## [3.1.3](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.2...v3.1.3) (2022-08-12) + + +### Dependencies + +* update interface-datastore ([#30](https://github.com/libp2p/js-libp2p-peer-store/issues/30)) ([5aff937](https://github.com/libp2p/js-libp2p-peer-store/commit/5aff9373a2757b6ac399e205f077820728d1c72e)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.1...v3.1.2) (2022-08-11) + + +### Dependencies + +* update peer-record ([#27](https://github.com/libp2p/js-libp2p-peer-store/issues/27)) ([e0f6db3](https://github.com/libp2p/js-libp2p-peer-store/commit/e0f6db30ef8e8ebf42d6f5172a652028e2646c5a)) +* update protons to 5.1.0 ([#24](https://github.com/libp2p/js-libp2p-peer-store/issues/24)) ([9eed384](https://github.com/libp2p/js-libp2p-peer-store/commit/9eed3842bace3b3ff15ef5fbbc33ca4288dc79e0)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.1.0...v3.1.1) (2022-08-03) + + +### Trivial Changes + +* update project ([#20](https://github.com/libp2p/js-libp2p-peer-store/issues/20)) ([fce3db5](https://github.com/libp2p/js-libp2p-peer-store/commit/fce3db536890b4bf223ce0a92453dadb6898440a)) + + +### Dependencies + +* update uint8arraylist and interface deps ([#21](https://github.com/libp2p/js-libp2p-peer-store/issues/21)) ([f693e84](https://github.com/libp2p/js-libp2p-peer-store/commit/f693e8442d342d2e340dcec4ca2c7f5ede91ceb9)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v3.0.0...v3.1.0) (2022-06-24) + + +### Features + +* add peer tagging ([#12](https://github.com/libp2p/js-libp2p-peer-store/issues/12)) ([c360e41](https://github.com/libp2p/js-libp2p-peer-store/commit/c360e4151e4c5b78d3cbe4658477163e52acc205)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v2.0.0...v3.0.0) (2022-06-16) + + +### ⚠ BREAKING CHANGES + +* registrar API has changed + +### Trivial Changes + +* update deps ([#10](https://github.com/libp2p/js-libp2p-peer-store/issues/10)) ([9d0c7c0](https://github.com/libp2p/js-libp2p-peer-store/commit/9d0c7c05b6b254de7768cf5966729e9e1a53c715)) +* update readme and package.json ([#11](https://github.com/libp2p/js-libp2p-peer-store/issues/11)) ([be2de56](https://github.com/libp2p/js-libp2p-peer-store/commit/be2de561932c7f6af63ec08b82ace75b5f3b9223)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-peer-store/compare/v1.0.17...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +Co-authored-by: Alex Potsides + +### Features + +* update to latest interfaces ([#4](https://github.com/libp2p/js-libp2p-peer-store/issues/4)) ([8cb4a9a](https://github.com/libp2p/js-libp2p-peer-store/commit/8cb4a9a6f4904197552bf26cde62c1ac2f8172d0)) + +### [1.0.17](https://github.com/libp2p/js-libp2p-peer-store/compare/v1.0.16...v1.0.17) (2022-06-09) + + +### Trivial Changes + +* use correct module name in readme ([#1](https://github.com/libp2p/js-libp2p-peer-store/issues/1)) ([4f8377d](https://github.com/libp2p/js-libp2p-peer-store/commit/4f8377dd0f277e6490eabafe8cc4416e03976c14)) + +## [@libp2p/peer-store-v1.0.16](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.15...@libp2p/peer-store-v1.0.16) (2022-05-20) + + +### Bug Fixes + +* update sibling deps ([#216](https://github.com/libp2p/js-libp2p-interfaces/issues/216)) ([0ceca65](https://github.com/libp2p/js-libp2p-interfaces/commit/0ceca658901e92de554c828105b328b88a1416f8)) + +## [@libp2p/peer-store-v1.0.15](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.14...@libp2p/peer-store-v1.0.15) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/peer-store-v1.0.14](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.13...@libp2p/peer-store-v1.0.14) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/peer-store-v1.0.13](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.12...@libp2p/peer-store-v1.0.13) (2022-05-10) + + +### Bug Fixes + +* regenerate protobuf code ([#212](https://github.com/libp2p/js-libp2p-interfaces/issues/212)) ([3cf210e](https://github.com/libp2p/js-libp2p-interfaces/commit/3cf210e230863f8049ac6c3ed2e73abb180fb8b2)) + +## [@libp2p/peer-store-v1.0.12](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.11...@libp2p/peer-store-v1.0.12) (2022-05-04) + + +### Bug Fixes + +* move startable and events interfaces ([#209](https://github.com/libp2p/js-libp2p-interfaces/issues/209)) ([8ce8a08](https://github.com/libp2p/js-libp2p-interfaces/commit/8ce8a08c94b0738aa32da516558977b195ddd8ed)) + +## [@libp2p/peer-store-v1.0.11](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.10...@libp2p/peer-store-v1.0.11) (2022-05-01) + + +### Bug Fixes + +* move connection manager mock to connection manager module ([#205](https://github.com/libp2p/js-libp2p-interfaces/issues/205)) ([a367375](https://github.com/libp2p/js-libp2p-interfaces/commit/a367375accc690d7b4608c9a3313f91df700efd8)) + +## [@libp2p/peer-store-v1.0.10](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.9...@libp2p/peer-store-v1.0.10) (2022-04-19) + + +### Bug Fixes + +* move dev deps to prod ([#195](https://github.com/libp2p/js-libp2p-interfaces/issues/195)) ([3e1ffc7](https://github.com/libp2p/js-libp2p-interfaces/commit/3e1ffc7b174e74be483943ad4e5fcab823ae3f6d)) + +## [@libp2p/peer-store-v1.0.9](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.8...@libp2p/peer-store-v1.0.9) (2022-04-14) + + +### Bug Fixes + +* add logger methods, fix peer id deserialization ([#194](https://github.com/libp2p/js-libp2p-interfaces/issues/194)) ([f0e1fad](https://github.com/libp2p/js-libp2p-interfaces/commit/f0e1fad42701d73eef4233ec2b9a8aafa0b2ab96)) + +## [@libp2p/peer-store-v1.0.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.7...@libp2p/peer-store-v1.0.8) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/peer-store-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.6...@libp2p/peer-store-v1.0.7) (2022-03-24) + + +### Bug Fixes + +* rename peer data to peer info ([#187](https://github.com/libp2p/js-libp2p-interfaces/issues/187)) ([dfea342](https://github.com/libp2p/js-libp2p-interfaces/commit/dfea3429bad57abde040397e4e7a58539829e9c2)) + +## [@libp2p/peer-store-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.5...@libp2p/peer-store-v1.0.6) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/peer-store-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.4...@libp2p/peer-store-v1.0.5) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/peer-store-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.3...@libp2p/peer-store-v1.0.4) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/peer-store-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.2...@libp2p/peer-store-v1.0.3) (2022-02-12) + + +### Bug Fixes + +* hide implementations behind factory methods ([#167](https://github.com/libp2p/js-libp2p-interfaces/issues/167)) ([2fba080](https://github.com/libp2p/js-libp2p-interfaces/commit/2fba0800c9896af6dcc49da4fa904bb4a3e3e40d)) + +## [@libp2p/peer-store-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.1...@libp2p/peer-store-v1.0.2) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/peer-store-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/peer-store-v1.0.0...@libp2p/peer-store-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/peer-store-v1.0.0 (2022-02-09) + + +### Features + +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) diff --git a/packages/peer-store/LICENSE b/packages/peer-store/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/peer-store/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/peer-store/LICENSE-APACHE b/packages/peer-store/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/peer-store/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/peer-store/LICENSE-MIT b/packages/peer-store/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/peer-store/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/peer-store/README.md b/packages/peer-store/README.md new file mode 100644 index 0000000000..41a310aad6 --- /dev/null +++ b/packages/peer-store/README.md @@ -0,0 +1,49 @@ +# @libp2p/peer-store + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Stores information about peers libp2p knows on the network + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[peer-id]: https://github.com/libp2p/js-peer-id + +[peer-store-events]: [https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#libp2ppeerStore] diff --git a/packages/peer-store/package.json b/packages/peer-store/package.json new file mode 100644 index 0000000000..9acd98b945 --- /dev/null +++ b/packages/peer-store/package.json @@ -0,0 +1,87 @@ +{ + "name": "@libp2p/peer-store", + "version": "8.2.1", + "description": "Stores information about peers libp2p knows on the network", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-store#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/pb/*.d.ts", + "src/pb/peer.js" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "generate": "protons src/pb/*.proto", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-collections": "^3.0.0", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/peer-record": "^5.0.0", + "@multiformats/multiaddr": "^12.1.3", + "interface-datastore": "^8.2.0", + "it-all": "^3.0.2", + "mortice": "^3.0.1", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "datastore-core": "^9.0.1", + "delay": "^6.0.0", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "protons": "^7.0.2", + "sinon": "^15.1.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/peer-store/src/errors.ts b/packages/peer-store/src/errors.ts new file mode 100644 index 0000000000..48c52e7e2c --- /dev/null +++ b/packages/peer-store/src/errors.ts @@ -0,0 +1,4 @@ + +export const codes = { + ERR_INVALID_PARAMETERS: 'ERR_INVALID_PARAMETERS' +} diff --git a/packages/peer-store/src/index.ts b/packages/peer-store/src/index.ts new file mode 100644 index 0000000000..20036c4668 --- /dev/null +++ b/packages/peer-store/src/index.ts @@ -0,0 +1,215 @@ +import { logger } from '@libp2p/logger' +import { RecordEnvelope, PeerRecord } from '@libp2p/peer-record' +import all from 'it-all' +import { PersistentStore, type PeerUpdate } from './store.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerStore, Peer, PeerData, PeerQuery } from '@libp2p/interface-peer-store' +import type { EventEmitter } from '@libp2p/interfaces/events' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Datastore } from 'interface-datastore' + +const log = logger('libp2p:peer-store') + +export interface PersistentPeerStoreComponents { + peerId: PeerId + datastore: Datastore + events: EventEmitter +} + +/** + * Return true to allow storing the passed multiaddr for the passed peer + */ +export interface AddressFilter { + (peerId: PeerId, multiaddr: Multiaddr): Promise +} + +export interface PersistentPeerStoreInit { + addressFilter?: AddressFilter +} + +/** + * An implementation of PeerStore that stores data in a Datastore + */ +export class PersistentPeerStore implements PeerStore { + private readonly store: PersistentStore + private readonly events: EventEmitter + private readonly peerId: PeerId + + constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) { + this.events = components.events + this.peerId = components.peerId + this.store = new PersistentStore(components, init) + } + + async forEach (fn: (peer: Peer,) => void, query?: PeerQuery): Promise { + log.trace('forEach await read lock') + const release = await this.store.lock.readLock() + log.trace('forEach got read lock') + + try { + for await (const peer of this.store.all(query)) { + fn(peer) + } + } finally { + log.trace('forEach release read lock') + release() + } + } + + async all (query?: PeerQuery): Promise { + log.trace('all await read lock') + const release = await this.store.lock.readLock() + log.trace('all got read lock') + + try { + return await all(this.store.all(query)) + } finally { + log.trace('all release read lock') + release() + } + } + + async delete (peerId: PeerId): Promise { + log.trace('delete await write lock') + const release = await this.store.lock.writeLock() + log.trace('delete got write lock') + + try { + await this.store.delete(peerId) + } finally { + log.trace('delete release write lock') + release() + } + } + + async has (peerId: PeerId): Promise { + log.trace('has await read lock') + const release = await this.store.lock.readLock() + log.trace('has got read lock') + + try { + return await this.store.has(peerId) + } finally { + log.trace('has release read lock') + release() + } + } + + async get (peerId: PeerId): Promise { + log.trace('get await read lock') + const release = await this.store.lock.readLock() + log.trace('get got read lock') + + try { + return await this.store.load(peerId) + } finally { + log.trace('get release read lock') + release() + } + } + + async save (id: PeerId, data: PeerData): Promise { + log.trace('save await write lock') + const release = await this.store.lock.writeLock() + log.trace('save got write lock') + + try { + const result = await this.store.save(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('save release write lock') + release() + } + } + + async patch (id: PeerId, data: PeerData): Promise { + log.trace('patch await write lock') + const release = await this.store.lock.writeLock() + log.trace('patch got write lock') + + try { + const result = await this.store.patch(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('patch release write lock') + release() + } + } + + async merge (id: PeerId, data: PeerData): Promise { + log.trace('merge await write lock') + const release = await this.store.lock.writeLock() + log.trace('merge got write lock') + + try { + const result = await this.store.merge(id, data) + + this.#emitIfUpdated(id, result) + + return result.peer + } finally { + log.trace('merge release write lock') + release() + } + } + + async consumePeerRecord (buf: Uint8Array, expectedPeer?: PeerId): Promise { + const envelope = await RecordEnvelope.openAndCertify(buf, PeerRecord.DOMAIN) + + if (expectedPeer?.equals(envelope.peerId) === false) { + log('envelope peer id was not the expected peer id - expected: %p received: %p', expectedPeer, envelope.peerId) + return false + } + + const peerRecord = PeerRecord.createFromProtobuf(envelope.payload) + let peer: Peer | undefined + + try { + peer = await this.get(envelope.peerId) + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + // ensure seq is greater than, or equal to, the last received + if (peer?.peerRecordEnvelope != null) { + const storedEnvelope = await RecordEnvelope.createFromProtobuf(peer.peerRecordEnvelope) + const storedRecord = PeerRecord.createFromProtobuf(storedEnvelope.payload) + + if (storedRecord.seqNumber >= peerRecord.seqNumber) { + log('sequence number was lower or equal to existing sequence number - stored: %d received: %d', storedRecord.seqNumber, peerRecord.seqNumber) + return false + } + } + + await this.patch(peerRecord.peerId, { + peerRecordEnvelope: buf, + addresses: peerRecord.multiaddrs.map(multiaddr => ({ + isCertified: true, + multiaddr + })) + }) + + return true + } + + #emitIfUpdated (id: PeerId, result: PeerUpdate): void { + if (!result.updated) { + return + } + + if (this.peerId.equals(id)) { + this.events.safeDispatchEvent('self:peer:update', { detail: result }) + } else { + this.events.safeDispatchEvent('peer:update', { detail: result }) + } + } +} diff --git a/packages/peer-store/src/pb/peer.proto b/packages/peer-store/src/pb/peer.proto new file mode 100644 index 0000000000..01c3be1990 --- /dev/null +++ b/packages/peer-store/src/pb/peer.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +message Peer { + // Multiaddrs we know about + repeated Address addresses = 1; + + // The protocols the peer supports + repeated string protocols = 2; + + // The public key of the peer + optional bytes public_key = 4; + + // The most recently received signed PeerRecord + optional bytes peer_record_envelope = 5; + + // Any peer metadata + map metadata = 6; + + // Any tags the peer has + map tags = 7; +} + +// Address represents a single multiaddr +message Address { + bytes multiaddr = 1; + + // Flag to indicate if the address comes from a certified source + optional bool isCertified = 2; +} + +message Tag { + uint32 value = 1; // tag value 0-100 + optional uint64 expiry = 2; // ms timestamp after which the tag is no longer valid +} diff --git a/packages/peer-store/src/pb/peer.ts b/packages/peer-store/src/pb/peer.ts new file mode 100644 index 0000000000..9ceb63f5ee --- /dev/null +++ b/packages/peer-store/src/pb/peer.ts @@ -0,0 +1,396 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Peer { + addresses: Address[] + protocols: string[] + publicKey?: Uint8Array + peerRecordEnvelope?: Uint8Array + metadata: Map + tags: Map +} + +export namespace Peer { + export interface Peer$metadataEntry { + key: string + value: Uint8Array + } + + export namespace Peer$metadataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key) + } + + if ((obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer$metadataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer$metadataEntry => { + return decodeMessage(buf, Peer$metadataEntry.codec()) + } + } + + export interface Peer$tagsEntry { + key: string + value?: Tag + } + + export namespace Peer$tagsEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key) + } + + if (obj.value != null) { + w.uint32(18) + Tag.codec().encode(obj.value, w) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = Tag.codec().decode(reader, reader.uint32()) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer$tagsEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer$tagsEntry => { + return decodeMessage(buf, Peer$tagsEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.addresses != null) { + for (const value of obj.addresses) { + w.uint32(10) + Address.codec().encode(value, w) + } + } + + if (obj.protocols != null) { + for (const value of obj.protocols) { + w.uint32(18) + w.string(value) + } + } + + if (obj.publicKey != null) { + w.uint32(34) + w.bytes(obj.publicKey) + } + + if (obj.peerRecordEnvelope != null) { + w.uint32(42) + w.bytes(obj.peerRecordEnvelope) + } + + if (obj.metadata != null && obj.metadata.size !== 0) { + for (const [key, value] of obj.metadata.entries()) { + w.uint32(50) + Peer.Peer$metadataEntry.codec().encode({ key, value }, w) + } + } + + if (obj.tags != null && obj.tags.size !== 0) { + for (const [key, value] of obj.tags.entries()) { + w.uint32(58) + Peer.Peer$tagsEntry.codec().encode({ key, value }, w) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + addresses: [], + protocols: [], + metadata: new Map(), + tags: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.addresses.push(Address.codec().decode(reader, reader.uint32())) + break + case 2: + obj.protocols.push(reader.string()) + break + case 4: + obj.publicKey = reader.bytes() + break + case 5: + obj.peerRecordEnvelope = reader.bytes() + break + case 6: { + const entry = Peer.Peer$metadataEntry.codec().decode(reader, reader.uint32()) + obj.metadata.set(entry.key, entry.value) + break + } + case 7: { + const entry = Peer.Peer$tagsEntry.codec().decode(reader, reader.uint32()) + obj.tags.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Peer.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Peer => { + return decodeMessage(buf, Peer.codec()) + } +} + +export interface Address { + multiaddr: Uint8Array + isCertified?: boolean +} + +export namespace Address { + let _codec: Codec
+ + export const codec = (): Codec
=> { + if (_codec == null) { + _codec = message
((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.multiaddr != null && obj.multiaddr.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.multiaddr) + } + + if (obj.isCertified != null) { + w.uint32(16) + w.bool(obj.isCertified) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + multiaddr: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.multiaddr = reader.bytes() + break + case 2: + obj.isCertified = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial
): Uint8Array => { + return encodeMessage(obj, Address.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Address => { + return decodeMessage(buf, Address.codec()) + } +} + +export interface Tag { + value: number + expiry?: bigint +} + +export namespace Tag { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.value != null && obj.value !== 0)) { + w.uint32(8) + w.uint32(obj.value) + } + + if (obj.expiry != null) { + w.uint32(16) + w.uint64(obj.expiry) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + value: 0 + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.value = reader.uint32() + break + case 2: + obj.expiry = reader.uint64() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Tag.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Tag => { + return decodeMessage(buf, Tag.codec()) + } +} diff --git a/packages/peer-store/src/store.ts b/packages/peer-store/src/store.ts new file mode 100644 index 0000000000..98ef673a6d --- /dev/null +++ b/packages/peer-store/src/store.ts @@ -0,0 +1,187 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { PeerMap } from '@libp2p/peer-collections' +import { peerIdFromBytes } from '@libp2p/peer-id' +import mortice, { type Mortice } from 'mortice' +import { base32 } from 'multiformats/bases/base32' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { codes } from './errors.js' +import { Peer as PeerPB } from './pb/peer.js' +import { bytesToPeer } from './utils/bytes-to-peer.js' +import { NAMESPACE_COMMON, peerIdToDatastoreKey } from './utils/peer-id-to-datastore-key.js' +import { toPeerPB } from './utils/to-peer-pb.js' +import type { AddressFilter, PersistentPeerStoreComponents, PersistentPeerStoreInit } from './index.js' +import type { PeerUpdate as PeerUpdateExternal } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Peer, PeerData, PeerQuery } from '@libp2p/interface-peer-store' +import type { Datastore, Key, Query } from 'interface-datastore' + +/** + * Event detail emitted when peer data changes + */ +export interface PeerUpdate extends PeerUpdateExternal { + updated: boolean +} + +function decodePeer (key: Key, value: Uint8Array, cache: PeerMap): Peer { + // /peers/${peer-id-as-libp2p-key-cid-string-in-base-32} + const base32Str = key.toString().split('/')[2] + const buf = base32.decode(base32Str) + const peerId = peerIdFromBytes(buf) + + const cached = cache.get(peerId) + + if (cached != null) { + return cached + } + + const peer = bytesToPeer(peerId, value) + + cache.set(peerId, peer) + + return peer +} + +function mapQuery (query: PeerQuery, cache: PeerMap): Query { + if (query == null) { + return {} + } + + return { + prefix: NAMESPACE_COMMON, + filters: (query.filters ?? []).map(fn => ({ key, value }) => { + return fn(decodePeer(key, value, cache)) + }), + orders: (query.orders ?? []).map(fn => (a, b) => { + return fn(decodePeer(a.key, a.value, cache), decodePeer(b.key, b.value, cache)) + }) + } +} + +export class PersistentStore { + private readonly peerId: PeerId + private readonly datastore: Datastore + public readonly lock: Mortice + private readonly addressFilter?: AddressFilter + + constructor (components: PersistentPeerStoreComponents, init: PersistentPeerStoreInit = {}) { + this.peerId = components.peerId + this.datastore = components.datastore + this.addressFilter = init.addressFilter + this.lock = mortice({ + name: 'peer-store', + singleProcess: true + }) + } + + async has (peerId: PeerId): Promise { + return this.datastore.has(peerIdToDatastoreKey(peerId)) + } + + async delete (peerId: PeerId): Promise { + if (this.peerId.equals(peerId)) { + throw new CodeError('Cannot delete self peer', codes.ERR_INVALID_PARAMETERS) + } + + await this.datastore.delete(peerIdToDatastoreKey(peerId)) + } + + async load (peerId: PeerId): Promise { + const buf = await this.datastore.get(peerIdToDatastoreKey(peerId)) + + return bytesToPeer(peerId, buf) + } + + async save (peerId: PeerId, data: PeerData): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', { + addressFilter: this.addressFilter + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async patch (peerId: PeerId, data: Partial): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'patch', { + addressFilter: this.addressFilter, + existingPeer + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async merge (peerId: PeerId, data: PeerData): Promise { + const { + existingBuf, + existingPeer + } = await this.#findExistingPeer(peerId) + + const peerPb: PeerPB = await toPeerPB(peerId, data, 'merge', { + addressFilter: this.addressFilter, + existingPeer + }) + + return this.#saveIfDifferent(peerId, peerPb, existingBuf, existingPeer) + } + + async * all (query?: PeerQuery): AsyncGenerator { + const peerCache = new PeerMap() + + for await (const { key, value } of this.datastore.query(mapQuery(query ?? {}, peerCache))) { + const peer = decodePeer(key, value, peerCache) + + if (peer.id.equals(this.peerId)) { + // Skip self peer if present + continue + } + + yield peer + } + } + + async #findExistingPeer (peerId: PeerId): Promise<{ existingBuf?: Uint8Array, existingPeer?: Peer }> { + try { + const existingBuf = await this.datastore.get(peerIdToDatastoreKey(peerId)) + const existingPeer = bytesToPeer(peerId, existingBuf) + + return { + existingBuf, + existingPeer + } + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + } + + return {} + } + + async #saveIfDifferent (peerId: PeerId, peer: PeerPB, existingBuf?: Uint8Array, existingPeer?: Peer): Promise { + const buf = PeerPB.encode(peer) + + if (existingBuf != null && uint8ArrayEquals(buf, existingBuf)) { + return { + peer: bytesToPeer(peerId, buf), + previous: existingPeer, + updated: false + } + } + + await this.datastore.put(peerIdToDatastoreKey(peerId), buf) + + return { + peer: bytesToPeer(peerId, buf), + previous: existingPeer, + updated: true + } + } +} diff --git a/packages/peer-store/src/utils/bytes-to-peer.ts b/packages/peer-store/src/utils/bytes-to-peer.ts new file mode 100644 index 0000000000..bd3c7c0d83 --- /dev/null +++ b/packages/peer-store/src/utils/bytes-to-peer.ts @@ -0,0 +1,43 @@ +import { peerIdFromPeerId } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' +import { Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Peer, Tag } from '@libp2p/interface-peer-store' + +export function bytesToPeer (peerId: PeerId, buf: Uint8Array): Peer { + const peer = PeerPB.decode(buf) + + if (peer.publicKey != null && peerId.publicKey == null) { + peerId = peerIdFromPeerId({ + ...peerId, + publicKey: peerId.publicKey + }) + } + + const tags = new Map() + + // remove any expired tags + const now = BigInt(Date.now()) + + for (const [key, tag] of peer.tags.entries()) { + if (tag.expiry != null && tag.expiry < now) { + continue + } + + tags.set(key, tag) + } + + return { + ...peer, + id: peerId, + addresses: peer.addresses.map(({ multiaddr: ma, isCertified }) => { + return { + multiaddr: multiaddr(ma), + isCertified: isCertified ?? false + } + }), + metadata: peer.metadata, + peerRecordEnvelope: peer.peerRecordEnvelope ?? undefined, + tags + } +} diff --git a/packages/peer-store/src/utils/dedupe-addresses.ts b/packages/peer-store/src/utils/dedupe-addresses.ts new file mode 100644 index 0000000000..ec8e6f6d9d --- /dev/null +++ b/packages/peer-store/src/utils/dedupe-addresses.ts @@ -0,0 +1,51 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { isMultiaddr, multiaddr } from '@multiformats/multiaddr' +import { codes } from '../errors.js' +import type { AddressFilter } from '../index.js' +import type { Address as AddressPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Address } from '@libp2p/interface-peer-store' + +export async function dedupeFilterAndSortAddresses (peerId: PeerId, filter: AddressFilter, addresses: Array
): Promise { + const addressMap = new Map() + + for (const addr of addresses) { + if (addr == null) { + continue + } + + if (addr.multiaddr instanceof Uint8Array) { + addr.multiaddr = multiaddr(addr.multiaddr) + } + + if (!isMultiaddr(addr.multiaddr)) { + throw new CodeError('Multiaddr was invalid', codes.ERR_INVALID_PARAMETERS) + } + + if (!(await filter(peerId, addr.multiaddr))) { + continue + } + + const isCertified = addr.isCertified ?? false + const maStr = addr.multiaddr.toString() + const existingAddr = addressMap.get(maStr) + + if (existingAddr != null) { + addr.isCertified = existingAddr.isCertified || isCertified + } else { + addressMap.set(maStr, { + multiaddr: addr.multiaddr, + isCertified + }) + } + } + + return [...addressMap.values()] + .sort((a, b) => { + return a.multiaddr.toString().localeCompare(b.multiaddr.toString()) + }) + .map(({ isCertified, multiaddr }) => ({ + isCertified, + multiaddr: multiaddr.bytes + })) +} diff --git a/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts b/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts new file mode 100644 index 0000000000..606aa63a5e --- /dev/null +++ b/packages/peer-store/src/utils/peer-data-to-datastore-peer.ts @@ -0,0 +1,116 @@ + +import { CodeError } from '@libp2p/interfaces/errors' +import { isMultiaddr } from '@multiformats/multiaddr' +import { equals as uint8arrayEquals } from 'uint8arrays/equals' +import { codes } from '../errors.js' +import type { Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +export function toDatastorePeer (peerId: PeerId, data: PeerData): PeerPB { + if (data == null) { + throw new CodeError('Invalid PeerData', codes.ERR_INVALID_PARAMETERS) + } + + if (data.publicKey != null && peerId.publicKey != null && !uint8arrayEquals(data.publicKey, peerId.publicKey)) { + throw new CodeError('publicKey bytes do not match peer id publicKey bytes', codes.ERR_INVALID_PARAMETERS) + } + + // merge addresses and multiaddrs, and dedupe + const addressSet = new Set() + + const output: PeerPB = { + addresses: (data.addresses ?? []) + .concat((data.multiaddrs ?? []).map(multiaddr => ({ multiaddr, isCertified: false }))) + .filter(address => { + if (!isMultiaddr(address.multiaddr)) { + throw new CodeError('Invalid mulitaddr', codes.ERR_INVALID_PARAMETERS) + } + + if (addressSet.has(address.multiaddr.toString())) { + return false + } + + addressSet.add(address.multiaddr.toString()) + return true + }) + .sort((a, b) => { + return a.multiaddr.toString().localeCompare(b.multiaddr.toString()) + }) + .map(({ multiaddr, isCertified }) => ({ + multiaddr: multiaddr.bytes, + isCertified + })), + protocols: (data.protocols ?? []).sort(), + metadata: new Map(), + tags: new Map(), + publicKey: data.publicKey, + peerRecordEnvelope: data.peerRecordEnvelope + } + + // remove invalid metadata + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? data.metadata.entries() : Object.entries(data.metadata) + + for (const [key, value] of metadataEntries) { + if (typeof key !== 'string') { + throw new CodeError('Peer metadata keys must be strings', codes.ERR_INVALID_PARAMETERS) + } + + if (value == null) { + continue + } + + if (!(value instanceof Uint8Array)) { + throw new CodeError('Peer metadata values must be Uint8Arrays', codes.ERR_INVALID_PARAMETERS) + } + + output.metadata.set(key, value) + } + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? data.tags.entries() : Object.entries(data.tags) + + for (const [key, value] of tagsEntries) { + if (typeof key !== 'string') { + throw new CodeError('Peer tag keys must be strings', codes.ERR_INVALID_PARAMETERS) + } + + if (value == null) { + continue + } + + const tag = { + name: key, + ttl: value.ttl, + value: value.value ?? 0 + } + + if (tag.value < 0 || tag.value > 100) { + throw new CodeError('Tag value must be between 0-100', codes.ERR_INVALID_PARAMETERS) + } + + if (parseInt(`${tag.value}`, 10) !== tag.value) { + throw new CodeError('Tag value must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.ttl != null) { + if (tag.ttl < 0) { + throw new CodeError('Tag ttl must be between greater than 0', codes.ERR_INVALID_PARAMETERS) + } + + if (parseInt(`${tag.ttl}`, 10) !== tag.ttl) { + throw new CodeError('Tag ttl must be an integer', codes.ERR_INVALID_PARAMETERS) + } + } + + output.tags.set(tag.name, { + value: tag.value, + expiry: tag.ttl == null ? undefined : BigInt(Date.now() + tag.ttl) + }) + } + } + + return output +} diff --git a/packages/peer-store/src/utils/peer-id-to-datastore-key.ts b/packages/peer-store/src/utils/peer-id-to-datastore-key.ts new file mode 100644 index 0000000000..4ff988ac91 --- /dev/null +++ b/packages/peer-store/src/utils/peer-id-to-datastore-key.ts @@ -0,0 +1,15 @@ +import { isPeerId, type PeerId } from '@libp2p/interface-peer-id' +import { CodeError } from '@libp2p/interfaces/errors' +import { Key } from 'interface-datastore/key' +import { codes } from '../errors.js' + +export const NAMESPACE_COMMON = '/peers/' + +export function peerIdToDatastoreKey (peerId: PeerId): Key { + if (!isPeerId(peerId) || peerId.type == null) { + throw new CodeError('Invalid PeerId', codes.ERR_INVALID_PARAMETERS) + } + + const b32key = peerId.toCID().toString() + return new Key(`${NAMESPACE_COMMON}${b32key}`) +} diff --git a/packages/peer-store/src/utils/to-peer-pb.ts b/packages/peer-store/src/utils/to-peer-pb.ts new file mode 100644 index 0000000000..1ba6fa29dd --- /dev/null +++ b/packages/peer-store/src/utils/to-peer-pb.ts @@ -0,0 +1,237 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { equals as uint8arrayEquals } from 'uint8arrays/equals' +import { codes } from '../errors.js' +import { dedupeFilterAndSortAddresses } from './dedupe-addresses.js' +import type { AddressFilter } from '../index.js' +import type { Tag, Peer as PeerPB } from '../pb/peer.js' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Address, Peer, PeerData, TagOptions } from '@libp2p/interface-peer-store' + +export interface ToPBPeerOptions { + addressFilter?: AddressFilter + existingPeer?: Peer +} + +export async function toPeerPB (peerId: PeerId, data: Partial, strategy: 'merge' | 'patch', options: ToPBPeerOptions): Promise { + if (data == null) { + throw new CodeError('Invalid PeerData', codes.ERR_INVALID_PARAMETERS) + } + + if (data.publicKey != null && peerId.publicKey != null && !uint8arrayEquals(data.publicKey, peerId.publicKey)) { + throw new CodeError('publicKey bytes do not match peer id publicKey bytes', codes.ERR_INVALID_PARAMETERS) + } + + const existingPeer = options.existingPeer + + if (existingPeer != null && !peerId.equals(existingPeer.id)) { + throw new CodeError('peer id did not match existing peer id', codes.ERR_INVALID_PARAMETERS) + } + + let addresses: Address[] = existingPeer?.addresses ?? [] + let protocols = new Set(existingPeer?.protocols ?? []) + let metadata: Map = existingPeer?.metadata ?? new Map() + let tags: Map = existingPeer?.tags ?? new Map() + let peerRecordEnvelope: Uint8Array | undefined = existingPeer?.peerRecordEnvelope + + // when patching, we replace the original fields with passed values + if (strategy === 'patch') { + if (data.multiaddrs != null || data.addresses != null) { + addresses = [] + + if (data.multiaddrs != null) { + addresses.push(...data.multiaddrs.map(multiaddr => ({ + isCertified: false, + multiaddr + }))) + } + + if (data.addresses != null) { + addresses.push(...data.addresses) + } + } + + if (data.protocols != null) { + protocols = new Set(data.protocols) + } + + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? [...data.metadata.entries()] : Object.entries(data.metadata) + + metadata = createSortedMap(metadataEntries, { + validate: validateMetadata + }) + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? [...data.tags.entries()] : Object.entries(data.tags) + + tags = createSortedMap(tagsEntries, { + validate: validateTag, + map: mapTag + }) + } + + if (data.peerRecordEnvelope != null) { + peerRecordEnvelope = data.peerRecordEnvelope + } + } + + // when merging, we join the original fields with passed values + if (strategy === 'merge') { + if (data.multiaddrs != null) { + addresses.push(...data.multiaddrs.map(multiaddr => ({ + isCertified: false, + multiaddr + }))) + } + + if (data.addresses != null) { + addresses.push(...data.addresses) + } + + if (data.protocols != null) { + protocols = new Set([...protocols, ...data.protocols]) + } + + if (data.metadata != null) { + const metadataEntries = data.metadata instanceof Map ? [...data.metadata.entries()] : Object.entries(data.metadata) + + for (const [key, value] of metadataEntries) { + if (value == null) { + metadata.delete(key) + } else { + metadata.set(key, value) + } + } + + metadata = createSortedMap([...metadata.entries()], { + validate: validateMetadata + }) + } + + if (data.tags != null) { + const tagsEntries = data.tags instanceof Map ? [...data.tags.entries()] : Object.entries(data.tags) + const mergedTags = new Map(tags) + + for (const [key, value] of tagsEntries) { + if (value == null) { + mergedTags.delete(key) + } else { + mergedTags.set(key, value) + } + } + + tags = createSortedMap([...mergedTags.entries()], { + validate: validateTag, + map: mapTag + }) + } + + if (data.peerRecordEnvelope != null) { + peerRecordEnvelope = data.peerRecordEnvelope + } + } + + const output: PeerPB = { + addresses: await dedupeFilterAndSortAddresses(peerId, options.addressFilter ?? (async () => true), addresses), + protocols: [...protocols.values()].sort((a, b) => { + return a.localeCompare(b) + }), + metadata, + tags, + + publicKey: existingPeer?.id.publicKey ?? data.publicKey ?? peerId.publicKey, + peerRecordEnvelope + } + + // Ed25519 and secp256k1 have their public key embedded in them so no need to duplicate it + if (peerId.type !== 'RSA') { + delete output.publicKey + } + + return output +} + +interface CreateSortedMapOptions { + validate: (key: string, value: V) => void + map?: (key: string, value: V) => R +} + +/** + * In JS maps are ordered by insertion order so create a new map with the + * keys inserted in alphabetical order. + */ +function createSortedMap (entries: Array<[string, V | undefined]>, options: CreateSortedMapOptions): Map { + const output = new Map() + + for (const [key, value] of entries) { + if (value == null) { + continue + } + + options.validate(key, value) + } + + for (const [key, value] of entries.sort(([a], [b]) => { + return a.localeCompare(b) + })) { + if (value != null) { + output.set(key, options.map?.(key, value) ?? value) + } + } + + return output +} + +function validateMetadata (key: string, value: Uint8Array): void { + if (typeof key !== 'string') { + throw new CodeError('Metadata key must be a string', codes.ERR_INVALID_PARAMETERS) + } + + if (!(value instanceof Uint8Array)) { + throw new CodeError('Metadata value must be a Uint8Array', codes.ERR_INVALID_PARAMETERS) + } +} + +function validateTag (key: string, tag: TagOptions): void { + if (typeof key !== 'string') { + throw new CodeError('Tag name must be a string', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.value != null) { + if (parseInt(`${tag.value}`, 10) !== tag.value) { + throw new CodeError('Tag value must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.value < 0 || tag.value > 100) { + throw new CodeError('Tag value must be between 0-100', codes.ERR_INVALID_PARAMETERS) + } + } + + if (tag.ttl != null) { + if (parseInt(`${tag.ttl}`, 10) !== tag.ttl) { + throw new CodeError('Tag ttl must be an integer', codes.ERR_INVALID_PARAMETERS) + } + + if (tag.ttl < 0) { + throw new CodeError('Tag ttl must be between greater than 0', codes.ERR_INVALID_PARAMETERS) + } + } +} + +function mapTag (key: string, tag: any): Tag { + let expiry: bigint | undefined + + if (tag.expiry != null) { + expiry = tag.expiry + } + + if (tag.ttl != null) { + expiry = BigInt(Date.now() + Number(tag.ttl)) + } + + return { + value: tag.value ?? 0, + expiry + } +} diff --git a/packages/peer-store/test/index.spec.ts b/packages/peer-store/test/index.spec.ts new file mode 100644 index 0000000000..efd9275f58 --- /dev/null +++ b/packages/peer-store/test/index.spec.ts @@ -0,0 +1,287 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { RecordEnvelope, PeerRecord } from '@libp2p/peer-record' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import delay from 'delay' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') + +describe('PersistentPeerStore', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('has an empty map of peers', async () => { + const peers = await peerStore.all() + expect(peers.length).to.equal(0) + }) + + describe('has', () => { + it('has peer data', async () => { + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + await expect(peerStore.has(otherPeerId)).to.eventually.be.true() + }) + }) + + describe('delete', () => { + it('deletes peer data', async () => { + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + await expect(peerStore.has(otherPeerId)).to.eventually.be.true() + await peerStore.delete(otherPeerId) + await expect(peerStore.has(otherPeerId)).to.eventually.be.false() + }) + + it('does not allow deleting the self peer', async () => { + await expect(peerStore.has(peerId)).to.eventually.be.false() + await peerStore.save(peerId, { + multiaddrs: [ + addr1 + ] + }) + + await expect(peerStore.delete(peerId)).to.eventually.be.rejected() + .with.property('code', 'ERR_INVALID_PARAMETERS') + }) + }) + + describe('tags', () => { + it('tags a peer', async () => { + const name = 'a-tag' + const peer = await peerStore.save(otherPeerId, { + tags: { + [name]: {} + } + }) + + expect(peer).to.have.property('tags') + .that.deep.equals(new Map([[name, { value: 0 }]]), 'Peer did not contain tag') + }) + + it('tags a peer with a value', async () => { + const name = 'a-tag' + const value = 50 + const peer = await peerStore.save(peerId, { + tags: { + [name]: { value } + } + }) + + expect(peer).to.have.property('tags') + .that.deep.equals(new Map([[name, { value }]]), 'Peer did not contain tag with a value') + }) + + it('tags a peer with a valid value', async () => { + const name = 'a-tag' + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: -1 } + } + }), 'PeerStore contain tag for peer where value was too small') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: 101 } + } + }), 'PeerStore contain tag for peer where value was too large') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + + await expect(peerStore.save(peerId, { + tags: { + [name]: { value: 5.5 } + } + }), 'PeerStore contain tag for peer where value was not an integer') + .to.eventually.be.rejected().with.property('code', 'ERR_INVALID_PARAMETERS') + }) + + it('tags a peer with an expiring value', async () => { + const name = 'a-tag' + const value = 50 + const peer = await peerStore.save(peerId, { + tags: { + [name]: { + value, + ttl: 50 + } + } + }) + + expect(peer).to.have.property('tags') + .that.has.key(name) + + await delay(100) + + const updatedPeer = await peerStore.get(peerId) + + expect(updatedPeer).to.have.property('tags') + .that.does.not.have.key(name) + }) + + it('untags a peer', async () => { + const name = 'a-tag' + const peer = await peerStore.save(peerId, { + tags: { + [name]: {} + } + }) + + expect(peer).to.have.property('tags') + .that.has.key(name) + + const updatedPeer = await peerStore.patch(peerId, { + tags: {} + }) + + expect(updatedPeer).to.have.property('tags') + .that.does.not.have.key(name) + }) + }) + + describe('peer record', () => { + it('consumes a peer record, creating a peer', async () => { + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ] + }) + const signedPeerRecord = await RecordEnvelope.seal(peerRecord, peerId) + + await expect(peerStore.has(peerId)).to.eventually.be.false() + await peerStore.consumePeerRecord(signedPeerRecord.marshal()) + await expect(peerStore.has(peerId)).to.eventually.be.true() + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/1234' + }]) + }) + + it('overwrites old addresses with those from a peer record', async () => { + await peerStore.patch(peerId, { + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ] + }) + + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ] + }) + const signedPeerRecord = await RecordEnvelope.seal(peerRecord, peerId) + + await peerStore.consumePeerRecord(signedPeerRecord.marshal()) + + await expect(peerStore.has(peerId)).to.eventually.be.true() + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/4567' + }]) + }) + + it('ignores older peer records', async () => { + const oldSignedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/1234') + ], + seqNumber: 1n + }), peerId) + + const newSignedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ], + seqNumber: 2n + }), peerId) + + await expect(peerStore.consumePeerRecord(newSignedPeerRecord.marshal())).to.eventually.equal(true) + await expect(peerStore.consumePeerRecord(oldSignedPeerRecord.marshal())).to.eventually.equal(false) + + const peer = await peerStore.get(peerId) + expect(peer.addresses.map(({ multiaddr, isCertified }) => ({ + isCertified, + multiaddr: multiaddr.toString() + }))).to.deep.equal([{ + isCertified: true, + multiaddr: '/ip4/127.0.0.1/tcp/4567' + }]) + }) + + it('ignores record for unexpected peer', async () => { + const signedPeerRecord = await RecordEnvelope.seal(new PeerRecord({ + peerId, + multiaddrs: [ + multiaddr('/ip4/127.0.0.1/tcp/4567') + ] + }), peerId) + + await expect(peerStore.has(peerId)).to.eventually.be.false() + await expect(peerStore.consumePeerRecord(signedPeerRecord.marshal(), otherPeerId)).to.eventually.equal(false) + await expect(peerStore.has(peerId)).to.eventually.be.false() + }) + + it('allows queries', async () => { + await peerStore.save(otherPeerId, { + multiaddrs: [ + addr1 + ] + }) + + const allPeers = await peerStore.all({ + filters: [ + () => true + ] + }) + + expect(allPeers).to.not.be.empty() + + const noPeers = await peerStore.all({ + filters: [ + () => false + ] + }) + + expect(noPeers).to.be.empty() + }) + }) +}) diff --git a/packages/peer-store/test/merge.spec.ts b/packages/peer-store/test/merge.spec.ts new file mode 100644 index 0000000000..b16a56dc5a --- /dev/null +++ b/packages/peer-store/test/merge.spec.ts @@ -0,0 +1,247 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { pEvent } from 'p-event' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') +const addr3 = multiaddr('/ip4/127.0.0.1/tcp/8002') + +describe('merge', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('emits peer:update event on merge', async () => { + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.merge(otherPeerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('emits self:peer:update event on merge for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.merge(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('merges multiaddrs', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + multiaddrs: [ + addr3 + ] + }) + + expect(updated).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr3, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }]) + + // other fields should be untouched + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges metadata', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]), + baz: Uint8Array.from([6, 7, 8]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + metadata: { + bar: Uint8Array.from([3, 4, 5]), + baz: undefined + } + }) + + expect(updated).to.have.property('metadata').that.deep.equals( + new Map([ + ['foo', Uint8Array.from([0, 1, 2])], + ['bar', Uint8Array.from([3, 4, 5])] + ]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges tags', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 }, + tag3: { value: 50 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.patch(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + tags: { + tag2: { value: 20 }, + tag3: undefined + } + }) + + expect(updated).to.have.property('tags').that.deep.equals( + new Map([ + ['tag1', { value: 10 }], + ['tag2', { value: 20 }] + ]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges protocols', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + protocols: [ + '/bar/foo' + ] + }) + + expect(updated).to.have.property('protocols').that.deep.equals([ + '/bar/foo', + '/foo/bar' + ]) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('merges peer record envelope', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.merge(otherPeerId, { + peerRecordEnvelope: Uint8Array.from([6, 7, 8]) + }) + + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals( + Uint8Array.from([6, 7, 8]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + }) +}) diff --git a/packages/peer-store/test/patch.spec.ts b/packages/peer-store/test/patch.spec.ts new file mode 100644 index 0000000000..3d64bf4d02 --- /dev/null +++ b/packages/peer-store/test/patch.spec.ts @@ -0,0 +1,231 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import { pEvent } from 'p-event' +import { PersistentPeerStore } from '../src/index.js' +import type { Libp2pEvents } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') +const addr3 = multiaddr('/ip4/127.0.0.1/tcp/8002') + +describe('patch', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('emits peer:update event on patch', async () => { + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.patch(otherPeerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('emits self:peer:update event on patch for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.patch(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('replaces multiaddrs', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + multiaddrs: [ + addr3 + ] + }) + + // upated field + expect(updated).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr3, + isCertified: false + }]) + + // other fields should be untouched + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces metadata', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + metadata: { + bar: Uint8Array.from([3, 4, 5]) + } + }) + + expect(updated).to.have.property('metadata').that.deep.equals( + new Map([['bar', Uint8Array.from([3, 4, 5])]]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces tags', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + tags: { + tag2: { value: 20 } + } + }) + + expect(updated).to.have.property('tags').that.deep.equals( + new Map([['tag2', { value: 20 }]]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces protocols', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + protocols: [ + '/bar/foo' + ] + }) + + expect(updated).to.have.property('protocols').that.deep.equals([ + '/bar/foo' + ]) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals(original.peerRecordEnvelope) + }) + + it('replaces peer record envelope', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const original = await peerStore.save(otherPeerId, peer) + const updated = await peerStore.patch(otherPeerId, { + peerRecordEnvelope: Uint8Array.from([6, 7, 8]) + }) + + expect(updated).to.have.property('peerRecordEnvelope').that.deep.equals( + Uint8Array.from([6, 7, 8]) + ) + + // other fields should be untouched + expect(updated).to.have.property('addresses').that.deep.equals(original.addresses) + expect(updated).to.have.property('metadata').that.deep.equals(original.metadata) + expect(updated).to.have.property('tags').that.deep.equals(original.tags) + expect(updated).to.have.property('protocols').that.deep.equals(original.protocols) + }) +}) diff --git a/packages/peer-store/test/save.spec.ts b/packages/peer-store/test/save.spec.ts new file mode 100644 index 0000000000..222ee2ea66 --- /dev/null +++ b/packages/peer-store/test/save.spec.ts @@ -0,0 +1,252 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import { EventEmitter } from '@libp2p/interfaces/events' +import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core/memory' +import pDefer from 'p-defer' +import { pEvent } from 'p-event' +import sinon from 'sinon' +import { codes } from '../src/errors.js' +import { PersistentPeerStore } from '../src/index.js' +import { Peer as PeerPB } from '../src/pb/peer.js' +import type { Libp2pEvents, PeerUpdate } from '@libp2p/interface-libp2p' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { PeerData } from '@libp2p/interface-peer-store' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') + +describe('save', () => { + let peerId: PeerId + let otherPeerId: PeerId + let peerStore: PersistentPeerStore + let events: EventEmitter + + beforeEach(async () => { + peerId = await createEd25519PeerId() + otherPeerId = await createEd25519PeerId() + events = new EventEmitter() + peerStore = new PersistentPeerStore({ peerId, events, datastore: new MemoryDatastore() }) + }) + + it('throws invalid parameters error if invalid PeerId is provided', async () => { + // @ts-expect-error invalid input + await expect(peerStore.save('invalid peerId')) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('throws invalid parameters error if no peer data provided', async () => { + // @ts-expect-error invalid input + await expect(peerStore.save(peerId)) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('throws invalid parameters error if invalid multiaddrs are provided', async () => { + await expect(peerStore.save(peerId, { + // @ts-expect-error invalid input + addresses: ['invalid multiaddr'] + })) + .to.eventually.be.rejected.with.property('code', codes.ERR_INVALID_PARAMETERS) + }) + + it('replaces the stored content by default and emit change event', async () => { + const supportedMultiaddrs = [addr1, addr2] + const eventPromise = pEvent(events, 'peer:update') + + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + const event = await eventPromise as CustomEvent + + const { peer, previous } = event.detail + + expect(peer.addresses).to.deep.equal( + supportedMultiaddrs.map((multiaddr) => ({ + isCertified: false, + multiaddr + })) + ) + expect(previous).to.be.undefined() + }) + + it('emits on set if not storing the exact same content', async () => { + const defer = pDefer() + + const supportedMultiaddrsA = [addr1, addr2] + const supportedMultiaddrsB = [addr2] + + let changeCounter = 0 + events.addEventListener('peer:update', () => { + changeCounter++ + if (changeCounter > 1) { + defer.resolve() + } + }) + + // set 1 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrsA + }) + + // set 2 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrsB + }) + + const peer = await peerStore.get(otherPeerId) + const multiaddrs = peer.addresses.map((mi) => mi.multiaddr) + expect(multiaddrs).to.have.deep.members(supportedMultiaddrsB) + + await defer.promise + }) + + it('emits self event on save for self peer', async () => { + const eventPromise = pEvent(events, 'self:peer:update') + + await peerStore.save(peerId, { + multiaddrs: [addr1, addr2] + }) + + await eventPromise + }) + + it('does not emit on set if it is storing the exact same content', async () => { + const defer = pDefer() + + const supportedMultiaddrs = [addr1, addr2] + + let changeCounter = 0 + events.addEventListener('peer:update', () => { + changeCounter++ + if (changeCounter > 1) { + defer.reject(new Error('Saved identical data twice')) + } + }) + + // set 1 + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + // set 2 (same content) + await peerStore.save(otherPeerId, { + multiaddrs: supportedMultiaddrs + }) + + // Wait 50ms for incorrect second event + setTimeout(() => { + defer.resolve() + }, 50) + + await defer.promise + }) + + it('should not set public key when key does not match', async () => { + const edKey = await createEd25519PeerId() + + if (peerId.publicKey == null) { + throw new Error('Public key was missing') + } + + await expect(peerStore.save(edKey, { + publicKey: peerId.publicKey + })).to.eventually.be.rejectedWith(/bytes do not match/) + }) + + it('should not store a public key if already stored', async () => { + // @ts-expect-error private fields + const spy = sinon.spy(peerStore.store.datastore, 'put') + + if (otherPeerId.publicKey == null) { + throw new Error('Public key was missing') + } + + // Set PeerId + await peerStore.save(otherPeerId, { + publicKey: otherPeerId.publicKey + }) + await peerStore.save(otherPeerId, { + publicKey: otherPeerId.publicKey + }) + + expect(spy).to.have.property('callCount', 1) + }) + + it('should not store a public key if part of peer id', async () => { + // @ts-expect-error private fields + const spy = sinon.spy(peerStore.store.datastore, 'put') + + if (otherPeerId.publicKey == null) { + throw new Error('Public key was missing') + } + + const edKey = await createEd25519PeerId() + await peerStore.save(edKey, { + publicKey: edKey.publicKey + }) + + const dbPeerEdKey = PeerPB.decode(spy.getCall(0).args[1]) + expect(dbPeerEdKey).to.not.have.property('publicKey') + + const secpKey = await createSecp256k1PeerId() + await peerStore.save(secpKey, { + publicKey: secpKey.publicKey + }) + + const dbPeerSecpKey = PeerPB.decode(spy.getCall(1).args[1]) + expect(dbPeerSecpKey).to.not.have.property('publicKey') + + const rsaKey = await createRSAPeerId() + await peerStore.save(rsaKey, { + publicKey: rsaKey.publicKey + }) + + const dbPeerRsaKey = PeerPB.decode(spy.getCall(2).args[1]) + expect(dbPeerRsaKey).to.have.property('publicKey').that.equalBytes(rsaKey.publicKey) + }) + + it('saves all of the fields', async () => { + const peer: PeerData = { + multiaddrs: [ + addr1, + addr2 + ], + metadata: { + foo: Uint8Array.from([0, 1, 2]) + }, + tags: { + tag1: { value: 10 } + }, + protocols: [ + '/foo/bar' + ], + peerRecordEnvelope: Uint8Array.from([3, 4, 5]) + } + + const saved = await peerStore.save(otherPeerId, peer) + + expect(saved).to.have.property('addresses').that.deep.equals([{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }]) + expect(saved).to.have.property('metadata').that.deep.equals( + new Map([ + ['foo', Uint8Array.from([0, 1, 2])] + ]) + ) + expect(saved).to.have.property('tags').that.deep.equals( + new Map([ + ['tag1', { value: 10 }] + ]) + ) + expect(saved).to.have.property('protocols').that.deep.equals(peer.protocols) + expect(saved).to.have.property('peerRecordEnvelope').that.deep.equals(peer.peerRecordEnvelope) + }) +}) diff --git a/packages/peer-store/test/utils/dedupe-addresses.spec.ts b/packages/peer-store/test/utils/dedupe-addresses.spec.ts new file mode 100644 index 0000000000..6fa5001007 --- /dev/null +++ b/packages/peer-store/test/utils/dedupe-addresses.spec.ts @@ -0,0 +1,79 @@ +/* eslint-env mocha */ + +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { dedupeFilterAndSortAddresses } from '../../src/utils/dedupe-addresses.js' +import type { PeerId } from '@libp2p/interface-peer-id' + +const addr1 = multiaddr('/ip4/127.0.0.1/tcp/8000') +const addr2 = multiaddr('/ip4/20.0.0.1/tcp/8001') + +describe('dedupe-addresses', () => { + let peerId: PeerId + + beforeEach(async () => { + peerId = await createEd25519PeerId() + }) + + it('should dedupe addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr2, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: false + }, { + multiaddr: addr2.bytes, + isCertified: false + }]) + }) + + it('should sort addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr2, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: false + }, { + multiaddr: addr2.bytes, + isCertified: false + }]) + }) + + it('should retain isCertified when deduping addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => true, [{ + multiaddr: addr1, + isCertified: true + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([{ + multiaddr: addr1.bytes, + isCertified: true + }]) + }) + + it('should filter addresses', async () => { + expect(await dedupeFilterAndSortAddresses(peerId, async () => false, [{ + multiaddr: addr1, + isCertified: true + }, { + multiaddr: addr1, + isCertified: false + }])).to.deep.equal([]) + }) +}) diff --git a/packages/peer-store/tsconfig.json b/packages/peer-store/tsconfig.json new file mode 100644 index 0000000000..7b195f0cb6 --- /dev/null +++ b/packages/peer-store/tsconfig.json @@ -0,0 +1,42 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "exclude": [ + "src/pb/peer.js" + ], + "references": [ + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../peer-collections" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../peer-record" + } + ] +} diff --git a/packages/record/CHANGELOG.md b/packages/record/CHANGELOG.md new file mode 100644 index 0000000000..4d904e156d --- /dev/null +++ b/packages/record/CHANGELOG.md @@ -0,0 +1,323 @@ +## [3.0.4](https://github.com/libp2p/js-libp2p-record/compare/v3.0.3...v3.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([afec2c9](https://github.com/libp2p/js-libp2p-record/commit/afec2c9d1685707c9cff82342099839abb6976da)) +* Update .github/workflows/stale.yml [skip ci] ([2d12a78](https://github.com/libp2p/js-libp2p-record/commit/2d12a789581d65181d463fa9b908280b14fc2070)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#95](https://github.com/libp2p/js-libp2p-record/issues/95)) ([30e0fb5](https://github.com/libp2p/js-libp2p-record/commit/30e0fb5dfb193e3289f1aec6c29a8d194fd9aa92)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-record/compare/v3.0.2...v3.0.3) (2023-04-04) + + +### Bug Fixes + +* correction package.json exports types path ([#87](https://github.com/libp2p/js-libp2p-record/issues/87)) ([c1e9a6d](https://github.com/libp2p/js-libp2p-record/commit/c1e9a6d402a971b3ef66484c151b9dc4627fea1b)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-record/compare/v3.0.1...v3.0.2) (2023-03-10) + + +### Dependencies + +* bump protons-runtime from 4.0.2 to 5.0.0 ([#73](https://github.com/libp2p/js-libp2p-record/issues/73)) ([4b1b67b](https://github.com/libp2p/js-libp2p-record/commit/4b1b67bac77cb13a01ce330a4a93eb6c3dc042a5)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-record/compare/v3.0.0...v3.0.1) (2023-03-10) + + +### Trivial Changes + +* replace err-code with CodeError ([#71](https://github.com/libp2p/js-libp2p-record/issues/71)) ([a843ae4](https://github.com/libp2p/js-libp2p-record/commit/a843ae4fbdc8c262a55f4ed87f989770d3783c5a)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([3982918](https://github.com/libp2p/js-libp2p-record/commit/3982918a51c25bf1f803702073eaf17cc5feee9b)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([79984c0](https://github.com/libp2p/js-libp2p-record/commit/79984c0ce651cb0bff634b3b8df630cf807baa59)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7ccccc7](https://github.com/libp2p/js-libp2p-record/commit/7ccccc73fb1aad74cd4b696acd8dc912199afec3)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#84](https://github.com/libp2p/js-libp2p-record/issues/84)) ([4cc5935](https://github.com/libp2p/js-libp2p-record/commit/4cc593576ccda281950f30aa6c8e769baa1aeee6)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-record/compare/v2.0.4...v3.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update multiformats to 11.x.x (#70) + +### Bug Fixes + +* update multiformats to 11.x.x ([#70](https://github.com/libp2p/js-libp2p-record/issues/70)) ([594fc41](https://github.com/libp2p/js-libp2p-record/commit/594fc4171ec20f4fc1fbc36c99c61eed06aeab25)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-record/compare/v2.0.3...v2.0.4) (2022-12-16) + + +### Documentation + +* publish api docs ([#68](https://github.com/libp2p/js-libp2p-record/issues/68)) ([5a3dd41](https://github.com/libp2p/js-libp2p-record/commit/5a3dd419f13b67e27c19f3b23252937d80fc8b93)) + +## [2.0.3](https://github.com/libp2p/js-libp2p-record/compare/v2.0.2...v2.0.3) (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([92044b6](https://github.com/libp2p/js-libp2p-record/commit/92044b646180e2b2d0f495d26cc54c184ad6fb7b)) + + +### Dependencies + +* bump uint8arrays, protons and multiformats ([#63](https://github.com/libp2p/js-libp2p-record/issues/63)) ([9106a6a](https://github.com/libp2p/js-libp2p-record/commit/9106a6abdc71a2c94359759bbc2f61213e9a6a0b)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-record/compare/v2.0.1...v2.0.2) (2022-08-11) + + +### Dependencies + +* update protons to 5.1.0 ([#58](https://github.com/libp2p/js-libp2p-record/issues/58)) ([24d4047](https://github.com/libp2p/js-libp2p-record/commit/24d404733aa89c28ae71abfcb51ee20b6af919cf)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-record/compare/v2.0.0...v2.0.1) (2022-08-03) + + +### Trivial Changes + +* update project ([#53](https://github.com/libp2p/js-libp2p-record/issues/53)) ([1927144](https://github.com/libp2p/js-libp2p-record/commit/1927144ce346592f513e2f29e0b4677dd1feb468)) + + +### Dependencies + +* update deps to support no-copy operations ([#55](https://github.com/libp2p/js-libp2p-record/issues/55)) ([7be8515](https://github.com/libp2p/js-libp2p-record/commit/7be8515ad87d062bbc9db20fc3134ed06b1286a9)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-record/compare/v1.0.5...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest libp2p interfaces ([#45](https://github.com/libp2p/js-libp2p-record/issues/45)) ([b5eb989](https://github.com/libp2p/js-libp2p-record/commit/b5eb9897f23ebf39e2a728672f3727222bc1159f)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-record/compare/v1.0.4...v1.0.5) (2022-05-25) + + +### Trivial Changes + +* **deps:** bump @libp2p/interfaces from 1.3.32 to 2.0.2 ([#43](https://github.com/libp2p/js-libp2p-record/issues/43)) ([992677b](https://github.com/libp2p/js-libp2p-record/commit/992677bd6bf432b3ce894c53ac7a721e2dd44bf9)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-record/compare/v1.0.3...v1.0.4) (2022-04-14) + + +### Bug Fixes + +* pad ns correctly ([#41](https://github.com/libp2p/js-libp2p-record/issues/41)) ([18030d9](https://github.com/libp2p/js-libp2p-record/commit/18030d9d3832a7d09dee928923909875a5780a2f)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-record/compare/v1.0.2...v1.0.3) (2022-04-13) + + +### Bug Fixes + +* update interfaces ([#40](https://github.com/libp2p/js-libp2p-record/issues/40)) ([e2713a3](https://github.com/libp2p/js-libp2p-record/commit/e2713a3a6b5351e2dc012cf734ff1c945479920b)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-record/compare/v1.0.1...v1.0.2) (2022-04-09) + + +### Bug Fixes + +* use protons ([#39](https://github.com/libp2p/js-libp2p-record/issues/39)) ([10b4cc2](https://github.com/libp2p/js-libp2p-record/commit/10b4cc2600e8f3bed9a2d646b68b0b2107e1caa4)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-record/compare/v1.0.0...v1.0.1) (2022-03-24) + + +### Bug Fixes + +* export selector/validators with the same name as their prefix ([#34](https://github.com/libp2p/js-libp2p-record/issues/34)) ([4913d1f](https://github.com/libp2p/js-libp2p-record/commit/4913d1fec2ed92d4803f3497bef81142bd560a91)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-record/compare/v0.10.6...v1.0.0) (2022-02-18) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#32](https://github.com/libp2p/js-libp2p-record/issues/32)) ([89cc2ef](https://github.com/libp2p/js-libp2p-record/commit/89cc2ef5234835c82ea29ff54a4887d630921ae3)) + +## [0.10.6](https://github.com/libp2p/js-libp2p-record/compare/v0.10.5...v0.10.6) (2021-09-24) + + +### Bug Fixes + +* auto select if only one record ([#31](https://github.com/libp2p/js-libp2p-record/issues/31)) ([53bc7f2](https://github.com/libp2p/js-libp2p-record/commit/53bc7f2627a95256337033977a05df54a534f951)) + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-record/compare/v0.10.4...v0.10.5) (2021-08-18) + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-record/compare/v0.10.3...v0.10.4) (2021-07-07) + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-record/compare/v0.10.2...v0.10.3) (2021-04-22) + + +### Bug Fixes + +* use dht selectors and validators from interfaces ([#28](https://github.com/libp2p/js-libp2p-record/issues/28)) ([7b211a5](https://github.com/libp2p/js-libp2p-record/commit/7b211a528675018abbc8e4674bedbdd5ab7b5eea)) + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-record/compare/v0.10.1...v0.10.2) (2021-04-20) + + +### Bug Fixes + +* specify pbjs root ([#27](https://github.com/libp2p/js-libp2p-record/issues/27)) ([32ddb1d](https://github.com/libp2p/js-libp2p-record/commit/32ddb1deec71543d0ef34157b6ef2d271e8408f5)) + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-record/compare/v0.10.0...v0.10.1) (2021-04-07) + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-record/compare/v0.8.0...v0.10.0) (2021-02-02) + + +### Features + +* add types and update deps ([#25](https://github.com/libp2p/js-libp2p-record/issues/25)) ([e2395de](https://github.com/libp2p/js-libp2p-record/commit/e2395de924a9c71d761c6ea3f5aab2844b252591)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-record/compare/v0.8.0...v0.9.0) (2020-08-07) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-record/compare/v0.7.3...v0.8.0) (2020-07-29) + + +### Bug Fixes + +* support uint8arrays in place of node buffers ([#23](https://github.com/libp2p/js-libp2p-record/issues/23)) ([3b99ee1](https://github.com/libp2p/js-libp2p-record/commit/3b99ee1)) + + +### BREAKING CHANGES + +* takes Uint8Arrays as well as Node Buffers + + + + +## [0.7.3](https://github.com/libp2p/js-libp2p-record/compare/v0.7.2...v0.7.3) (2020-04-27) + + +### Bug Fixes + +* remove buffer ([#21](https://github.com/libp2p/js-libp2p-record/issues/21)) ([80fb248](https://github.com/libp2p/js-libp2p-record/commit/80fb248)) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-record/compare/v0.7.1...v0.7.2) (2020-02-13) + + +### Bug Fixes + +* remove use of assert module ([#18](https://github.com/libp2p/js-libp2p-record/issues/18)) ([57e24a7](https://github.com/libp2p/js-libp2p-record/commit/57e24a7)) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-record/compare/v0.7.0...v0.7.1) (2020-01-03) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-record/compare/v0.6.3...v0.7.0) (2019-08-16) + + +### Code Refactoring + +* convert from callbacks to async ([#13](https://github.com/libp2p/js-libp2p-record/issues/13)) ([42eab95](https://github.com/libp2p/js-libp2p-record/commit/42eab95)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await + + + + +## [0.6.3](https://github.com/libp2p/js-libp2p-record/compare/v0.6.2...v0.6.3) (2019-05-23) + + +### Bug Fixes + +* remove leftpad ([#16](https://github.com/libp2p/js-libp2p-record/issues/16)) ([4f46885](https://github.com/libp2p/js-libp2p-record/commit/4f46885)) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-record/compare/v0.6.1...v0.6.2) (2019-02-20) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-record/compare/v0.6.0...v0.6.1) (2018-11-08) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-record/compare/v0.5.1...v0.6.0) (2018-10-18) + + +### Features + +* new record definition ([#8](https://github.com/libp2p/js-libp2p-record/issues/8)) ([10177ae](https://github.com/libp2p/js-libp2p-record/commit/10177ae)) + + +### BREAKING CHANGES + +* having the libp2p-record protobuf definition compliant with go-libp2p-record. Author and signature were removed. + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-record/compare/v0.5.0...v0.5.1) (2017-09-07) + + +### Features + +* replace protocol-buffers with protons ([#5](https://github.com/libp2p/js-libp2p-record/issues/5)) ([8774a4f](https://github.com/libp2p/js-libp2p-record/commit/8774a4f)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-record/compare/v0.4.0...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#4](https://github.com/libp2p/js-libp2p-record/issues/4)) ([bcba43c](https://github.com/libp2p/js-libp2p-record/commit/bcba43c)) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-record/compare/v0.3.1...v0.4.0) (2017-07-22) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-record/compare/v0.3.0...v0.3.1) (2017-03-29) + + + + +# 0.3.0 (2017-03-29) diff --git a/packages/record/LICENSE b/packages/record/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/record/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/record/LICENSE-APACHE b/packages/record/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/record/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/record/LICENSE-MIT b/packages/record/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/record/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/record/README.md b/packages/record/README.md new file mode 100644 index 0000000000..1f95184c88 --- /dev/null +++ b/packages/record/README.md @@ -0,0 +1,50 @@ +# @libp2p/record + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> libp2p record implementation + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +Implementation of [go-libp2p-record](https://github.com/libp2p/go-libp2p-record) in JavaScript. + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/record/package.json b/packages/record/package.json new file mode 100644 index 0000000000..caa5a086cd --- /dev/null +++ b/packages/record/package.json @@ -0,0 +1,94 @@ +{ + "name": "@libp2p/record", + "version": "3.0.4", + "description": "libp2p record implementation", + "author": "Friedel Ziegelmayer ", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/record#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./selectors": { + "types": "./dist/src/selectors.d.ts", + "import": "./dist/src/selectors.js" + }, + "./validators": { + "types": "./dist/src/validators.d.ts", + "import": "./dist/src/validators.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + }, + "ignorePatterns": [ + "src/record.d.ts" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check -i protons", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:chrome": "aegir test -t browser", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "build": "aegir build", + "generate": "protons ./src/record.proto" + }, + "dependencies": { + "@libp2p/interface-dht": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "multiformats": "^11.0.2", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@libp2p/crypto": "^1.0.0", + "aegir": "^39.0.10", + "protons": "^7.0.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/record/src/index.ts b/packages/record/src/index.ts new file mode 100644 index 0000000000..06ab8976b9 --- /dev/null +++ b/packages/record/src/index.ts @@ -0,0 +1,70 @@ +import { + Record +} from './record.js' +import * as utils from './utils.js' +import type { Uint8ArrayList } from 'uint8arraylist' + +export class Libp2pRecord { + public key: Uint8Array + public value: Uint8Array + public timeReceived: Date + + constructor (key: Uint8Array, value: Uint8Array, timeReceived: Date) { + if (!(key instanceof Uint8Array)) { + throw new Error('key must be a Uint8Array') + } + + if (!(value instanceof Uint8Array)) { + throw new Error('value must be a Uint8Array') + } + + this.key = key + this.value = value + this.timeReceived = timeReceived + } + + serialize (): Uint8Array { + return Record.encode(this.prepareSerialize()) + } + + /** + * Return the object format ready to be given to the protobuf library. + */ + prepareSerialize (): Record { + return { + key: this.key, + value: this.value, + timeReceived: utils.toRFC3339(this.timeReceived) + } + } + + /** + * Decode a protobuf encoded record + */ + static deserialize (raw: Uint8Array | Uint8ArrayList): Libp2pRecord { + const rec = Record.decode(raw) + + return new Libp2pRecord(rec.key, rec.value, new Date(rec.timeReceived)) + } + + /** + * Create a record from the raw object returned from the protobuf library + */ + static fromDeserialized (obj: Record): Libp2pRecord { + const recvtime = utils.parseRFC3339(obj.timeReceived) + + if (obj.key == null) { + throw new Error('key missing from deserialized object') + } + + if (obj.value == null) { + throw new Error('value missing from deserialized object') + } + + const rec = new Libp2pRecord( + obj.key, obj.value, recvtime + ) + + return rec + } +} diff --git a/packages/record/src/record.proto b/packages/record/src/record.proto new file mode 100644 index 0000000000..ed962bfca0 --- /dev/null +++ b/packages/record/src/record.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +// Record represents a dht record that contains a value +// for a key value pair +message Record { + // The key that references this record + bytes key = 1; + + // The actual value this record is storing + bytes value = 2; + + // Note: These fields were removed from the Record message + // hash of the authors public key + // optional bytes author = 3; + // A PKI signature for the key+value+author + // optional bytes signature = 4; + + // Time the record was received, set by receiver + string timeReceived = 5; +} diff --git a/packages/record/src/record.ts b/packages/record/src/record.ts new file mode 100644 index 0000000000..89da228397 --- /dev/null +++ b/packages/record/src/record.ts @@ -0,0 +1,87 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Record { + key: Uint8Array + value: Uint8Array + timeReceived: string +} + +export namespace Record { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if ((obj.key != null && obj.key.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.key) + } + + if ((obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value) + } + + if ((obj.timeReceived != null && obj.timeReceived !== '')) { + w.uint32(42) + w.string(obj.timeReceived) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: new Uint8Array(0), + value: new Uint8Array(0), + timeReceived: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.bytes() + break + case 2: + obj.value = reader.bytes() + break + case 5: + obj.timeReceived = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Record.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Record => { + return decodeMessage(buf, Record.codec()) + } +} diff --git a/packages/record/src/selectors.ts b/packages/record/src/selectors.ts new file mode 100644 index 0000000000..ed28d0124d --- /dev/null +++ b/packages/record/src/selectors.ts @@ -0,0 +1,50 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Selectors } from '@libp2p/interface-dht' + +/** + * Select the best record out of the given records + */ +export function bestRecord (selectors: Selectors, k: Uint8Array, records: Uint8Array[]): number { + if (records.length === 0) { + const errMsg = 'No records given' + + throw new CodeError(errMsg, 'ERR_NO_RECORDS_RECEIVED') + } + + const kStr = uint8ArrayToString(k) + const parts = kStr.split('/') + + if (parts.length < 3) { + const errMsg = 'Record key does not have a selector function' + + throw new CodeError(errMsg, 'ERR_NO_SELECTOR_FUNCTION_FOR_RECORD_KEY') + } + + const selector = selectors[parts[1].toString()] + + if (selector == null) { + const errMsg = `Unrecognized key prefix: ${parts[1]}` + + throw new CodeError(errMsg, 'ERR_UNRECOGNIZED_KEY_PREFIX') + } + + if (records.length === 1) { + return 0 + } + + return selector(k, records) +} + +/** + * Best record selector, for public key records. + * Simply returns the first record, as all valid public key + * records are equal + */ +function publickKey (k: Uint8Array, records: Uint8Array[]): number { + return 0 +} + +export const selectors: Selectors = { + pk: publickKey +} diff --git a/packages/record/src/utils.ts b/packages/record/src/utils.ts new file mode 100644 index 0000000000..b9ee448887 --- /dev/null +++ b/packages/record/src/utils.ts @@ -0,0 +1,46 @@ +/** + * Convert a JavaScript date into an `RFC3339Nano` formatted + * string + */ +export function toRFC3339 (time: Date): string { + const year = time.getUTCFullYear() + const month = String(time.getUTCMonth() + 1).padStart(2, '0') + const day = String(time.getUTCDate()).padStart(2, '0') + const hour = String(time.getUTCHours()).padStart(2, '0') + const minute = String(time.getUTCMinutes()).padStart(2, '0') + const seconds = String(time.getUTCSeconds()).padStart(2, '0') + const milliseconds = time.getUTCMilliseconds() + const nanoseconds = String(milliseconds * 1000 * 1000).padStart(9, '0') + + return `${year}-${month}-${day}T${hour}:${minute}:${seconds}.${nanoseconds}Z` +} + +/** + * Parses a date string formatted as `RFC3339Nano` into a + * JavaScript Date object + */ +export function parseRFC3339 (time: string): Date { + const rfc3339Matcher = new RegExp( + // 2006-01-02T + '(\\d{4})-(\\d{2})-(\\d{2})T' + + // 15:04:05 + '(\\d{2}):(\\d{2}):(\\d{2})' + + // .999999999Z + '\\.(\\d+)Z' + ) + const m = String(time).trim().match(rfc3339Matcher) + + if (m == null) { + throw new Error('Invalid format') + } + + const year = parseInt(m[1], 10) + const month = parseInt(m[2], 10) - 1 + const date = parseInt(m[3], 10) + const hour = parseInt(m[4], 10) + const minute = parseInt(m[5], 10) + const second = parseInt(m[6], 10) + const millisecond = parseInt(m[7].slice(0, -6), 10) + + return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) +} diff --git a/packages/record/src/validators.ts b/packages/record/src/validators.ts new file mode 100644 index 0000000000..f0d438b6a7 --- /dev/null +++ b/packages/record/src/validators.ts @@ -0,0 +1,69 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { sha256 } from 'multiformats/hashes/sha2' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Libp2pRecord } from './index.js' +import type { Validators } from '@libp2p/interface-dht' + +/** + * Checks a record and ensures it is still valid. + * It runs the needed validators. + * If verification fails the returned Promise will reject with the error. + */ +export async function verifyRecord (validators: Validators, record: Libp2pRecord): Promise { + const key = record.key + const keyString = uint8ArrayToString(key) + const parts = keyString.split('/') + + if (parts.length < 3) { + // No validator available + return + } + + const validator = validators[parts[1].toString()] + + if (validator == null) { + const errMsg = 'Invalid record keytype' + + throw new CodeError(errMsg, 'ERR_INVALID_RECORD_KEY_TYPE') + } + + await validator(key, record.value) +} + +/** + * Validator for public key records. + * Verifies that the passed in record value is the PublicKey + * that matches the passed in key. + * If validation fails the returned Promise will reject with the error. + * + * @param {Uint8Array} key - A valid key is of the form `'/pk/'` + * @param {Uint8Array} publicKey - The public key to validate against (protobuf encoded). + */ +const validatePublicKeyRecord = async (key: Uint8Array, publicKey: Uint8Array): Promise => { + if (!(key instanceof Uint8Array)) { + throw new CodeError('"key" must be a Uint8Array', 'ERR_INVALID_RECORD_KEY_NOT_BUFFER') + } + + if (key.byteLength < 5) { + throw new CodeError('invalid public key record', 'ERR_INVALID_RECORD_KEY_TOO_SHORT') + } + + const prefix = uint8ArrayToString(key.subarray(0, 4)) + + if (prefix !== '/pk/') { + throw new CodeError('key was not prefixed with /pk/', 'ERR_INVALID_RECORD_KEY_BAD_PREFIX') + } + + const keyhash = key.slice(4) + + const publicKeyHash = await sha256.digest(publicKey) + + if (!uint8ArrayEquals(keyhash, publicKeyHash.bytes)) { + throw new CodeError('public key does not match passed in key', 'ERR_INVALID_RECORD_HASH_MISMATCH') + } +} + +export const validators: Validators = { + pk: validatePublicKeyRecord +} diff --git a/packages/record/test/fixtures/go-key-records.ts b/packages/record/test/fixtures/go-key-records.ts new file mode 100644 index 0000000000..0bba87ea1d --- /dev/null +++ b/packages/record/test/fixtures/go-key-records.ts @@ -0,0 +1,5 @@ +import { base64pad } from 'multiformats/bases/base64' + +export const publicKey = base64pad.decode( + 'MCAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=' +) diff --git a/packages/record/test/fixtures/go-record.ts b/packages/record/test/fixtures/go-record.ts new file mode 100644 index 0000000000..5582f81ffc --- /dev/null +++ b/packages/record/test/fixtures/go-record.ts @@ -0,0 +1,26 @@ +import { base16 } from 'multiformats/bases/base16' + +// Fixtures generated using gore (https://github.com/motemen/gore) +// +// :import github.com/libp2p/go-libp2p-record +// :import github.com/libp2p/go-libp2p-crypto +// +// priv, pub, err := crypto.GenerateKeyPair(crypto.RSA, 1024) +// +// rec, err := record.MakePutRecord(priv, "hello", []byte("world"), false) +// rec2, err := recordd.MakePutRecord(priv, "hello", []byte("world"), true) +// +// :import github.com/gogo/protobuf/proto +// enc, err := proto.Marshal(rec) +// enc2, err := proto.Marshal(rec2) +// +// :import io/ioutil +// ioutil.WriteFile("js-libp2p-record/test/fixtures/record.bin", enc, 0644) +// ioutil.WriteFile("js-libp2p-record/test/fixtures/record-signed.bin", enc2, 0644) +export const serialized = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116' +) + +export const serializedSigned = base16.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117' +) diff --git a/packages/record/test/record.spec.ts b/packages/record/test/record.spec.ts new file mode 100644 index 0000000000..ce091b4d4d --- /dev/null +++ b/packages/record/test/record.spec.ts @@ -0,0 +1,49 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '../src/index.js' +import * as fixture from './fixtures/go-record.js' + +const date = new Date() + +describe('record', () => { + it('new', () => { + const rec = new Libp2pRecord( + uint8ArrayFromString('hello'), + uint8ArrayFromString('world'), + new Date() + ) + + expect(rec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(rec).to.have.property('value').eql(uint8ArrayFromString('world')) + }) + + it('serialize & deserialize', () => { + const rec = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) + const dec = Libp2pRecord.deserialize(rec.serialize()) + + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + expect(dec.timeReceived).to.be.eql(date) + }) + + it('serialize & deserialize with padding', () => { + // m/d/h/m/s/ms all need padding with 0s when converted to RFC3339 format + const date = new Date('2022-04-03T01:04:08.078Z') + + const rec = new Libp2pRecord(uint8ArrayFromString('hello'), uint8ArrayFromString('world'), date) + const dec = Libp2pRecord.deserialize(rec.serialize()) + + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + expect(dec.timeReceived).to.be.eql(date) + }) + + describe('go interop', () => { + it('no signature', () => { + const dec = Libp2pRecord.deserialize(fixture.serialized) + expect(dec).to.have.property('key').eql(uint8ArrayFromString('hello')) + expect(dec).to.have.property('value').eql(uint8ArrayFromString('world')) + }) + }) +}) diff --git a/packages/record/test/selection.spec.ts b/packages/record/test/selection.spec.ts new file mode 100644 index 0000000000..443be63691 --- /dev/null +++ b/packages/record/test/selection.spec.ts @@ -0,0 +1,73 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as selection from '../src/selectors.js' +import type { Selectors } from '@libp2p/interface-dht' + +const records = [new Uint8Array(), uint8ArrayFromString('hello')] + +describe('selection', () => { + describe('bestRecord', () => { + it('throws no records given when no records received', () => { + expect( + () => selection.bestRecord({}, uint8ArrayFromString('/'), []) + ).to.throw( + /No records given/ + ) + }) + + it('throws on missing selector in the record key', () => { + expect( + () => selection.bestRecord({}, uint8ArrayFromString('/'), records) + ).to.throw( + /Record key does not have a selector function/ + ) + }) + + it('throws on unknown key prefix', () => { + expect( + // @ts-expect-error invalid input + () => selection.bestRecord({ world () {} }, uint8ArrayFromString('/hello/'), records) + ).to.throw( + /Unrecognized key prefix: hello/ + ) + }) + + it('returns the index from the matching selector', () => { + const selectors: Selectors = { + hello (k, recs) { + expect(k).to.be.eql(uint8ArrayFromString('/hello/world')) + expect(recs).to.be.eql(records) + + return 1 + } + } + + expect( + selection.bestRecord(selectors, uint8ArrayFromString('/hello/world'), records) + ).to.equal( + 1 + ) + }) + }) + + describe('selectors', () => { + it('public key', () => { + expect( + selection.selectors.pk(uint8ArrayFromString('/hello/world'), records) + ).to.equal( + 0 + ) + }) + + it('returns the first record when there is only one to select', () => { + expect( + selection.selectors.pk(uint8ArrayFromString('/hello/world'), [records[0]]) + ).to.equal( + 0 + ) + }) + }) +}) diff --git a/packages/record/test/utils.spec.ts b/packages/record/test/utils.spec.ts new file mode 100644 index 0000000000..2babd36d5d --- /dev/null +++ b/packages/record/test/utils.spec.ts @@ -0,0 +1,47 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import * as utils from '../src/utils.js' + +const dates = [{ + obj: new Date(Date.UTC(2016, 0, 1, 8, 22, 33, 392)), + str: '2016-01-01T08:22:33.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 3, 392)), + str: '2016-12-30T20:02:03.392000000Z' +}, { + obj: new Date(Date.UTC(2016, 11, 30, 20, 2, 5, 297)), + str: '2016-12-30T20:02:05.297000000Z' +}, { + obj: new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)), + str: '2012-02-25T10:10:10.010000000Z' +}] + +describe('utils', () => { + it('toRFC3339', () => { + dates.forEach((c) => { + expect(utils.toRFC3339(c.obj)).to.be.eql(c.str) + }) + }) + + it('parseRFC3339', () => { + dates.forEach((c) => { + expect(utils.parseRFC3339(c.str)).to.be.eql(c.obj) + }) + }) + + it('to and from RFC3339', () => { + dates.forEach((c) => { + expect( + utils.parseRFC3339(utils.toRFC3339(c.obj)) + ).to.be.eql( + c.obj + ) + expect( + utils.toRFC3339(utils.parseRFC3339(c.str)) + ).to.be.eql( + c.str + ) + }) + }) +}) diff --git a/packages/record/test/validator.spec.ts b/packages/record/test/validator.spec.ts new file mode 100644 index 0000000000..c2c20e187b --- /dev/null +++ b/packages/record/test/validator.spec.ts @@ -0,0 +1,137 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ + +import { generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys' +import { expect } from 'aegir/chai' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '../src/index.js' +import * as validator from '../src/validators.js' +import * as fixture from './fixtures/go-key-records.js' +import type { Validators } from '@libp2p/interface-dht' + +interface Cases { + valid: { + publicKey: Uint8Array[] + } + invalid: { + publicKey: Array<{ + data: Uint8Array + code: string + }> + } +} + +const generateCases = (hash: Uint8Array): Cases => { + return { + valid: { + publicKey: [ + Uint8Array.of( + ...uint8ArrayFromString('/pk/'), + ...hash + ) + ] + }, + invalid: { + publicKey: [{ + data: uint8ArrayFromString('/pk/'), + code: 'ERR_INVALID_RECORD_KEY_TOO_SHORT' + }, { + data: Uint8Array.of(...uint8ArrayFromString('/pk/'), ...uint8ArrayFromString('random')), + code: 'ERR_INVALID_RECORD_HASH_MISMATCH' + }, { + data: hash, + code: 'ERR_INVALID_RECORD_KEY_BAD_PREFIX' + }, { + // @ts-expect-error invalid input + data: 'not a buffer', + code: 'ERR_INVALID_RECORD_KEY_NOT_BUFFER' + }] + } + } +} + +describe('validator', () => { + let key: any + let hash: Uint8Array + let cases: Cases + + before(async () => { + key = await generateKeyPair('RSA', 1024) + hash = await key.public.hash() + cases = generateCases(hash) + }) + + describe('verifyRecord', () => { + it('calls matching validator', async () => { + const k = uint8ArrayFromString('/hello/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world'), new Date()) + + const validators: Validators = { + async hello (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + await validator.verifyRecord(validators, rec) + }) + + it('calls not matching any validator', async () => { + const k = uint8ArrayFromString('/hallo/you') + const rec = new Libp2pRecord(k, uint8ArrayFromString('world'), new Date()) + + const validators: Validators = { + async hello (key, value) { + expect(key).to.eql(k) + expect(value).to.eql(uint8ArrayFromString('world')) + } + } + await expect(validator.verifyRecord(validators, rec)) + .to.eventually.rejectedWith( + /Invalid record keytype/ + ) + }) + }) + + describe('validators', () => { + it('exports pk', () => { + expect(validator.validators).to.have.keys(['pk']) + }) + + describe('public key', () => { + it('exports func', () => { + const pk = validator.validators.pk + + expect(pk).to.be.a('function') + }) + + it('does not error on valid record', async () => { + return Promise.all(cases.valid.publicKey.map(async (k) => { + await validator.validators.pk(k, key.public.bytes) + })) + }) + + it('throws on invalid records', async () => { + return Promise.all(cases.invalid.publicKey.map(async ({ data, code }) => { + try { + // + await validator.validators.pk(data, key.public.bytes) + } catch (err: any) { + expect(err.code).to.eql(code) + return + } + expect.fail('did not throw an error with code ' + code) + })) + }) + }) + }) + + describe('go interop', () => { + it('record with key from from go', async () => { + const pubKey = unmarshalPublicKey(fixture.publicKey) + + const hash = await pubKey.hash() + const k = Uint8Array.of(...uint8ArrayFromString('/pk/'), ...hash) + await validator.validators.pk(k, pubKey.bytes) + }) + }) +}) diff --git a/packages/record/tsconfig.json b/packages/record/tsconfig.json new file mode 100644 index 0000000000..e59bf6ba31 --- /dev/null +++ b/packages/record/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface-dht" + }, + { + "path": "../interfaces" + } + ] +} diff --git a/packages/stream-multiplexer-mplex/.aegir.js b/packages/stream-multiplexer-mplex/.aegir.js new file mode 100644 index 0000000000..e6a4b3f7b4 --- /dev/null +++ b/packages/stream-multiplexer-mplex/.aegir.js @@ -0,0 +1,7 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + bundlesizeMax: '17KB' + } +} diff --git a/packages/stream-multiplexer-mplex/CHANGELOG.md b/packages/stream-multiplexer-mplex/CHANGELOG.md new file mode 100644 index 0000000000..9018e4926a --- /dev/null +++ b/packages/stream-multiplexer-mplex/CHANGELOG.md @@ -0,0 +1,765 @@ +## [8.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.3...v8.0.4) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1ebb5e4](https://github.com/libp2p/js-libp2p-mplex/commit/1ebb5e4ba3f3151c3da071e3e1064a2e53791cb2)) +* Update .github/workflows/stale.yml [skip ci] ([74e37d7](https://github.com/libp2p/js-libp2p-mplex/commit/74e37d7ba2c4b83a6b9340d2c9d71d875563f994)) + + +### Dependencies + +* **dev:** bump cborg from 1.10.2 to 2.0.1 ([#282](https://github.com/libp2p/js-libp2p-mplex/issues/282)) ([4dbc590](https://github.com/libp2p/js-libp2p-mplex/commit/4dbc590d1ac92581fe2e937757567eef3854acf4)) +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#281](https://github.com/libp2p/js-libp2p-mplex/issues/281)) ([1e03e75](https://github.com/libp2p/js-libp2p-mplex/commit/1e03e75369722be9872f747cd83f555bc08d49fe)) + +## [8.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.2...v8.0.3) (2023-05-17) + + +### Bug Fixes + +* use abstract stream class from muxer interface ([#279](https://github.com/libp2p/js-libp2p-mplex/issues/279)) ([73df4cf](https://github.com/libp2p/js-libp2p-mplex/commit/73df4cfe933e15ba7c52f1a01649deabf7acf502)) + +## [8.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.1...v8.0.2) (2023-05-17) + + +### Bug Fixes + +* remove unused eslint-plugin-etc dep ([#280](https://github.com/libp2p/js-libp2p-mplex/issues/280)) ([41b9f06](https://github.com/libp2p/js-libp2p-mplex/commit/41b9f06bd352c23fd619ddbf07c72f9c40f40df2)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.6 ([#278](https://github.com/libp2p/js-libp2p-mplex/issues/278)) ([3c7229b](https://github.com/libp2p/js-libp2p-mplex/commit/3c7229bf198cfaf45fc29bc2ac07cb7ff1c83149)) + +## [8.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v8.0.0...v8.0.1) (2023-04-19) + + +### Dependencies + +* update abortable-iterator to 5.x.x ([#273](https://github.com/libp2p/js-libp2p-mplex/issues/273)) ([c667204](https://github.com/libp2p/js-libp2p-mplex/commit/c6672049409bf25c07467774bdb46385fbc9a594)) + +## [8.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.7...v8.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* update to new stream type deps (#272) + +### Dependencies + +* update to new stream type deps ([#272](https://github.com/libp2p/js-libp2p-mplex/issues/272)) ([04c8c7f](https://github.com/libp2p/js-libp2p-mplex/commit/04c8c7fa69335a3c1495265666140f97a6287626)) + +## [7.1.7](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.6...v7.1.7) (2023-04-13) + + +### Dependencies + +* **dev:** bump it-drain from 2.0.1 to 3.0.1 ([#262](https://github.com/libp2p/js-libp2p-mplex/issues/262)) ([d96125b](https://github.com/libp2p/js-libp2p-mplex/commit/d96125be24ac80598f56de49c918dcba8b4db2b5)) +* **dev:** bump it-to-buffer from 3.0.1 to 4.0.1 ([#258](https://github.com/libp2p/js-libp2p-mplex/issues/258)) ([59e7558](https://github.com/libp2p/js-libp2p-mplex/commit/59e755892287c86bfd706e9dbfa942ce410067d1)) + +## [7.1.6](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.5...v7.1.6) (2023-04-13) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#260](https://github.com/libp2p/js-libp2p-mplex/issues/260)) ([c63ed58](https://github.com/libp2p/js-libp2p-mplex/commit/c63ed5843215353f9fdc81dffd5a635984f9d97c)) +* **dev:** bump it-foreach from 1.0.1 to 2.0.2 ([#265](https://github.com/libp2p/js-libp2p-mplex/issues/265)) ([76d27a4](https://github.com/libp2p/js-libp2p-mplex/commit/76d27a4b11f7b3d27d6b17f175ce9c5a72cda870)) +* **dev:** bump it-pipe from 2.0.5 to 3.0.1 ([#268](https://github.com/libp2p/js-libp2p-mplex/issues/268)) ([bd37717](https://github.com/libp2p/js-libp2p-mplex/commit/bd37717aa11ef68a25677645dadf6a75e99ae70b)) + +## [7.1.5](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.4...v7.1.5) (2023-04-13) + + +### Dependencies + +* update any-signal to 4.x.x ([#270](https://github.com/libp2p/js-libp2p-mplex/issues/270)) ([2820884](https://github.com/libp2p/js-libp2p-mplex/commit/2820884d005dfe44bf6009cf7a0c5d28bdc23f14)) + +## [7.1.4](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.3...v7.1.4) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#269](https://github.com/libp2p/js-libp2p-mplex/issues/269)) ([d14a122](https://github.com/libp2p/js-libp2p-mplex/commit/d14a122772680db171746acb9b639f3e7ffb46f4)) + +## [7.1.3](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.2...v7.1.3) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-map from 2.0.1 to 3.0.1 ([#263](https://github.com/libp2p/js-libp2p-mplex/issues/263)) ([c438001](https://github.com/libp2p/js-libp2p-mplex/commit/c4380018b73a00ab360997524a3375b8210f91e8)) + +## [7.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.1...v7.1.2) (2023-03-21) + + +### Trivial Changes + +* replace err-code with CodeError ([#242](https://github.com/libp2p/js-libp2p-mplex/issues/242)) ([8d58a3b](https://github.com/libp2p/js-libp2p-mplex/commit/8d58a3b187aa174970251f6040fe82f9e5ff0c6d)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([54de88d](https://github.com/libp2p/js-libp2p-mplex/commit/54de88df5443b031c4d443ed23480229d3e89cbb)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([df03e8d](https://github.com/libp2p/js-libp2p-mplex/commit/df03e8df27fe52f4471b259ff65a14afc8d5d203)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9c3f235](https://github.com/libp2p/js-libp2p-mplex/commit/9c3f235a7f2cb776b90387aaf53c57084e4de95f)) + + +### Dependencies + +* **dev:** bump typescript from 4.9.5 to 5.0.2 ([#256](https://github.com/libp2p/js-libp2p-mplex/issues/256)) ([a3590af](https://github.com/libp2p/js-libp2p-mplex/commit/a3590af093dee73c873aef1539bc8d81b0d61b08)) +* **dev:** Upgrade aegir to 38.1.7 ([#257](https://github.com/libp2p/js-libp2p-mplex/issues/257)) ([e0bf45a](https://github.com/libp2p/js-libp2p-mplex/commit/e0bf45af3ec85986d15ce3d05cb62637bc761a3e)) + +## [7.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v7.1.0...v7.1.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#241](https://github.com/libp2p/js-libp2p-mplex/issues/241)) ([083f462](https://github.com/libp2p/js-libp2p-mplex/commit/083f462b36429ae0f326b79c253017f824fba6ed)) + +## [7.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.7...v7.1.0) (2022-11-25) + + +### Features + +* add message byte batching ([#235](https://github.com/libp2p/js-libp2p-mplex/issues/235)) ([4e2a49d](https://github.com/libp2p/js-libp2p-mplex/commit/4e2a49df22430316140cd37a96fc3b6a8f95b76a)) + +## [7.0.7](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.6...v7.0.7) (2022-11-25) + + +### Bug Fixes + +* only accept lists of messages in encoder ([#236](https://github.com/libp2p/js-libp2p-mplex/issues/236)) ([4175cac](https://github.com/libp2p/js-libp2p-mplex/commit/4175cacbc32a76a525beccfad8fc13f87733e725)) + +## [7.0.6](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.5...v7.0.6) (2022-11-25) + + +### Bug Fixes + +* reduce async iterator loops per package in _createSink ([#224](https://github.com/libp2p/js-libp2p-mplex/issues/224)) ([e2a32ad](https://github.com/libp2p/js-libp2p-mplex/commit/e2a32ad1cc9bf396c95906e500c1f74acc134828)), closes [/github.com/libp2p/js-libp2p/issues/1420#issuecomment-1273272662](https://github.com/libp2p//github.com/libp2p/js-libp2p/issues/1420/issues/issuecomment-1273272662) + +## [7.0.5](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.4...v7.0.5) (2022-11-24) + + +### Bug Fixes + +* apply message size limit before decoding message ([#231](https://github.com/libp2p/js-libp2p-mplex/issues/231)) ([279ad47](https://github.com/libp2p/js-libp2p-mplex/commit/279ad47517ae3d4bc99ab499bf1fd9ef67dbb74b)) +* limit unprocessed message queue size separately to message size ([#234](https://github.com/libp2p/js-libp2p-mplex/issues/234)) ([2297856](https://github.com/libp2p/js-libp2p-mplex/commit/2297856c3ffb05f9cabf52efc3b78ef96d3faf1e)) +* yield single buffers ([#233](https://github.com/libp2p/js-libp2p-mplex/issues/233)) ([31d3938](https://github.com/libp2p/js-libp2p-mplex/commit/31d3938f8fcdf56debbf8824ccbcbc057d5bd5be)) + +## [7.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.3...v7.0.4) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-map from 1.0.6 to 2.0.0 ([#225](https://github.com/libp2p/js-libp2p-mplex/issues/225)) ([a153108](https://github.com/libp2p/js-libp2p-mplex/commit/a15310817a325b5106112562d82739d86fa50a49)) + + +### Trivial Changes + +* update benchmark ([#232](https://github.com/libp2p/js-libp2p-mplex/issues/232)) ([d73381e](https://github.com/libp2p/js-libp2p-mplex/commit/d73381e00b5109505a15b10512e52d17b4b78dd6)) + +## [7.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.2...v7.0.3) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#227](https://github.com/libp2p/js-libp2p-mplex/issues/227)) ([345b37d](https://github.com/libp2p/js-libp2p-mplex/commit/345b37d3668298ca7d55fbc7e7e12091add1a219)) +* **dev:** bump it-foreach from 0.1.1 to 1.0.0 ([#226](https://github.com/libp2p/js-libp2p-mplex/issues/226)) ([01bae35](https://github.com/libp2p/js-libp2p-mplex/commit/01bae35a5c41346a945990be2618385bbea79572)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.1...v7.0.2) (2022-11-23) + + +### Dependencies + +* **dev:** bump it-drain from 1.0.5 to 2.0.0 ([#228](https://github.com/libp2p/js-libp2p-mplex/issues/228)) ([263251f](https://github.com/libp2p/js-libp2p-mplex/commit/263251fe3a4b8f55f6b4f431bdbfdb5b1006b42a)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v7.0.0...v7.0.1) (2022-11-21) + + +### Bug Fixes + +* type errors ([#230](https://github.com/libp2p/js-libp2p-mplex/issues/230)) ([e9c390a](https://github.com/libp2p/js-libp2p-mplex/commit/e9c390a195c46718e31e1f3bd233b0ab7c1f76b0)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.2...v7.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#223](https://github.com/libp2p/js-libp2p-mplex/issues/223)) ([9c9497f](https://github.com/libp2p/js-libp2p-mplex/commit/9c9497f5cb7a7fbe095d44d508a57a458dae9129)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.1...v6.0.2) (2022-10-07) + + +### Dependencies + +* bump @libp2p/interface-stream-muxer from 2.0.2 to 3.0.0 ([#220](https://github.com/libp2p/js-libp2p-mplex/issues/220)) ([5b45249](https://github.com/libp2p/js-libp2p-mplex/commit/5b452497d1f294ddf7c74283a16ea9e12f98c438)) + +## [6.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v6.0.0...v6.0.1) (2022-10-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-stream-muxer-compliance-tests from 4.0.0 to 5.0.0 ([#221](https://github.com/libp2p/js-libp2p-mplex/issues/221)) ([1e3153e](https://github.com/libp2p/js-libp2p-mplex/commit/1e3153e3da749ce159708e838bc350f9b024a32b)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.4...v6.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/components from 2.1.1 to 3.0.0 (#222) + +### Dependencies + +* bump @libp2p/components from 2.1.1 to 3.0.0 ([#222](https://github.com/libp2p/js-libp2p-mplex/issues/222)) ([9b7a800](https://github.com/libp2p/js-libp2p-mplex/commit/9b7a80003f87c76df4d069d3e0d29c940a9237b2)) + +## [5.2.4](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.3...v5.2.4) (2022-09-23) + + +### Bug Fixes + +* remove tracked map as the stats overwrite each other ([#217](https://github.com/libp2p/js-libp2p-mplex/issues/217)) ([d5f4d5f](https://github.com/libp2p/js-libp2p-mplex/commit/d5f4d5f6c92f2d6cc275d8a2250b1feb9b6b756f)) + + +### Trivial Changes + +* ignore coverage dir ([#219](https://github.com/libp2p/js-libp2p-mplex/issues/219)) ([298590f](https://github.com/libp2p/js-libp2p-mplex/commit/298590f839d147abc1f7168561a589b5977299ef)) +* refactor benchmarks for use in the browser ([#218](https://github.com/libp2p/js-libp2p-mplex/issues/218)) ([ccd3dc7](https://github.com/libp2p/js-libp2p-mplex/commit/ccd3dc74289273135345005c6e64e2a3b4e48be7)) + +## [5.2.3](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.2...v5.2.3) (2022-09-20) + + +### Bug Fixes + +* optimize stream sink for small messages ([#216](https://github.com/libp2p/js-libp2p-mplex/issues/216)) ([a10205b](https://github.com/libp2p/js-libp2p-mplex/commit/a10205bbf19db147a33201ba8fe1fc793661080d)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cf94f56](https://github.com/libp2p/js-libp2p-mplex/commit/cf94f56d82457f1c40e5d715d378216bb7d0ed5a)) + +## [5.2.2](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.1...v5.2.2) (2022-09-12) + + +### Bug Fixes + +* chunk messages over maxMsgSize ([#214](https://github.com/libp2p/js-libp2p-mplex/issues/214)) ([6d2d8cc](https://github.com/libp2p/js-libp2p-mplex/commit/6d2d8ccf412b4937a955f567bd5f65705b9827a1)) + +## [5.2.1](https://github.com/libp2p/js-libp2p-mplex/compare/v5.2.0...v5.2.1) (2022-09-08) + + +### Bug Fixes + +* do not treat stream source as pushable ([#211](https://github.com/libp2p/js-libp2p-mplex/issues/211)) ([359c103](https://github.com/libp2p/js-libp2p-mplex/commit/359c1038b870f84a9dfd390d4a0021c157fe0bcc)) + +## [5.2.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.2...v5.2.0) (2022-09-07) + + +### Features + +* close connections when too many streams are opened ([#213](https://github.com/libp2p/js-libp2p-mplex/issues/213)) ([9140770](https://github.com/libp2p/js-libp2p-mplex/commit/9140770f4559677bbe69fc84d4d2dbcc70068c9e)) + +## [5.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.1...v5.1.2) (2022-09-07) + + +### Documentation + +* make the example work ([#206](https://github.com/libp2p/js-libp2p-mplex/issues/206)) ([f07acc3](https://github.com/libp2p/js-libp2p-mplex/commit/f07acc361a0627ae848804ed6eb422efad70878f)) + +## [5.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v5.1.0...v5.1.1) (2022-08-30) + + +### Dependencies + +* update it-pushable ([#210](https://github.com/libp2p/js-libp2p-mplex/issues/210)) ([1188272](https://github.com/libp2p/js-libp2p-mplex/commit/1188272df64c490449c1d3341e4c06c320116b30)), closes [#209](https://github.com/libp2p/js-libp2p-mplex/issues/209) + +## [5.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v5.0.0...v5.1.0) (2022-08-30) + + +### Features + +* add benchmark ([#207](https://github.com/libp2p/js-libp2p-mplex/issues/207)) ([6bf491f](https://github.com/libp2p/js-libp2p-mplex/commit/6bf491fdad73ee29849740754d5094bc85e26c78)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.3...v5.0.0) (2022-08-10) + + +### ⚠ BREAKING CHANGES + +* mulitplexed streams now emit `Uint8ArrayList`s and not `Uint8Array`s to handle the case for when transports have smaller chunk sizes than the multiplexer + +### Bug Fixes + +* emit uint8arraylists for data ([#201](https://github.com/libp2p/js-libp2p-mplex/issues/201)) ([e85ebab](https://github.com/libp2p/js-libp2p-mplex/commit/e85ebab233117643ba8b5acc33b7f90dc491f27d)) + +## [4.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.2...v4.0.3) (2022-08-03) + + +### Trivial Changes + +* update project config ([#197](https://github.com/libp2p/js-libp2p-mplex/issues/197)) ([46334e6](https://github.com/libp2p/js-libp2p-mplex/commit/46334e6859cd17c47fe3ffcf2f194eb00f3e748a)) + + +### Dependencies + +* update uint8arraylist dep ([#199](https://github.com/libp2p/js-libp2p-mplex/issues/199)) ([6e3b9d8](https://github.com/libp2p/js-libp2p-mplex/commit/6e3b9d8b38d283e62103322f1173ccfed4db5a6a)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.1...v4.0.2) (2022-07-25) + + +### Bug Fixes + +* remove MPLEX_ prefix from error codes ([#195](https://github.com/libp2p/js-libp2p-mplex/issues/195)) ([c6c9581](https://github.com/libp2p/js-libp2p-mplex/commit/c6c9581b34259e1d3811a2edb91a1cc1ef854364)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v4.0.0...v4.0.1) (2022-07-22) + + +### Bug Fixes + +* remove need of buffer polyfill config for browser ([#194](https://github.com/libp2p/js-libp2p-mplex/issues/194)) ([7c39830](https://github.com/libp2p/js-libp2p-mplex/commit/7c39830280347dbcf976a921f677e7b0e725b9f7)) +* reset stream when over inbound stream limit ([#193](https://github.com/libp2p/js-libp2p-mplex/issues/193)) ([41fefa4](https://github.com/libp2p/js-libp2p-mplex/commit/41fefa4280e122f553fed72ce5c81805755dcc35)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v3.0.0...v4.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* upgrade to interface-stream-muxer 2.0.0 (#186) + +### Bug Fixes + +* upgrade to interface-stream-muxer 2.0.0 ([#186](https://github.com/libp2p/js-libp2p-mplex/issues/186)) ([f11f2ce](https://github.com/libp2p/js-libp2p-mplex/commit/f11f2ce88f705d0836414fa3ddda1b08f046437c)), closes [#185](https://github.com/libp2p/js-libp2p-mplex/issues/185) + +## [3.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v2.0.0...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* updates to simplified connection interface + +### Bug Fixes + +* limit incoming and outgoing streams separately ([#184](https://github.com/libp2p/js-libp2p-mplex/issues/184)) ([cd55d36](https://github.com/libp2p/js-libp2p-mplex/commit/cd55d36d4245868ebb884f0ce69fc6dfa5d8ca4b)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.2.1...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#181](https://github.com/libp2p/js-libp2p-mplex/issues/181)) ([dcd02d9](https://github.com/libp2p/js-libp2p-mplex/commit/dcd02d9456f223c43062ac031c7a03aa6c635f30)) + +### [1.2.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.2.0...v1.2.1) (2022-06-13) + + +### Bug Fixes + +* fix typo in error message ([#177](https://github.com/libp2p/js-libp2p-mplex/issues/177)) ([f71119d](https://github.com/libp2p/js-libp2p-mplex/commit/f71119d640ac8f3721ad1a87a5c4ccc8fc4bda1d)) + +## [1.2.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.2...v1.2.0) (2022-06-13) + + +### Features + +* limit internal message buffer size ([#174](https://github.com/libp2p/js-libp2p-mplex/issues/174)) ([0c8e1b0](https://github.com/libp2p/js-libp2p-mplex/commit/0c8e1b06d31c46b6ef768139c822caac1904789d)), closes [/github.com/libp2p/go-mplex/blob/master/multiplex.go#L26](https://github.com/libp2p//github.com/libp2p/go-mplex/blob/master/multiplex.go/issues/L26) + +### [1.1.2](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.1...v1.1.2) (2022-06-08) + + +### Bug Fixes + +* add per-connection stream limit ([#173](https://github.com/libp2p/js-libp2p-mplex/issues/173)) ([21371e7](https://github.com/libp2p/js-libp2p-mplex/commit/21371e7251b1d5523d7e4e09afa9a2ea3daa8079)) + +### [1.1.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.1.0...v1.1.1) (2022-06-08) + + +### Bug Fixes + +* re-enable encode from Uint8ArrayList test ([#172](https://github.com/libp2p/js-libp2p-mplex/issues/172)) ([897031f](https://github.com/libp2p/js-libp2p-mplex/commit/897031fa79cf5b8c2a746228341c8d31169c2af9)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.5...v1.1.0) (2022-05-23) + + +### Features + +* close read and write streams ([#170](https://github.com/libp2p/js-libp2p-mplex/issues/170)) ([3917968](https://github.com/libp2p/js-libp2p-mplex/commit/39179686ae033a2cc2821707dbec9e766fb4e099)), closes [#120](https://github.com/libp2p/js-libp2p-mplex/issues/120) [#115](https://github.com/libp2p/js-libp2p-mplex/issues/115) + +### [1.0.5](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.4...v1.0.5) (2022-05-05) + + +### Bug Fixes + +* ignore missing stream ([#169](https://github.com/libp2p/js-libp2p-mplex/issues/169)) ([f6d3dd9](https://github.com/libp2p/js-libp2p-mplex/commit/f6d3dd9f55020df93c8e7e116cb2ce1614b3404b)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.3...v1.0.4) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#168](https://github.com/libp2p/js-libp2p-mplex/issues/168)) ([f592f96](https://github.com/libp2p/js-libp2p-mplex/commit/f592f96adb6527da633fdc235890e32e53625906)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.2...v1.0.3) (2022-04-09) + + +### Trivial Changes + +* update aegir ([#167](https://github.com/libp2p/js-libp2p-mplex/issues/167)) ([0ef0c36](https://github.com/libp2p/js-libp2p-mplex/commit/0ef0c36f4d84d85ddbc06b725967bd9edac7a1cc)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.1...v1.0.2) (2022-03-17) + + +### Bug Fixes + +* update interfaces ([#162](https://github.com/libp2p/js-libp2p-mplex/issues/162)) ([ab9079c](https://github.com/libp2p/js-libp2p-mplex/commit/ab9079c26a5c98ea5487107e79bbf17ae9b34ad2)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-mplex/compare/v1.0.0...v1.0.1) (2022-02-21) + + +### Bug Fixes + +* update interfaces ([#160](https://github.com/libp2p/js-libp2p-mplex/issues/160)) ([43db1cb](https://github.com/libp2p/js-libp2p-mplex/commit/43db1cb61440859abc2cdefe5a9a362d0bf19497)) + + +### Trivial Changes + +* module name ([0137b94](https://github.com/libp2p/js-libp2p-mplex/commit/0137b9451e554a32d7e1f1c10eaacc00df225762)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.7...v1.0.0) (2022-02-14) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +Co-authored-by: Marin Petrunić + +### Features + +* convert to typescript ([#158](https://github.com/libp2p/js-libp2p-mplex/issues/158)) ([0cf727a](https://github.com/libp2p/js-libp2p-mplex/commit/0cf727ae101b3006400701b781d05a12eada59b7)) + +### [0.10.7](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.6...v0.10.7) (2022-01-14) + + +### Bug Fixes + +* remove abort controller dep ([#152](https://github.com/libp2p/js-libp2p-mplex/issues/152)) ([96943cb](https://github.com/libp2p/js-libp2p-mplex/commit/96943cb68bc01efffd7045f0c5a9a3ed978fbf0e)) + +### [0.10.6](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.5...v0.10.6) (2022-01-14) + + +### Trivial Changes + +* switch to unified ci ([#151](https://github.com/libp2p/js-libp2p-mplex/issues/151)) ([f14c349](https://github.com/libp2p/js-libp2p-mplex/commit/f14c34974c8b298179782f5ce3de93fb439fd764)) + +## [0.10.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.4...v0.10.5) (2021-12-07) + + +### Performance Improvements + +* do not call varint.decode() if buffer has 0 length ([#125](https://github.com/libp2p/js-libp2p-mplex/issues/125)) ([92f1727](https://github.com/libp2p/js-libp2p-mplex/commit/92f1727342c278a8dd025623cc4fe6cb265485e9)) + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.3...v0.10.4) (2021-07-08) + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.2...v0.10.3) (2021-04-16) + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.5...v0.10.2) (2021-01-29) + + +### Bug Fixes + +* ensure stream closes on abort or reset ([#116](https://github.com/libp2p/js-libp2p-mplex/issues/116)) ([77835b3](https://github.com/libp2p/js-libp2p-mplex/commit/77835b326fbce02e3a9bf92f0084d01e4e1d9cf9)) +* replace node buffers with uint8arrays ([#114](https://github.com/libp2p/js-libp2p-mplex/issues/114)) ([d005338](https://github.com/libp2p/js-libp2p-mplex/commit/d005338154b6882a22396e921ba4a38cc4e213fc)) + + +### BREAKING CHANGES + +* - All use of node Buffers has been replaced with Uint8Arrays + +* fix: keep allocUnsafe for node for performance + +Co-authored-by: Jacob Heun + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.10.0...v0.10.1) (2020-10-22) + + +### Bug Fixes + +* ensure stream closes on abort or reset ([#116](https://github.com/libp2p/js-libp2p-mplex/issues/116)) ([77835b3](https://github.com/libp2p/js-libp2p-mplex/commit/77835b326fbce02e3a9bf92f0084d01e4e1d9cf9)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.5...v0.10.0) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#114](https://github.com/libp2p/js-libp2p-mplex/issues/114)) ([d005338](https://github.com/libp2p/js-libp2p-mplex/commit/d005338)) + + +### BREAKING CHANGES + +* - All use of node Buffers has been replaced with Uint8Arrays + +* fix: keep allocUnsafe for node for performance + +Co-authored-by: Jacob Heun + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.4...v0.9.5) (2020-03-18) + + +### Bug Fixes + +* add buffer ([#106](https://github.com/libp2p/js-libp2p-mplex/issues/106)) ([71f3e5b](https://github.com/libp2p/js-libp2p-mplex/commit/71f3e5b)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.3...v0.9.4) (2020-02-13) + + +### Performance Improvements + +* small bl ([#101](https://github.com/libp2p/js-libp2p-mplex/issues/101)) ([7da79b6](https://github.com/libp2p/js-libp2p-mplex/commit/7da79b6)) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.2...v0.9.3) (2019-11-28) + + +### Features + +* message splitting ([#100](https://github.com/libp2p/js-libp2p-mplex/issues/100)) ([fba56a5](https://github.com/libp2p/js-libp2p-mplex/commit/fba56a5)) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.1...v0.9.2) (2019-10-28) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.9.0...v0.9.1) (2019-09-23) + + +### Features + +* add better support for external stream metadata tracking ([#98](https://github.com/libp2p/js-libp2p-mplex/issues/98)) ([96f1ca0](https://github.com/libp2p/js-libp2p-mplex/commit/96f1ca0)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.5...v0.9.0) (2019-09-18) + + +### Code Refactoring + +* async iterators ([#94](https://github.com/libp2p/js-libp2p-mplex/issues/94)) ([c9bede5](https://github.com/libp2p/js-libp2p-mplex/commit/c9bede5)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await while pull-streams are replaced with async iterators. The API has also been updated according to the latest `interface-stream-muxer` version, https://github.com/libp2p/interface-stream-muxer/tree/v0.7.0. + +License: MIT +Signed-off-by: Alan Shaw + + + + +## [0.8.5](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.4...v0.8.5) (2019-03-18) + + + + +## [0.8.4](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.3...v0.8.4) (2018-11-15) + + + + +## [0.8.3](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.2...v0.8.3) (2018-11-08) + + +### Bug Fixes + +* muxer.end will no longer hang ([#86](https://github.com/libp2p/js-libp2p-mplex/issues/86)) ([e23cbaf](https://github.com/libp2p/js-libp2p-mplex/commit/e23cbaf)) + + + + +## [0.8.2](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.1...v0.8.2) (2018-10-01) + + +### Bug Fixes + +* improve resiliency of internals _send ([#84](https://github.com/libp2p/js-libp2p-mplex/issues/84)) ([70dafb7](https://github.com/libp2p/js-libp2p-mplex/commit/70dafb7)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-mplex/compare/v0.8.0...v0.8.1) (2018-10-01) + + +### Bug Fixes + +* verify drain before new push ([#82](https://github.com/libp2p/js-libp2p-mplex/issues/82)) ([cd77e01](https://github.com/libp2p/js-libp2p-mplex/commit/cd77e01)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.7.0...v0.8.0) (2018-06-19) + + +### Bug Fixes + +* add setImmediatte to the call of callback ([8cdcd0d](https://github.com/libp2p/js-libp2p-mplex/commit/8cdcd0d)) +* catch Multiplexer is destroyed error into callback ([#79](https://github.com/libp2p/js-libp2p-mplex/issues/79)) ([b60205f](https://github.com/libp2p/js-libp2p-mplex/commit/b60205f)) +* missing dep and readme example ([#77](https://github.com/libp2p/js-libp2p-mplex/issues/77)) ([904cd7c](https://github.com/libp2p/js-libp2p-mplex/commit/904cd7c)) +* package.json deps semver ([126b966](https://github.com/libp2p/js-libp2p-mplex/commit/126b966)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.6.0...v0.7.0) (2018-04-05) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-mplex/compare/v0.5.1...v0.6.0) (2018-02-19) + + +### Features + +* mplex is all here ([20cf80a](https://github.com/libp2p/js-libp2p-mplex/commit/20cf80a)) +* support new Buffer ([c1384c3](https://github.com/libp2p/js-libp2p-mplex/commit/c1384c3)) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.5.0...v0.5.1) (2017-12-14) + + +### Features + +* porting to new aegir ([#70](https://github.com/libp2p/js-libp2p-multiplex/issues/70)) ([30fc825](https://github.com/libp2p/js-libp2p-multiplex/commit/30fc825)) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.4...v0.5.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#69](https://github.com/libp2p/js-libp2p-multiplex/issues/69)) ([d58f50e](https://github.com/libp2p/js-libp2p-multiplex/commit/d58f50e)) + + + + +## [0.4.4](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.3...v0.4.4) (2017-07-08) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.2...v0.4.3) (2017-03-21) + + + + +## [0.4.2](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.1...v0.4.2) (2017-03-21) + + +### Bug Fixes + +* add missing setImmediate shim ([b039b81](https://github.com/libp2p/js-libp2p-multiplex/commit/b039b81)), closes [#61](https://github.com/libp2p/js-libp2p-multiplex/issues/61) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.4.0...v0.4.1) (2017-02-21) + + +### Bug Fixes + +* correct handling of multiplex options ([fa78df4](https://github.com/libp2p/js-libp2p-multiplex/commit/fa78df4)) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.6...v0.4.0) (2017-02-15) + + + + +## [0.3.6](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.5...v0.3.6) (2017-02-09) + + + + +## [0.3.5](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.4...v0.3.5) (2017-01-26) + + + + +## [0.3.4](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.3...v0.3.4) (2017-01-24) + + + + +## [0.3.3](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.2...v0.3.3) (2017-01-24) + + +### Bug Fixes + +* check for callbacks ([9ef5553](https://github.com/libp2p/js-libp2p-multiplex/commit/9ef5553)) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.1...v0.3.2) (2017-01-24) + + +### Bug Fixes + +* dropped packed ([a7cfb8b](https://github.com/libp2p/js-libp2p-multiplex/commit/a7cfb8b)) + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.3.0...v0.3.1) (2017-01-20) + + +### Bug Fixes + +* **docs:** Update readme.md's example and added files for it ([ccd94c8](https://github.com/libp2p/js-libp2p-multiplex/commit/ccd94c8)) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.2.1...v0.3.0) (2017-01-20) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.2.0...v0.2.1) (2016-03-22) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-multiplex/compare/v0.1.0...v0.2.0) (2016-03-07) + + + + +# 0.1.0 (2016-03-07) diff --git a/packages/stream-multiplexer-mplex/LICENSE b/packages/stream-multiplexer-mplex/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/stream-multiplexer-mplex/LICENSE-APACHE b/packages/stream-multiplexer-mplex/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/stream-multiplexer-mplex/LICENSE-MIT b/packages/stream-multiplexer-mplex/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/stream-multiplexer-mplex/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/stream-multiplexer-mplex/README.md b/packages/stream-multiplexer-mplex/README.md new file mode 100644 index 0000000000..ee8b856784 --- /dev/null +++ b/packages/stream-multiplexer-mplex/README.md @@ -0,0 +1,73 @@ +# @libp2p/mplex + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> JavaScript implementation of + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://github.com/libp2p/interface-stream-muxer/raw/master/img/badge.png)](https://github.com/libp2p/interface-stream-muxer) + +## Usage + +```js +import { mplex } from '@libp2p/mplex' +import { pipe } from 'it-pipe' + +const factory = mplex() + +const muxer = factory.createStreamMuxer(components, { + onStream: stream => { // Receive a duplex stream from the remote + // ...receive data from the remote and optionally send data back + }, + onStreamEnd: stream => { + // ...handle any tracking you may need of stream closures + } +}) + +pipe(conn, muxer, conn) // conn is duplex connection to another peer + +const stream = muxer.newStream() // Create a new duplex stream to the remote + +// Use the duplex stream to send some data to the remote... +pipe([1, 2, 3], stream) +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js b/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js new file mode 100644 index 0000000000..cb247896c0 --- /dev/null +++ b/packages/stream-multiplexer-mplex/benchmark/send-and-receive.js @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ + +/* +$ node benchmark/send-and-receive.js +$ npx playwright-test benchmark/send-and-receive.js --runner benchmark +*/ + +import Benchmark from 'benchmark' +import { pipe } from 'it-pipe' +import { expect } from 'aegir/chai' +import { pushable } from 'it-pushable' +import { mplex } from '../dist/src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +const factory = mplex()() +const muxer = factory.createStreamMuxer() +const stream1 = muxer.newStream('hello') +const muxer2 = factory.createStreamMuxer({ + onIncomingStream: async (stream) => { + await pipe( + stream, + async function * transform (source) { // A generator is async iterable + for await (const chunk of source) { + yield chunk + } + }, + stream + ) + } +}) + +pipe(muxer, muxer2, muxer) + +const p = pushable() +const promise = pipe(p, stream1, async function collect (source) { + const vals = [] + for await (const val of source) { + vals.push(val) + } + return vals +}) + +// typical data of ethereum consensus attestation +const data = uint8ArrayFromString( + 'e40000000a000000000000000a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa070a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa070a00000000000000a45c8daa336e17a150300afd4c717313c84f291754c51a378f20958083c5fa0795d2ef8ae4e2b4d1e5b3d5ce47b518e3db2c8c4d082e4498805ac2a686c69f248761b78437db2927470c1e77ede9c18606110faacbcbe4f13052bde7f7eff6aab09edf7bc4929fda2230f943aba2c47b6f940d350cb20c76fad4a8d40e2f3f1f01', + 'hex' +) + +const count = 1000 + +new Benchmark.Suite() + .add('send and receive', async () => { + for (let i = 0; i < count; i++) { + p.push(data) + } + p.end() + const arr = await promise + expect(arr.length).to.be.equal(count) + }) + .on('error', (err) => { + console.error(err) + }) + .on('cycle', (event) => { + console.info(String(event.target)) + }) + .on('complete', function () { + // @ts-expect-error types are wrong + console.info(`Fastest is ${this.filter('fastest').map('name')}`) // eslint-disable-line @typescript-eslint/restrict-template-expressions + }) + // run async + .run({ async: true }) diff --git a/packages/stream-multiplexer-mplex/examples/dialer.js b/packages/stream-multiplexer-mplex/examples/dialer.js new file mode 100644 index 0000000000..9c33ba34ba --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/dialer.js @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ +'use strict' + +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import tcp from 'net' +import { pipe } from 'it-pipe' +import { toIterable } from './util.js' +import { Mplex } from '../dist/src/index.js' + +const socket = toIterable(tcp.connect(9999)) +console.log('[dialer] socket stream opened') + +const controller = new AbortController() + +const factory = new Mplex({ signal: controller.signal }) +const muxer = factory.createStreamMuxer() + +const pipeMuxerToSocket = async () => { + await pipe(muxer, socket, muxer) + console.log('[dialer] socket stream closed') +} + +const sendAndReceive = async () => { + const muxedStream = muxer.newStream('hello') + console.log('[dialer] muxed stream opened') + + await pipe( + [uint8ArrayFromString('hey, how is it going. I am the dialer')], + muxedStream, + async source => { + for await (const chunk of source) { + console.log('[dialer] received:') + console.log(uint8ArrayToString(chunk.slice())) + } + } + ) + console.log('[dialer] muxed stream closed') + + // Close the socket stream after 1s + setTimeout(() => controller.abort(), 1000) +} + +pipeMuxerToSocket() +sendAndReceive() diff --git a/packages/stream-multiplexer-mplex/examples/listener.js b/packages/stream-multiplexer-mplex/examples/listener.js new file mode 100644 index 0000000000..189c945531 --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/listener.js @@ -0,0 +1,37 @@ +/* eslint-disable no-console */ +'use strict' + +import tcp from 'net' +import { pipe } from 'it-pipe' +import { toIterable } from './util.js' +import { Mplex } from '../dist/src/index.js' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' + +const listener = tcp.createServer(async socket => { + console.log('[listener] Got connection!') + + const factory = new Mplex() + socket = toIterable(socket) + const muxer = factory.createStreamMuxer({ + onIncomingStream: async (stream) => { + console.log('[listener] muxed stream opened, id:', stream.id) + await pipe( + stream, + source => (async function * () { + for await (const chunk of source) { + console.log('[listener] received:') + console.log(uint8ArrayToString(chunk.slice())) + yield uint8ArrayFromString('thanks for the message, I am the listener') + } + })(), + stream + ) + console.log('[listener] muxed stream closed') + } + }) + await pipe(socket, muxer, socket) + console.log('[listener] socket stream closed') +}) + +listener.listen(9999, () => console.log('[listener] listening on 9999')) diff --git a/packages/stream-multiplexer-mplex/examples/util.js b/packages/stream-multiplexer-mplex/examples/util.js new file mode 100644 index 0000000000..057c0c6787 --- /dev/null +++ b/packages/stream-multiplexer-mplex/examples/util.js @@ -0,0 +1,17 @@ +// Simple convertion of Node.js duplex to iterable duplex (no backpressure) +export const toIterable = socket => { + return { + sink: async source => { + try { + for await (const chunk of source) { + socket.write(chunk) + } + } catch (err) { + // If not an abort then destroy the socket with an error + return socket.destroy(err.code === 'ABORT_ERR' ? null : err) + } + socket.end() + }, + source: socket + } +} diff --git a/packages/stream-multiplexer-mplex/package.json b/packages/stream-multiplexer-mplex/package.json new file mode 100644 index 0000000000..aaafc96a07 --- /dev/null +++ b/packages/stream-multiplexer-mplex/package.json @@ -0,0 +1,96 @@ +{ + "name": "@libp2p/mplex", + "version": "8.0.4", + "description": "JavaScript implementation of https://github.com/libp2p/mplex", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/stream-multiplexer-mplex#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "connection", + "duplex", + "libp2p", + "mplex", + "multiplex", + "muxer", + "stream" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "benchmark": "node ./node_modules/.bin/benchmark benchmark/send-and-receive.js", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "abortable-iterator": "^5.0.1", + "any-signal": "^4.1.1", + "benchmark": "^2.1.4", + "it-batched-bytes": "^2.0.2", + "it-pushable": "^3.1.3", + "it-stream-types": "^2.0.1", + "rate-limiter-flexible": "^2.3.11", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3", + "varint": "^6.0.0" + }, + "devDependencies": { + "@libp2p/interface-stream-muxer-compliance-tests": "^7.0.0", + "@types/varint": "^6.0.0", + "aegir": "^39.0.10", + "cborg": "^2.0.1", + "delay": "^6.0.0", + "iso-random-stream": "^2.0.2", + "it-all": "^3.0.1", + "it-drain": "^3.0.2", + "it-foreach": "^2.0.2", + "it-map": "^3.0.3", + "it-pipe": "^3.0.1", + "it-to-buffer": "^4.0.1", + "p-defer": "^4.0.0", + "random-int": "^3.0.0" + }, + "browser": { + "./dist/src/alloc-unsafe.js": "./dist/src/alloc-unsafe-browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts b/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts new file mode 100644 index 0000000000..111defb5c6 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/alloc-unsafe-browser.ts @@ -0,0 +1,3 @@ +export function allocUnsafe (size: number): Uint8Array { + return new Uint8Array(size) +} diff --git a/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts b/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts new file mode 100644 index 0000000000..5387f4153c --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/alloc-unsafe.ts @@ -0,0 +1,3 @@ +export function allocUnsafe (size: number): Buffer { + return Buffer.allocUnsafe(size) +} diff --git a/packages/stream-multiplexer-mplex/src/decode.ts b/packages/stream-multiplexer-mplex/src/decode.ts new file mode 100644 index 0000000000..2b85cd5e83 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/decode.ts @@ -0,0 +1,142 @@ +import { Uint8ArrayList } from 'uint8arraylist' +import { MessageTypeNames, MessageTypes } from './message-types.js' +import type { Message } from './message-types.js' + +export const MAX_MSG_SIZE = 1 << 20 // 1MB +export const MAX_MSG_QUEUE_SIZE = 4 << 20 // 4MB + +interface MessageHeader { + id: number + type: keyof typeof MessageTypeNames + offset: number + length: number +} + +export class Decoder { + private readonly _buffer: Uint8ArrayList + private _headerInfo: MessageHeader | null + private readonly _maxMessageSize: number + private readonly _maxUnprocessedMessageQueueSize: number + + constructor (maxMessageSize: number = MAX_MSG_SIZE, maxUnprocessedMessageQueueSize: number = MAX_MSG_QUEUE_SIZE) { + this._buffer = new Uint8ArrayList() + this._headerInfo = null + this._maxMessageSize = maxMessageSize + this._maxUnprocessedMessageQueueSize = maxUnprocessedMessageQueueSize + } + + write (chunk: Uint8Array | Uint8ArrayList): Message[] { + if (chunk == null || chunk.length === 0) { + return [] + } + + this._buffer.append(chunk) + + if (this._buffer.byteLength > this._maxUnprocessedMessageQueueSize) { + throw Object.assign(new Error('unprocessed message queue size too large!'), { code: 'ERR_MSG_QUEUE_TOO_BIG' }) + } + + const msgs: Message[] = [] + + while (this._buffer.length !== 0) { + if (this._headerInfo == null) { + try { + this._headerInfo = this._decodeHeader(this._buffer) + } catch (err: any) { + if (err.code === 'ERR_MSG_TOO_BIG') { + throw err + } + + break // We haven't received enough data yet + } + } + + const { id, type, length, offset } = this._headerInfo + const bufferedDataLength = this._buffer.length - offset + + if (bufferedDataLength < length) { + break // not enough data yet + } + + const msg: any = { + id, + type + } + + if (type === MessageTypes.NEW_STREAM || type === MessageTypes.MESSAGE_INITIATOR || type === MessageTypes.MESSAGE_RECEIVER) { + msg.data = this._buffer.sublist(offset, offset + length) + } + + msgs.push(msg) + + this._buffer.consume(offset + length) + this._headerInfo = null + } + + return msgs + } + + /** + * Attempts to decode the message header from the buffer + */ + _decodeHeader (data: Uint8ArrayList): MessageHeader { + const { + value: h, + offset + } = readVarInt(data) + const { + value: length, + offset: end + } = readVarInt(data, offset) + + const type = h & 7 + + // @ts-expect-error h is a number not a CODE + if (MessageTypeNames[type] == null) { + throw new Error(`Invalid type received: ${type}`) + } + + // test message type varint + data length + if (length > this._maxMessageSize) { + throw Object.assign(new Error('message size too large!'), { code: 'ERR_MSG_TOO_BIG' }) + } + + // @ts-expect-error h is a number not a CODE + return { id: h >> 3, type, offset: offset + end, length } + } +} + +const MSB = 0x80 +const REST = 0x7F + +export interface ReadVarIntResult { + value: number + offset: number +} + +function readVarInt (buf: Uint8ArrayList, offset: number = 0): ReadVarIntResult { + let res = 0 + let shift = 0 + let counter = offset + let b: number + const l = buf.length + + do { + if (counter >= l || shift > 49) { + offset = 0 + throw new RangeError('Could not decode varint') + } + b = buf.get(counter++) + res += shift < 28 + ? (b & REST) << shift + : (b & REST) * Math.pow(2, shift) + shift += 7 + } while (b >= MSB) + + offset = counter - offset + + return { + value: res, + offset + } +} diff --git a/packages/stream-multiplexer-mplex/src/encode.ts b/packages/stream-multiplexer-mplex/src/encode.ts new file mode 100644 index 0000000000..9dc5194fdd --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/encode.ts @@ -0,0 +1,84 @@ +import batchedBytes from 'it-batched-bytes' +import { Uint8ArrayList } from 'uint8arraylist' +import varint from 'varint' +import { allocUnsafe } from './alloc-unsafe.js' +import { type Message, MessageTypes } from './message-types.js' +import type { Source } from 'it-stream-types' + +const POOL_SIZE = 10 * 1024 + +class Encoder { + private _pool: Uint8Array + private _poolOffset: number + + constructor () { + this._pool = allocUnsafe(POOL_SIZE) + this._poolOffset = 0 + } + + /** + * Encodes the given message and adds it to the passed list + */ + write (msg: Message, list: Uint8ArrayList): void { + const pool = this._pool + let offset = this._poolOffset + + varint.encode(msg.id << 3 | msg.type, pool, offset) + offset += varint.encode.bytes ?? 0 + + if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) { + varint.encode(msg.data.length, pool, offset) + } else { + varint.encode(0, pool, offset) + } + + offset += varint.encode.bytes ?? 0 + + const header = pool.subarray(this._poolOffset, offset) + + if (POOL_SIZE - offset < 100) { + this._pool = allocUnsafe(POOL_SIZE) + this._poolOffset = 0 + } else { + this._poolOffset = offset + } + + list.append(header) + + if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) { + list.append(msg.data) + } + } +} + +const encoder = new Encoder() + +/** + * Encode and yield one or more messages + */ +export async function * encode (source: Source, minSendBytes: number = 0): AsyncGenerator { + if (minSendBytes == null || minSendBytes === 0) { + // just send the messages + for await (const messages of source) { + const list = new Uint8ArrayList() + + for (const msg of messages) { + encoder.write(msg, list) + } + + yield list.subarray() + } + + return + } + + // batch messages up for sending + yield * batchedBytes(source, { + size: minSendBytes, + serialize: (obj, list) => { + for (const m of obj) { + encoder.write(m, list) + } + } + }) +} diff --git a/packages/stream-multiplexer-mplex/src/index.ts b/packages/stream-multiplexer-mplex/src/index.ts new file mode 100644 index 0000000000..d78f65255d --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/index.ts @@ -0,0 +1,81 @@ +import { MplexStreamMuxer } from './mplex.js' +import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface-stream-muxer' + +export interface MplexInit { + /** + * The maximum size of message that can be sent in one go in bytes. + * Messages larger than this will be split into multiple smaller + * messages. If we receive a message larger than this an error will + * be thrown and the connection closed. (default: 1MB) + */ + maxMsgSize?: number + + /** + * Constrains the size of the unprocessed message queue buffer. + * Before messages are deserialized, the raw bytes are buffered to ensure + * we have the complete message to deserialized. If the queue gets longer + * than this value an error will be thrown and the connection closed. + * (default: 4MB) + */ + maxUnprocessedMessageQueueSize?: number + + /** + * Each byte array written into a multiplexed stream is converted to one or + * more messages which are sent as byte arrays to the remote node. Sending + * lots of small messages can be expensive - use this setting to batch up + * the serialized bytes of all messages sent during the current tick up to + * this limit to send in one go similar to Nagle's algorithm. N.b. you + * should benchmark your application carefully when using this setting as it + * may cause the opposite of the desired effect. Omit this setting to send + * all messages as they become available. (default: undefined) + */ + minSendBytes?: number + + /** + * The maximum number of multiplexed streams that can be open at any + * one time. A request to open more than this will have a stream + * reset message sent immediately as a response for the newly opened + * stream id (default: 1024) + */ + maxInboundStreams?: number + + /** + * The maximum number of multiplexed streams that can be open at any + * one time. An attempt to open more than this will throw (default: 1024) + */ + maxOutboundStreams?: number + + /** + * Incoming stream messages are buffered until processed by the stream + * handler. If the buffer reaches this size in bytes the stream will + * be reset (default: 4MB) + */ + maxStreamBufferSize?: number + + /** + * When `maxInboundStreams` is hit, if the remote continues try to open + * more than this many new multiplexed streams per second the connection + * will be closed (default: 5) + */ + disconnectThreshold?: number +} + +class Mplex implements StreamMuxerFactory { + public protocol = '/mplex/6.7.0' + private readonly _init: MplexInit + + constructor (init: MplexInit = {}) { + this._init = init + } + + createStreamMuxer (init: StreamMuxerInit = {}): StreamMuxer { + return new MplexStreamMuxer({ + ...init, + ...this._init + }) + } +} + +export function mplex (init: MplexInit = {}): () => StreamMuxerFactory { + return () => new Mplex(init) +} diff --git a/packages/stream-multiplexer-mplex/src/message-types.ts b/packages/stream-multiplexer-mplex/src/message-types.ts new file mode 100644 index 0000000000..852acb7eac --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/message-types.ts @@ -0,0 +1,79 @@ +import type { Uint8ArrayList } from 'uint8arraylist' + +type INITIATOR_NAME = 'NEW_STREAM' | 'MESSAGE' | 'CLOSE' | 'RESET' +type RECEIVER_NAME = 'MESSAGE' | 'CLOSE' | 'RESET' +type NAME = 'NEW_STREAM' | 'MESSAGE_INITIATOR' | 'CLOSE_INITIATOR' | 'RESET_INITIATOR' | 'MESSAGE_RECEIVER' | 'CLOSE_RECEIVER' | 'RESET_RECEIVER' +type CODE = 0 | 1 | 2 | 3 | 4 | 5 | 6 + +export enum MessageTypes { + NEW_STREAM = 0, + MESSAGE_RECEIVER = 1, + MESSAGE_INITIATOR = 2, + CLOSE_RECEIVER = 3, + CLOSE_INITIATOR = 4, + RESET_RECEIVER = 5, + RESET_INITIATOR = 6 +} + +export const MessageTypeNames: Record = Object.freeze({ + 0: 'NEW_STREAM', + 1: 'MESSAGE_RECEIVER', + 2: 'MESSAGE_INITIATOR', + 3: 'CLOSE_RECEIVER', + 4: 'CLOSE_INITIATOR', + 5: 'RESET_RECEIVER', + 6: 'RESET_INITIATOR' +}) + +export const InitiatorMessageTypes: Record = Object.freeze({ + NEW_STREAM: MessageTypes.NEW_STREAM, + MESSAGE: MessageTypes.MESSAGE_INITIATOR, + CLOSE: MessageTypes.CLOSE_INITIATOR, + RESET: MessageTypes.RESET_INITIATOR +}) + +export const ReceiverMessageTypes: Record = Object.freeze({ + MESSAGE: MessageTypes.MESSAGE_RECEIVER, + CLOSE: MessageTypes.CLOSE_RECEIVER, + RESET: MessageTypes.RESET_RECEIVER +}) + +export interface NewStreamMessage { + id: number + type: MessageTypes.NEW_STREAM + data: Uint8ArrayList +} + +export interface MessageReceiverMessage { + id: number + type: MessageTypes.MESSAGE_RECEIVER + data: Uint8ArrayList +} + +export interface MessageInitiatorMessage { + id: number + type: MessageTypes.MESSAGE_INITIATOR + data: Uint8ArrayList +} + +export interface CloseReceiverMessage { + id: number + type: MessageTypes.CLOSE_RECEIVER +} + +export interface CloseInitiatorMessage { + id: number + type: MessageTypes.CLOSE_INITIATOR +} + +export interface ResetReceiverMessage { + id: number + type: MessageTypes.RESET_RECEIVER +} + +export interface ResetInitiatorMessage { + id: number + type: MessageTypes.RESET_INITIATOR +} + +export type Message = NewStreamMessage | MessageReceiverMessage | MessageInitiatorMessage | CloseReceiverMessage | CloseInitiatorMessage | ResetReceiverMessage | ResetInitiatorMessage diff --git a/packages/stream-multiplexer-mplex/src/mplex.ts b/packages/stream-multiplexer-mplex/src/mplex.ts new file mode 100644 index 0000000000..7b216711a7 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/mplex.ts @@ -0,0 +1,328 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import { anySignal } from 'any-signal' +import { pushableV } from 'it-pushable' +import { RateLimiterMemory } from 'rate-limiter-flexible' +import { toString as uint8ArrayToString } from 'uint8arrays' +import { Decoder } from './decode.js' +import { encode } from './encode.js' +import { MessageTypes, MessageTypeNames, type Message } from './message-types.js' +import { createStream } from './stream.js' +import type { MplexInit } from './index.js' +import type { Stream } from '@libp2p/interface-connection' +import type { StreamMuxer, StreamMuxerInit } from '@libp2p/interface-stream-muxer' +import type { Sink, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const log = logger('libp2p:mplex') + +const MAX_STREAMS_INBOUND_STREAMS_PER_CONNECTION = 1024 +const MAX_STREAMS_OUTBOUND_STREAMS_PER_CONNECTION = 1024 +const MAX_STREAM_BUFFER_SIZE = 1024 * 1024 * 4 // 4MB +const DISCONNECT_THRESHOLD = 5 + +function printMessage (msg: Message): any { + const output: any = { + ...msg, + type: `${MessageTypeNames[msg.type]} (${msg.type})` + } + + if (msg.type === MessageTypes.NEW_STREAM) { + output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.subarray()) + } + + if (msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.subarray(), 'base16') + } + + return output +} + +export interface MplexStream extends Stream { + sourceReadableLength: () => number + sourcePush: (data: Uint8ArrayList) => void +} + +interface MplexStreamMuxerInit extends MplexInit, StreamMuxerInit {} + +export class MplexStreamMuxer implements StreamMuxer { + public protocol = '/mplex/6.7.0' + + public sink: Sink, Promise> + public source: AsyncGenerator + + private _streamId: number + private readonly _streams: { initiators: Map, receivers: Map } + private readonly _init: MplexStreamMuxerInit + private readonly _source: { push: (val: Message) => void, end: (err?: Error) => void } + private readonly closeController: AbortController + private readonly rateLimiter: RateLimiterMemory + + constructor (init?: MplexStreamMuxerInit) { + init = init ?? {} + + this._streamId = 0 + this._streams = { + /** + * Stream to ids map + */ + initiators: new Map(), + /** + * Stream to ids map + */ + receivers: new Map() + } + this._init = init + + /** + * An iterable sink + */ + this.sink = this._createSink() + + /** + * An iterable source + */ + const source = this._createSource() + this._source = source + this.source = source + + /** + * Close controller + */ + this.closeController = new AbortController() + + this.rateLimiter = new RateLimiterMemory({ + points: init.disconnectThreshold ?? DISCONNECT_THRESHOLD, + duration: 1 + }) + } + + /** + * Returns a Map of streams and their ids + */ + get streams (): Stream[] { + // Inbound and Outbound streams may have the same ids, so we need to make those unique + const streams: Stream[] = [] + for (const stream of this._streams.initiators.values()) { + streams.push(stream) + } + + for (const stream of this._streams.receivers.values()) { + streams.push(stream) + } + return streams + } + + /** + * Initiate a new stream with the given name. If no name is + * provided, the id of the stream will be used. + */ + newStream (name?: string): Stream { + if (this.closeController.signal.aborted) { + throw new Error('Muxer already closed') + } + const id = this._streamId++ + name = name == null ? id.toString() : name.toString() + const registry = this._streams.initiators + return this._newStream({ id, name, type: 'initiator', registry }) + } + + /** + * Close or abort all tracked streams and stop the muxer + */ + close (err?: Error | undefined): void { + if (this.closeController.signal.aborted) return + + if (err != null) { + this.streams.forEach(s => { s.abort(err) }) + } else { + this.streams.forEach(s => { s.close() }) + } + this.closeController.abort() + } + + /** + * Called whenever an inbound stream is created + */ + _newReceiverStream (options: { id: number, name: string }): MplexStream { + const { id, name } = options + const registry = this._streams.receivers + return this._newStream({ id, name, type: 'receiver', registry }) + } + + _newStream (options: { id: number, name: string, type: 'initiator' | 'receiver', registry: Map }): MplexStream { + const { id, name, type, registry } = options + + log('new %s stream %s', type, id) + + if (type === 'initiator' && this._streams.initiators.size === (this._init.maxOutboundStreams ?? MAX_STREAMS_OUTBOUND_STREAMS_PER_CONNECTION)) { + throw new CodeError('Too many outbound streams open', 'ERR_TOO_MANY_OUTBOUND_STREAMS') + } + + if (registry.has(id)) { + throw new Error(`${type} stream ${id} already exists!`) + } + + const send = (msg: Message): void => { + if (log.enabled) { + log.trace('%s stream %s send', type, id, printMessage(msg)) + } + + this._source.push(msg) + } + + const onEnd = (): void => { + log('%s stream with id %s and protocol %s ended', type, id, stream.stat.protocol) + registry.delete(id) + + if (this._init.onStreamEnd != null) { + this._init.onStreamEnd(stream) + } + } + + const stream = createStream({ id, name, send, type, onEnd, maxMsgSize: this._init.maxMsgSize }) + registry.set(id, stream) + return stream + } + + /** + * Creates a sink with an abortable source. Incoming messages will + * also have their size restricted. All messages will be varint decoded. + */ + _createSink (): Sink, Promise> { + const sink: Sink, Promise> = async source => { + const signal = anySignal([this.closeController.signal, this._init.signal]) + + try { + source = abortableSource(source, signal) + + const decoder = new Decoder(this._init.maxMsgSize, this._init.maxUnprocessedMessageQueueSize) + + for await (const chunk of source) { + for (const msg of decoder.write(chunk)) { + await this._handleIncoming(msg) + } + } + + this._source.end() + } catch (err: any) { + log('error in sink', err) + this._source.end(err) // End the source with an error + } finally { + signal.clear() + } + } + + return sink + } + + /** + * Creates a source that restricts outgoing message sizes + * and varint encodes them + */ + _createSource (): any { + const onEnd = (err?: Error): void => { + this.close(err) + } + const source = pushableV({ + objectMode: true, + onEnd + }) + + return Object.assign(encode(source, this._init.minSendBytes), { + push: source.push, + end: source.end, + return: source.return + }) + } + + async _handleIncoming (message: Message): Promise { + const { id, type } = message + + if (log.enabled) { + log.trace('incoming message', printMessage(message)) + } + + // Create a new stream? + if (message.type === MessageTypes.NEW_STREAM) { + if (this._streams.receivers.size === (this._init.maxInboundStreams ?? MAX_STREAMS_INBOUND_STREAMS_PER_CONNECTION)) { + log('too many inbound streams open') + + // not going to allow this stream, send the reset message manually + // instead of setting it up just to tear it down + this._source.push({ + id, + type: MessageTypes.RESET_RECEIVER + }) + + // if we've hit our stream limit, and the remote keeps trying to open + // more new streams, if they are doing this very quickly maybe they + // are attacking us and we should close the connection + try { + await this.rateLimiter.consume('new-stream', 1) + } catch { + log('rate limit hit when opening too many new streams over the inbound stream limit - closing remote connection') + // since there's no backpressure in mplex, the only thing we can really do to protect ourselves is close the connection + this._source.end(new Error('Too many open streams')) + return + } + + return + } + + const stream = this._newReceiverStream({ id, name: uint8ArrayToString(message.data instanceof Uint8Array ? message.data : message.data.subarray()) }) + + if (this._init.onIncomingStream != null) { + this._init.onIncomingStream(stream) + } + + return + } + + const list = (type & 1) === 1 ? this._streams.initiators : this._streams.receivers + const stream = list.get(id) + + if (stream == null) { + log('missing stream %s for message type %s', id, MessageTypeNames[type]) + + return + } + + const maxBufferSize = this._init.maxStreamBufferSize ?? MAX_STREAM_BUFFER_SIZE + + switch (type) { + case MessageTypes.MESSAGE_INITIATOR: + case MessageTypes.MESSAGE_RECEIVER: + if (stream.sourceReadableLength() > maxBufferSize) { + // Stream buffer has got too large, reset the stream + this._source.push({ + id: message.id, + type: type === MessageTypes.MESSAGE_INITIATOR ? MessageTypes.RESET_RECEIVER : MessageTypes.RESET_INITIATOR + }) + + // Inform the stream consumer they are not fast enough + const error = new CodeError('Input buffer full - increase Mplex maxBufferSize to accommodate slow consumers', 'ERR_STREAM_INPUT_BUFFER_FULL') + stream.abort(error) + + return + } + + // We got data from the remote, push it into our local stream + stream.sourcePush(message.data) + break + case MessageTypes.CLOSE_INITIATOR: + case MessageTypes.CLOSE_RECEIVER: + // We should expect no more data from the remote, stop reading + stream.closeRead() + break + case MessageTypes.RESET_INITIATOR: + case MessageTypes.RESET_RECEIVER: + // Stop reading and writing to the stream immediately + stream.reset() + break + default: + log('unknown message type %s', type) + } + } +} diff --git a/packages/stream-multiplexer-mplex/src/stream.ts b/packages/stream-multiplexer-mplex/src/stream.ts new file mode 100644 index 0000000000..14d705ecb9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/src/stream.ts @@ -0,0 +1,71 @@ +import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { MAX_MSG_SIZE } from './decode.js' +import { InitiatorMessageTypes, ReceiverMessageTypes } from './message-types.js' +import type { Message } from './message-types.js' + +export interface Options { + id: number + send: (msg: Message) => void + name?: string + onEnd?: (err?: Error) => void + type?: 'initiator' | 'receiver' + maxMsgSize?: number +} + +interface MplexStreamInit extends AbstractStreamInit { + streamId: number + name: string + send: (msg: Message) => void +} + +class MplexStream extends AbstractStream { + private readonly name: string + private readonly streamId: number + private readonly send: (msg: Message) => void + private readonly types: Record + + constructor (init: MplexStreamInit) { + super(init) + + this.types = init.direction === 'outbound' ? InitiatorMessageTypes : ReceiverMessageTypes + this.send = init.send + this.name = init.name + this.streamId = init.streamId + } + + sendNewStream (): void { + this.send({ id: this.streamId, type: InitiatorMessageTypes.NEW_STREAM, data: new Uint8ArrayList(uint8ArrayFromString(this.name)) }) + } + + sendData (data: Uint8ArrayList): void { + this.send({ id: this.streamId, type: this.types.MESSAGE, data }) + } + + sendReset (): void { + this.send({ id: this.streamId, type: this.types.RESET }) + } + + sendCloseWrite (): void { + this.send({ id: this.streamId, type: this.types.CLOSE }) + } + + sendCloseRead (): void { + // mplex does not support close read, only close write + } +} + +export function createStream (options: Options): MplexStream { + const { id, name, send, onEnd, type = 'initiator', maxMsgSize = MAX_MSG_SIZE } = options + + return new MplexStream({ + id: type === 'initiator' ? (`i${id}`) : `r${id}`, + streamId: id, + name: `${name == null ? id : name}`, + direction: type === 'initiator' ? 'outbound' : 'inbound', + maxDataSize: maxMsgSize, + onEnd, + send + }) +} diff --git a/packages/stream-multiplexer-mplex/test/coder.spec.ts b/packages/stream-multiplexer-mplex/test/coder.spec.ts new file mode 100644 index 0000000000..af0b6cb225 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/coder.spec.ts @@ -0,0 +1,94 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import all from 'it-all' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { encode } from '../src/encode.js' +import { decode } from './fixtures/decode.js' +import { messageWithBytes } from './fixtures/utils.js' +import type { Message, NewStreamMessage } from '../src/message-types.js' + +describe('coder', () => { + it('should encode header', async () => { + const source: Message[][] = [[{ id: 17, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('17')) }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + const expectedHeader = uint8ArrayFromString('880102', 'base16') + expect(data.slice(0, expectedHeader.length)).to.equalBytes(expectedHeader) + }) + + it('should decode header', async () => { + const source = [uint8ArrayFromString('8801023137', 'base16')] + for await (const msg of decode()(source)) { + expect(messageWithBytes(msg)).to.be.deep.equal({ id: 17, type: 0, data: uint8ArrayFromString('17') }) + } + }) + + it('should encode several msgs into buffer', async () => { + const source: Message[][] = [[ + { id: 17, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('17')) }, + { id: 19, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('19')) }, + { id: 21, type: 0, data: new Uint8ArrayList(uint8ArrayFromString('21')) } + ]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes(uint8ArrayFromString('88010231379801023139a801023231', 'base16')) + }) + + it('should encode from Uint8ArrayList', async () => { + const source: NewStreamMessage[][] = [[{ + id: 17, + type: 0, + data: new Uint8ArrayList( + uint8ArrayFromString(Math.random().toString()), + uint8ArrayFromString(Math.random().toString()) + ) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes( + uint8ArrayConcat([ + uint8ArrayFromString('8801', 'base16'), + Uint8Array.from([source[0][0].data.length]), + source[0][0].data instanceof Uint8Array ? source[0][0].data : source[0][0].data.slice() + ]) + ) + }) + + it('should decode msgs from buffer', async () => { + const source = [uint8ArrayFromString('88010231379801023139a801023231', 'base16')] + + const res = [] + for await (const msg of decode()(source)) { + res.push(msg) + } + + expect(res.map(messageWithBytes)).to.deep.equal([ + { id: 17, type: 0, data: uint8ArrayFromString('17') }, + { id: 19, type: 0, data: uint8ArrayFromString('19') }, + { id: 21, type: 0, data: uint8ArrayFromString('21') } + ]) + }) + + it('should encode zero length body msg', async () => { + const source: Message[][] = [[{ id: 17, type: 0 }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + expect(data).to.equalBytes(uint8ArrayFromString('880100', 'base16')) + }) + + it('should decode zero length body msg', async () => { + const source = [uint8ArrayFromString('880100', 'base16')] + + for await (const msg of decode()(source)) { + expect(messageWithBytes(msg)).to.be.eql({ id: 17, type: 0, data: new Uint8Array(0) }) + } + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/compliance.spec.ts b/packages/stream-multiplexer-mplex/test/compliance.spec.ts new file mode 100644 index 0000000000..3162211646 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/compliance.spec.ts @@ -0,0 +1,16 @@ +/* eslint-env mocha */ + +import tests from '@libp2p/interface-stream-muxer-compliance-tests' +import { mplex } from '../src/index.js' + +describe('compliance', () => { + tests({ + async setup () { + return mplex({ + maxInboundStreams: Infinity, + disconnectThreshold: Infinity + })() + }, + async teardown () {} + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/fixtures/decode.ts b/packages/stream-multiplexer-mplex/test/fixtures/decode.ts new file mode 100644 index 0000000000..a050d4fd2a --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/fixtures/decode.ts @@ -0,0 +1,19 @@ +/* eslint-env mocha */ + +import { Decoder, MAX_MSG_QUEUE_SIZE, MAX_MSG_SIZE } from '../../src/decode.js' +import type { Message } from '../../src/message-types.js' +import type { Source } from 'it-stream-types' + +export function decode (maxMessageSize: number = MAX_MSG_SIZE, maxUnprocessedMessageQueueSize: number = MAX_MSG_QUEUE_SIZE) { + return async function * decodeMessages (source: Source): Source { + const decoder = new Decoder(maxMessageSize, maxUnprocessedMessageQueueSize) + + for await (const chunk of source) { + const msgs = decoder.write(chunk) + + if (msgs.length > 0) { + yield * msgs + } + } + } +} diff --git a/packages/stream-multiplexer-mplex/test/fixtures/utils.ts b/packages/stream-multiplexer-mplex/test/fixtures/utils.ts new file mode 100644 index 0000000000..edc3d1ed27 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/fixtures/utils.ts @@ -0,0 +1,18 @@ +import { type Message, MessageTypes } from '../../src/message-types.js' + +export type MessageWithBytes = { + [k in keyof Message]: Message[k] +} & { + data: Uint8Array +} + +export function messageWithBytes (msg: Message): Message | MessageWithBytes { + if (msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + return { + ...msg, + data: msg.data.slice() // convert Uint8ArrayList to Uint8Array + } + } + + return msg +} diff --git a/packages/stream-multiplexer-mplex/test/mplex.spec.ts b/packages/stream-multiplexer-mplex/test/mplex.spec.ts new file mode 100644 index 0000000000..d6e9a63bb9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/mplex.spec.ts @@ -0,0 +1,227 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import { pushable } from 'it-pushable' +import pDefer from 'p-defer' +import { Uint8ArrayList } from 'uint8arraylist' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { encode } from '../src/encode.js' +import { mplex } from '../src/index.js' +import { type CloseInitiatorMessage, type Message, type MessageInitiatorMessage, MessageTypes, type NewStreamMessage } from '../src/message-types.js' +import { decode } from './fixtures/decode.js' +import type { Source } from 'it-stream-types' + +describe('mplex', () => { + it('should restrict number of initiator streams per connection', async () => { + const maxOutboundStreams = 10 + const factory = mplex({ + maxOutboundStreams + })() + const muxer = factory.createStreamMuxer() + + // max out the streams for this connection + for (let i = 0; i < maxOutboundStreams; i++) { + await muxer.newStream() + } + + // open one more + await expect((async () => { + await muxer.newStream() + })()).eventually.be.rejected + .with.property('code', 'ERR_TOO_MANY_OUTBOUND_STREAMS') + }) + + it('should restrict number of recipient streams per connection', async () => { + const maxInboundStreams = 10 + const factory = mplex({ + maxInboundStreams, + disconnectThreshold: Infinity + })() + const muxer = factory.createStreamMuxer() + const stream = pushable() + + // max out the streams for this connection + for (let i = 0; i < maxInboundStreams; i++) { + const source: NewStreamMessage[][] = [[{ + id: i, + type: 0, + data: new Uint8ArrayList(uint8ArrayFromString('17')) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + stream.push(data) + } + + // simulate a new incoming stream + const source: NewStreamMessage[][] = [[{ + id: 11, + type: 0, + data: new Uint8ArrayList(uint8ArrayFromString('17')) + }]] + + const data = uint8ArrayConcat(await all(encode(source))) + + stream.push(data) + stream.end() + + const bufs: Uint8Array[] = [] + const sinkDone = pDefer() + + void Promise.resolve().then(async () => { + for await (const buf of muxer.source) { + bufs.push(buf) + } + sinkDone.resolve() + }) + + await muxer.sink(stream) + await sinkDone.promise + + const messages = await all(decode()(bufs)) + + expect(messages).to.have.nested.property('[0].id', 11, 'Did not specify the correct stream id') + expect(messages).to.have.nested.property('[0].type', MessageTypes.RESET_RECEIVER, 'Did not reset the stream that tipped us over the inbound stream limit') + }) + + it('should reset a stream that fills the message buffer', async () => { + let sent = 0 + const streamSourceError = pDefer() + const maxStreamBufferSize = 1024 * 1024 // 1MB + const id = 17 + + // simulate a new incoming stream that sends lots of data + const input: Source = (async function * send () { + const newStreamMessage: NewStreamMessage = { + id, + type: MessageTypes.NEW_STREAM, + data: new Uint8ArrayList(new Uint8Array(1024)) + } + yield [newStreamMessage] + + await delay(10) + + for (let i = 0; i < 100; i++) { + const dataMessage: MessageInitiatorMessage = { + id, + type: MessageTypes.MESSAGE_INITIATOR, + data: new Uint8ArrayList(new Uint8Array(1024 * 1000)) + } + yield [dataMessage] + + sent++ + + await delay(10) + } + + await delay(10) + + const closeMessage: CloseInitiatorMessage = { + id, + type: MessageTypes.CLOSE_INITIATOR + } + yield [closeMessage] + })() + + // create the muxer + const factory = mplex({ + maxStreamBufferSize + })() + const muxer = factory.createStreamMuxer({ + onIncomingStream () { + // do nothing with the stream so the buffer fills up + }, + onStreamEnd (stream) { + void all(stream.source) + .then(() => { + streamSourceError.reject(new Error('Stream source did not error')) + }) + .catch(err => { + // should have errored before all 102 messages were sent + expect(sent).to.be.lessThan(10) + streamSourceError.resolve(err) + }) + } + }) + + // collect outgoing mplex messages + const muxerFinished = pDefer() + let messages: Message[] = [] + void Promise.resolve().then(async () => { + messages = await all(decode()(muxer.source)) + muxerFinished.resolve() + }) + + // the muxer processes the messages + await muxer.sink(encode(input)) + + // source should have errored with appropriate code + const err = await streamSourceError.promise + expect(err).to.have.property('code', 'ERR_STREAM_INPUT_BUFFER_FULL') + + // should have sent reset message to peer for this stream + await muxerFinished.promise + expect(messages).to.have.nested.property('[0].id', id) + expect(messages).to.have.nested.property('[0].type', MessageTypes.RESET_RECEIVER) + }) + + it('should batch bytes to send', async () => { + const minSendBytes = 10 + + // input bytes, smaller than batch size + const input: Uint8Array[] = [ + Uint8Array.from([0, 1, 2, 3, 4]), + Uint8Array.from([0, 1, 2, 3, 4]), + Uint8Array.from([0, 1, 2, 3, 4]) + ] + + // create the muxer + const factory = mplex({ + minSendBytes + })() + const muxer = factory.createStreamMuxer({}) + + // collect outgoing mplex messages + const muxerFinished = pDefer() + let output: Uint8Array[] = [] + void Promise.resolve().then(async () => { + output = await all(muxer.source) + muxerFinished.resolve() + }) + + // create a stream + const stream = await muxer.newStream() + const streamFinished = pDefer() + // send messages over the stream + void Promise.resolve().then(async () => { + await stream.sink(async function * () { + yield * input + }()) + stream.close() + streamFinished.resolve() + }) + + // wait for all data to be sent over the stream + await streamFinished.promise + + // close the muxer + await muxer.sink([]) + + // wait for all output to be collected + await muxerFinished.promise + + // last message is unbatched + const closeMessage = output.pop() + expect(closeMessage).to.have.lengthOf(2) + + // all other messages should be above or equal to the batch size + expect(output).to.have.lengthOf(2) + for (const buf of output) { + expect(buf).to.have.length.that.is.at.least(minSendBytes) + } + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts b/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts new file mode 100644 index 0000000000..ba23704537 --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/restrict-size.spec.ts @@ -0,0 +1,125 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import randomBytes from 'iso-random-stream/src/random.js' +import all from 'it-all' +import drain from 'it-drain' +import each from 'it-foreach' +import { pipe } from 'it-pipe' +import toBuffer from 'it-to-buffer' +import { Uint8ArrayList } from 'uint8arraylist' +import { encode } from '../src/encode.js' +import { type Message, MessageTypes } from '../src/message-types.js' +import { decode } from './fixtures/decode.js' + +describe('restrict size', () => { + it('should throw when size is too big', async () => { + const maxSize = 32 + + const input: Message[][] = [ + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(8)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(maxSize)) }], + [{ id: 0, type: 1, data: new Uint8ArrayList(randomBytes(64)) }] + ] + + const output: Message[] = [] + + try { + await pipe( + input, + encode, + decode(maxSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_TOO_BIG') + expect(output).to.have.length(3) + expect(output[0]).to.deep.equal(input[0][0]) + expect(output[1]).to.deep.equal(input[1][0]) + expect(output[2]).to.deep.equal(input[2][0]) + return + } + throw new Error('did not restrict size') + }) + + it('should allow message with no data property', async () => { + const message: Message = { + id: 4, + type: MessageTypes.CLOSE_RECEIVER + } + const input: Message[][] = [[message]] + + const output = await pipe( + input, + encode, + decode(32), + async (source) => all(source) + ) + expect(output).to.deep.equal(input[0]) + }) + + it('should throw when unprocessed message queue size is too big', async () => { + const maxMessageSize = 32 + const maxUnprocessedMessageQueueSize = 64 + + const input: Message[][] = [[ + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) }, + { id: 0, type: 1, data: new Uint8ArrayList(randomBytes(16)) } + ]] + + const output: Message[] = [] + + try { + await pipe( + input, + encode, + async function * (source) { + // make one big buffer + yield toBuffer(source) + }, + decode(maxMessageSize, maxUnprocessedMessageQueueSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_QUEUE_TOO_BIG') + expect(output).to.have.length(0) + return + } + throw new Error('did not restrict size') + }) + + it('should throw when unprocessed message queue size is too big because of garbage', async () => { + const maxMessageSize = 32 + const maxUnprocessedMessageQueueSize = 64 + const input = randomBytes(maxUnprocessedMessageQueueSize + 1) + const output: Message[] = [] + + try { + await pipe( + [input], + decode(maxMessageSize, maxUnprocessedMessageQueueSize), + (source) => each(source, chunk => { + output.push(chunk) + }), + async (source) => { await drain(source) } + ) + } catch (err: any) { + expect(err).to.have.property('code', 'ERR_MSG_QUEUE_TOO_BIG') + expect(output).to.have.length(0) + return + } + throw new Error('did not restrict size') + }) +}) diff --git a/packages/stream-multiplexer-mplex/test/stream.spec.ts b/packages/stream-multiplexer-mplex/test/stream.spec.ts new file mode 100644 index 0000000000..ee6e97e2dd --- /dev/null +++ b/packages/stream-multiplexer-mplex/test/stream.spec.ts @@ -0,0 +1,613 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import * as cborg from 'cborg' +import randomBytes from 'iso-random-stream/src/random.js' +import drain from 'it-drain' +import each from 'it-foreach' +import map from 'it-map' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import randomInt from 'random-int' +import { Uint8ArrayList } from 'uint8arraylist' +import { fromString as uint8ArrayFromString } from 'uint8arrays' +import { MessageTypes, MessageTypeNames } from '../src/message-types.js' +import { createStream } from '../src/stream.js' +import { messageWithBytes } from './fixtures/utils.js' +import type { Message } from '../src/message-types.js' +import type { MplexStream } from '../src/mplex.js' + +function randomInput (min = 1, max = 100): Uint8ArrayList[] { + return Array.from(Array(randomInt(min, max)), () => new Uint8ArrayList(randomBytes(randomInt(1, 128)))) +} + +function expectMsgType (actual: keyof typeof MessageTypeNames, expected: keyof typeof MessageTypeNames): void { + expect(MessageTypeNames[actual]).to.equal(MessageTypeNames[expected]) +} + +function echoedMessage (message: Message): Message { + if (message.type !== MessageTypes.MESSAGE_RECEIVER) { + throw new Error('Message was not a receiver message') + } + + return bufferToMessage(message.data.slice()) +} + +function expectMessages (messages: Message[], codes: Array): void { + messages.slice(0, codes.length).forEach((msg, index) => { + expect(msg).to.have.property('type', codes[index]) + + if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes([index - 1]) + } + }) +} + +function expectEchoedMessages (messages: Message[], codes: Array): void { + expectMessages(messages.slice(0, codes.length).map(echoedMessage), codes) +} + +const msgToBuffer = (msg: Message): Uint8ArrayList => { + const m: any = { + ...msg + } + + if (msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) { + m.data = msg.data.slice() + } + + return new Uint8ArrayList(cborg.encode(m)) +} + +const bufferToMessage = (buf: Uint8Array | Uint8ArrayList): Message => cborg.decode(buf.subarray()) + +interface onMessage { + (msg: Message, initator: MplexStream, receiver: MplexStream): void +} + +export interface StreamPair { + initiatorMessages: Message[] + receiverMessages: Message[] +} + +async function streamPair (n: number, onInitiatorMessage?: onMessage, onReceiverMessage?: onMessage): Promise { + const receiverMessages: Message[] = [] + const initiatorMessages: Message[] = [] + const id = 5 + + const mockInitiatorSend = (msg: Message): void => { + initiatorMessages.push(msg) + + if (onInitiatorMessage != null) { + onInitiatorMessage(msg, initiator, receiver) + } + + receiver.sourcePush(msgToBuffer(msg)) + } + const mockReceiverSend = (msg: Message): void => { + receiverMessages.push(msg) + + if (onReceiverMessage != null) { + onReceiverMessage(msg, initiator, receiver) + } + + initiator.sourcePush(msgToBuffer(msg)) + } + const initiator = createStream({ id, send: mockInitiatorSend, type: 'initiator' }) + const receiver = createStream({ id, send: mockReceiverSend, type: 'receiver' }) + const input = new Array(n).fill(0).map((_, i) => new Uint8ArrayList(Uint8Array.from([i]))) + + void pipe( + receiver, + source => each(source, buf => { + const msg = bufferToMessage(buf) + + // when the initiator sends a CLOSE message, we call close + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + receiver.closeRead() + } + + // when the initiator sends a RESET message, we call close + if (msg.type === MessageTypes.RESET_INITIATOR) { + receiver.reset() + } + }), + receiver + ).catch(() => {}) + + try { + await pipe( + input, + initiator, + (source) => map(source, buf => { + const msg: Message = bufferToMessage(buf) + + // when the receiver sends a CLOSE message, we call close + if (msg.type === MessageTypes.CLOSE_RECEIVER) { + initiator.close() + } + + // when the receiver sends a RESET message, we call close + if (msg.type === MessageTypes.RESET_RECEIVER) { + initiator.reset() + } + }), + drain + ) + } catch { + + } + + return { + receiverMessages, + initiatorMessages + } +} + +describe('stream', () => { + it('should initiate stream with NEW_STREAM message', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const stream = createStream({ id, send: mockSend }) + const input = randomInput() + + await pipe(input, stream) + + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + expect(messageWithBytes(msgs[0])).to.have.property('data').that.equalBytes(uint8ArrayFromString(id.toString())) + }) + + it('should initiate named stream with NEW_STREAM message', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const stream = createStream({ id, name, send: mockSend }) + const input = randomInput() + + await pipe(input, stream) + + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + expect(messageWithBytes(msgs[0])).to.have.property('data').that.equalBytes(uint8ArrayFromString(name)) + }) + + it('should end a stream when it is aborted', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const deferred = defer() + const stream = createStream({ id, name, onEnd: deferred.resolve, send: mockSend }) + + const error = new Error('boom') + stream.abort(error) + + const err = await deferred.promise + expect(err).to.equal(error) + }) + + it('should end a stream when it is reset', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const deferred = defer() + const stream = createStream({ id, name, onEnd: deferred.resolve, send: mockSend }) + + stream.reset() + + const err = await deferred.promise + expect(err).to.exist() + expect(err).to.have.property('code', 'ERR_STREAM_RESET') + }) + + it('should send data with MESSAGE_INITIATOR messages if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const input = randomInput() + + await pipe(input, stream) + + // First and last should be NEW_STREAM and CLOSE + const dataMsgs = msgs.slice(1, -1) + expect(dataMsgs).have.length(input.length) + + dataMsgs.forEach((msg, i) => { + expect(msg.id).to.equal(id) + expectMsgType(msg.type, MessageTypes.MESSAGE_INITIATOR) + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes(input[i].subarray()) + }) + }) + + it('should send data with MESSAGE_RECEIVER messages if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const input = randomInput() + + await pipe(input, stream) + + // Last should be CLOSE + const dataMsgs = msgs.slice(0, -1) + expect(dataMsgs).have.length(input.length) + + dataMsgs.forEach((msg, i) => { + expect(msg.id).to.equal(id) + expectMsgType(msg.type, MessageTypes.MESSAGE_RECEIVER) + expect(messageWithBytes(msg)).to.have.property('data').that.equalBytes(input[i].subarray()) + }) + }) + + it('should close stream with CLOSE_INITIATOR message if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const input = randomInput() + + await pipe(input, stream) + + const closeMsg = msgs[msgs.length - 1] + + expect(closeMsg.id).to.equal(id) + expectMsgType(closeMsg.type, MessageTypes.CLOSE_INITIATOR) + expect(closeMsg).to.not.have.property('data') + }) + + it('should close stream with CLOSE_RECEIVER message if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const input = randomInput() + + await pipe(input, stream) + + const closeMsg = msgs[msgs.length - 1] + + expect(closeMsg.id).to.equal(id) + expectMsgType(closeMsg.type, MessageTypes.CLOSE_RECEIVER) + expect(closeMsg).to.not.have.property('data') + }) + + it('should reset stream on error with RESET_INITIATOR message if stream initiator', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'initiator' }) + const error = new Error(`Boom ${Date.now()}`) + const input = { + [Symbol.iterator]: function * () { + for (let i = 0; i < randomInt(1, 10); i++) { + yield new Uint8ArrayList(randomBytes(randomInt(1, 128))) + } + throw error + } + } + + await expect(pipe(input, stream)).to.eventually.be + .rejected.with.property('message', error.message) + + const resetMsg = msgs[msgs.length - 1] + + expect(resetMsg.id).to.equal(id) + expectMsgType(resetMsg.type, MessageTypes.RESET_INITIATOR) + expect(resetMsg).to.not.have.property('data') + }) + + it('should reset stream on error with RESET_RECEIVER message if stream receiver', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = id.toString() + const stream = createStream({ id, name, send: mockSend, type: 'receiver' }) + const error = new Error(`Boom ${Date.now()}`) + const input = { + [Symbol.iterator]: function * () { + for (let i = 0; i < randomInt(1, 10); i++) { + yield new Uint8ArrayList(randomBytes(randomInt(1, 128))) + } + throw error + } + } + + await expect(pipe(input, stream)).to.eventually.be.rejected + .with.property('message', error.message) + + const resetMsg = msgs[msgs.length - 1] + + expect(resetMsg.id).to.equal(id) + expectMsgType(resetMsg.type, MessageTypes.RESET_RECEIVER) + expect(resetMsg).to.not.have.property('data') + }) + + it('should close for reading (remote close)', async () => { + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength) + + // 1x NEW_STREAM, dataLength x MESSAGE_INITIATOR 1x CLOSE_INITIATOR + expect(initiatorMessages).to.have.lengthOf(1 + dataLength + 1) + expectMessages(initiatorMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.CLOSE_INITIATOR + ]) + + // all the initiator messages plus CLOSE_RECEIVER + expect(receiverMessages).to.have.lengthOf(8) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.MESSAGE_INITIATOR, + MessageTypes.CLOSE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.CLOSE_RECEIVER) + }) + + it('should close for reading and writing (abort on local error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, (initiatorMessage, initiator) => { + messages++ + + if (messages === maxMsgs) { + initiator.abort(error) + } + }) + + expect(initiatorMessages).to.have.lengthOf(3) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.RESET_INITIATOR) + + // Reset after two messages + expect(receiverMessages).to.have.lengthOf(2) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + }) + + it('should close for reading and writing (abort on remote error)', async () => { + const maxMsgs = 4 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, (initiatorMessage, initiator, recipient) => { + messages++ + + if (messages === maxMsgs) { + recipient.abort(error) + } + }) + + // All messages sent to recipient + expect(initiatorMessages).to.have.lengthOf(7) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[3]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[4]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[5]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[6]).to.have.property('type', MessageTypes.CLOSE_INITIATOR) + + // Recipient reset after two messages + expect(receiverMessages).to.have.lengthOf(3) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.RESET_RECEIVER) + }) + + it('should close immediately for reading and writing (reset on local error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, () => { + messages++ + + if (messages === maxMsgs) { + throw error + } + }) + + expect(initiatorMessages).to.have.lengthOf(3) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.RESET_INITIATOR) + + // Reset after two messages + expect(receiverMessages).to.have.lengthOf(1) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM + ]) + }) + + it('should close immediately for reading and writing (reset on remote error)', async () => { + const maxMsgs = 2 + const error = new Error(`Boom ${Date.now()}`) + let messages = 0 + + const dataLength = 5 + const { + initiatorMessages, + receiverMessages + } = await streamPair(dataLength, () => {}, () => { + messages++ + + if (messages === maxMsgs) { + throw error + } + }) + + // All messages sent to recipient + expect(initiatorMessages).to.have.lengthOf(7) + expect(initiatorMessages[0]).to.have.property('type', MessageTypes.NEW_STREAM) + expect(initiatorMessages[1]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[2]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[3]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[4]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[5]).to.have.property('type', MessageTypes.MESSAGE_INITIATOR) + expect(initiatorMessages[6]).to.have.property('type', MessageTypes.CLOSE_INITIATOR) + + // Recipient reset after two messages + expect(receiverMessages).to.have.lengthOf(3) + expectEchoedMessages(receiverMessages, [ + MessageTypes.NEW_STREAM, + MessageTypes.MESSAGE_INITIATOR + ]) + expect(receiverMessages[receiverMessages.length - 1]).to.have.property('type', MessageTypes.RESET_RECEIVER) + }) + + it('should call onEnd only when both sides have closed', async () => { + const send = (msg: Message): void => { + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + // simulate remote closing connection + stream.closeRead() + } else if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + stream.sourcePush(msgToBuffer(msg)) + } + } + const id = randomInt(1000) + const name = id.toString() + const deferred = defer() + const onEnd = (err?: any): void => { err != null ? deferred.reject(err) : deferred.resolve() } + const stream = createStream({ id, name, send, onEnd }) + const input = randomInput() + + void pipe( + input, + stream, + drain + ) + + await deferred.promise + }) + + it('should call onEnd with error for local error', async () => { + const send = (): void => { + throw new Error(`Local boom ${Date.now()}`) + } + const id = randomInt(1000) + const deferred = defer() + const onEnd = (err?: any): void => { err != null ? deferred.reject(err) : deferred.resolve() } + const stream = createStream({ id, send, onEnd }) + const input = randomInput() + + pipe( + input, + stream, + drain + ).catch(() => {}) + + await expect(deferred.promise).to.eventually.be.rejectedWith(/Local boom/) + }) + + it('should split writes larger than max message size', async () => { + const messages: Message[] = [] + + const send = (msg: Message): void => { + if (msg.type === MessageTypes.CLOSE_INITIATOR) { + stream.closeRead() + } else if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + messages.push(msg) + } + } + const maxMsgSize = 10 + const id = randomInt(1000) + const stream = createStream({ id, send, maxMsgSize }) + + await pipe( + [ + new Uint8ArrayList(new Uint8Array(maxMsgSize * 2)) + ], + stream, + drain + ) + + expect(messages.length).to.equal(2) + expect(messages[0]).to.have.nested.property('data.length', maxMsgSize) + expect(messages[1]).to.have.nested.property('data.length', maxMsgSize) + }) + + it('should error on double-sink', async () => { + const send = (): void => {} + const id = randomInt(1000) + const stream = createStream({ id, send }) + + // first sink is ok + await stream.sink([]) + + // cannot sink twice + await expect(stream.sink([])) + .to.eventually.be.rejected.with.property('code', 'ERR_DOUBLE_SINK') + }) + + it('should chunk really big messages', async () => { + const msgs: Message[] = [] + const mockSend = (msg: Message): void => { msgs.push(msg) } + const id = randomInt(1000) + const name = `STREAM${Date.now()}` + const maxMsgSize = 10 + const stream = createStream({ id, name, send: mockSend, maxMsgSize }) + const input = [ + new Uint8Array(1024).map(() => randomInt(0, 255)) + ] + const output = new Uint8ArrayList() + + await pipe(input, stream) + + expect(msgs).to.have.lengthOf(105) + expect(msgs[0].id).to.equal(id) + expectMsgType(msgs[0].type, MessageTypes.NEW_STREAM) + + for (let i = 1; i < msgs.length - 1; i++) { + const msg = msgs[i] + expectMsgType(msg.type, MessageTypes.MESSAGE_INITIATOR) + + if (msg.type === MessageTypes.MESSAGE_INITIATOR) { + output.append(msg.data) + } + } + + expectMsgType(msgs[msgs.length - 1].type, MessageTypes.CLOSE_INITIATOR) + expect(output.subarray()).to.equalBytes(input[0]) + }) +}) diff --git a/packages/stream-multiplexer-mplex/tsconfig.json b/packages/stream-multiplexer-mplex/tsconfig.json new file mode 100644 index 0000000000..5eeae73ed9 --- /dev/null +++ b/packages/stream-multiplexer-mplex/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-stream-muxer-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/topology/CHANGELOG.md b/packages/topology/CHANGELOG.md new file mode 100644 index 0000000000..bad4141262 --- /dev/null +++ b/packages/topology/CHANGELOG.md @@ -0,0 +1,248 @@ +## [4.0.3](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.2...v4.0.3) (2023-06-15) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.8 ([#31](https://github.com/libp2p/js-libp2p-topology/issues/31)) ([628f1a8](https://github.com/libp2p/js-libp2p-topology/commit/628f1a8618411dff87c9283292da0e43f95d57d1)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.1...v4.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([944bcae](https://github.com/libp2p/js-libp2p-topology/commit/944bcae65d709ac70f986df403e367cb898f7700)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([8d3e9af](https://github.com/libp2p/js-libp2p-topology/commit/8d3e9afa5925856b5ad89e46d87aa944ff7678d3)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([aae4c14](https://github.com/libp2p/js-libp2p-topology/commit/aae4c146b0fa310920a229867ac188a89cb5b951)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([eed0e46](https://github.com/libp2p/js-libp2p-topology/commit/eed0e462b9c9d9dd51adffd9755f1bb7fa911432)) +* Update .github/workflows/stale.yml [skip ci] ([fdc316d](https://github.com/libp2p/js-libp2p-topology/commit/fdc316d09807aeb69df9c63810255a249c311bad)) + + +### Dependencies + +* bump it-all from 2.0.1 to 3.0.1 ([#32](https://github.com/libp2p/js-libp2p-topology/issues/32)) ([d2db182](https://github.com/libp2p/js-libp2p-topology/commit/d2db18262c918db5a61a946a3b42a406031d4861)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-topology/compare/v4.0.0...v4.0.1) (2023-01-13) + + +### Dependencies + +* remove err-code ([#18](https://github.com/libp2p/js-libp2p-topology/issues/18)) ([70cc52b](https://github.com/libp2p/js-libp2p-topology/commit/70cc52b0da883fba9288fab1de362d3f71707f28)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.2...v4.0.0) (2023-01-06) + + +### ⚠ BREAKING CHANGES + +* update to multiformats v11 (#17) + +### Bug Fixes + +* update to multiformats v11 ([#17](https://github.com/libp2p/js-libp2p-topology/issues/17)) ([e7d9c91](https://github.com/libp2p/js-libp2p-topology/commit/e7d9c91e1c0697ec885828c661e874dc15bd0919)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.1...v3.0.2) (2022-12-16) + + +### Documentation + +* publish api docs ([#16](https://github.com/libp2p/js-libp2p-topology/issues/16)) ([285caba](https://github.com/libp2p/js-libp2p-topology/commit/285caba282d75cb9156a298c7a1b6463d58e3064)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-topology/compare/v3.0.0...v3.0.1) (2022-09-21) + + +### Bug Fixes + +* remove unused deps and update project config ([#9](https://github.com/libp2p/js-libp2p-topology/issues/9)) ([2aa138f](https://github.com/libp2p/js-libp2p-topology/commit/2aa138f4784662ada8f82304d2b50858a0b0e5ba)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ce9f715](https://github.com/libp2p/js-libp2p-topology/commit/ce9f71582680514156031f75fa00e8925d12b85e)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v2.0.0...v3.0.0) (2022-06-16) + + +### ⚠ BREAKING CHANGES + +* registrar API has changed + +### Trivial Changes + +* update deps ([#6](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/6)) ([ad2330b](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/ad2330be33f4bf5ba91ef67f92eb23109504fb0a)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v1.1.9...v2.0.0) (2022-06-14) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#2](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/2)) ([201ade5](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/201ade5b8ce2b8233d267f71a5ffd685110a4115)) + +### [1.1.9](https://github.com/libp2p/js-libp2p-pubsub-topology/compare/v1.1.8...v1.1.9) (2022-06-09) + + +### Trivial Changes + +* update readme ([#1](https://github.com/libp2p/js-libp2p-pubsub-topology/issues/1)) ([f1cfdc6](https://github.com/libp2p/js-libp2p-pubsub-topology/commit/f1cfdc67808ae2ec6fa01a54f96d708129b25371)) + +## [@libp2p/topology-v1.1.8](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.7...@libp2p/topology-v1.1.8) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/topology-v1.1.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.6...@libp2p/topology-v1.1.7) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/topology-v1.1.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.5...@libp2p/topology-v1.1.6) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/topology-v1.1.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.4...@libp2p/topology-v1.1.5) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/topology-v1.1.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.3...@libp2p/topology-v1.1.4) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/topology-v1.1.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.2...@libp2p/topology-v1.1.3) (2022-02-11) + + +### Bug Fixes + +* simpler topologies ([#164](https://github.com/libp2p/js-libp2p-interfaces/issues/164)) ([45fcaa1](https://github.com/libp2p/js-libp2p-interfaces/commit/45fcaa10a6a3215089340ff2eff117d7fd1100e7)) + +## [@libp2p/topology-v1.1.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.1...@libp2p/topology-v1.1.2) (2022-02-10) + + +### Bug Fixes + +* make registrar simpler ([#163](https://github.com/libp2p/js-libp2p-interfaces/issues/163)) ([d122f3d](https://github.com/libp2p/js-libp2p-interfaces/commit/d122f3daaccc04039d90814960da92b513265644)) + +## [@libp2p/topology-v1.1.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.1.0...@libp2p/topology-v1.1.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## [@libp2p/topology-v1.1.0](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.3...@libp2p/topology-v1.1.0) (2022-02-09) + + +### Features + +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) + +## [@libp2p/topology-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.2...@libp2p/topology-v1.0.3) (2022-01-15) + + +### Trivial Changes + +* update project config ([#149](https://github.com/libp2p/js-libp2p-interfaces/issues/149)) ([6eb8556](https://github.com/libp2p/js-libp2p-interfaces/commit/6eb85562c0da167d222808da10a7914daf12970b)) + +## [@libp2p/topology-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/topology-v1.0.1...@libp2p/topology-v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#141](https://github.com/libp2p/js-libp2p-interfaces/issues/141)) ([5f0de59](https://github.com/libp2p/js-libp2p-interfaces/commit/5f0de59136b6343d2411abb2d6a4dd2cd0b7efe4)) +* update package versions ([#140](https://github.com/libp2p/js-libp2p-interfaces/issues/140)) ([cd844f6](https://github.com/libp2p/js-libp2p-interfaces/commit/cd844f6e39f4ee50d006e86eac8dadf696900eb5)) + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.2.0 (2022-01-04) + + +### chore + +* update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) + + +### Features + +* add auto-publish ([7aede5d](https://github.com/libp2p/js-libp2p-interfaces/commit/7aede5df39ea6b5f243348ec9a212b3e33c16a81)) +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) +* split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) +* update package names ([#133](https://github.com/libp2p/js-libp2p-interfaces/issues/133)) ([337adc9](https://github.com/libp2p/js-libp2p-interfaces/commit/337adc9a9bc0278bdae8cbce9c57d07a83c8b5c2)) + + +### BREAKING CHANGES + +* requires node 15+ +* not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out + + + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.3.0...libp2p-topology@0.3.1) (2022-01-02) + +**Note:** Version bump only for package libp2p-topology + + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.2.0...libp2p-topology@0.3.0) (2022-01-02) + + +### Features + +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) + + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-interfaces/compare/libp2p-topology@0.1.0...libp2p-topology@0.2.0) (2021-12-02) + + +### chore + +* update libp2p-crypto and peer-id ([c711e8b](https://github.com/libp2p/js-libp2p-interfaces/commit/c711e8bd4d606f6974b13fad2eeb723f93cebb87)) + + +### BREAKING CHANGES + +* requires node 15+ + + + + + +# 0.1.0 (2021-11-22) + + +### Features + +* split out code, convert to typescript ([#111](https://github.com/libp2p/js-libp2p-interfaces/issues/111)) ([e174bba](https://github.com/libp2p/js-libp2p-interfaces/commit/e174bba889388269b806643c79a6b53c8d6a0f8c)), closes [#110](https://github.com/libp2p/js-libp2p-interfaces/issues/110) [#101](https://github.com/libp2p/js-libp2p-interfaces/issues/101) + + +### BREAKING CHANGES + +* not all fields from concrete classes have been added to the interfaces, some adjustment may be necessary as this gets rolled out diff --git a/packages/topology/LICENSE b/packages/topology/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/topology/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/topology/LICENSE-APACHE b/packages/topology/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/topology/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/topology/LICENSE-MIT b/packages/topology/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/topology/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/topology/README.md b/packages/topology/README.md new file mode 100644 index 0000000000..915f0bc473 --- /dev/null +++ b/packages/topology/README.md @@ -0,0 +1,45 @@ +# @libp2p/topology + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> libp2p network topology + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/topology +``` + +## Usage + +```javascript +import { createTopology } from '@libp2p/topology' + +const topology = createTopology({ ... }) +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/topology/package.json b/packages/topology/package.json new file mode 100644 index 0000000000..055834a6bd --- /dev/null +++ b/packages/topology/package.json @@ -0,0 +1,74 @@ +{ + "name": "@libp2p/topology", + "version": "4.0.3", + "description": "libp2p network topology", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/topology#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "interface", + "libp2p" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./multicodec-topology": { + "types": "./dist/src/multicodec-topology.d.ts", + "import": "./dist/src/multicodec-topology.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build" + }, + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0" + }, + "devDependencies": { + "aegir": "^39.0.10" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/topology/src/index.ts b/packages/topology/src/index.ts new file mode 100644 index 0000000000..d81878b728 --- /dev/null +++ b/packages/topology/src/index.ts @@ -0,0 +1,49 @@ +import { topologySymbol as symbol } from '@libp2p/interface-registrar' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Topology, TopologyInit, onConnectHandler, onDisconnectHandler, Registrar } from '@libp2p/interface-registrar' + +const noop = (): void => {} + +class TopologyImpl implements Topology { + public min: number + public max: number + + /** + * Set of peers that support the protocol + */ + public peers: Set + public onConnect: onConnectHandler + public onDisconnect: onDisconnectHandler + + protected registrar: Registrar | undefined + + constructor (init: TopologyInit) { + this.min = init.min ?? 0 + this.max = init.max ?? Infinity + this.peers = new Set() + + this.onConnect = init.onConnect ?? noop + this.onDisconnect = init.onDisconnect ?? noop + } + + get [Symbol.toStringTag] (): string { + return symbol.toString() + } + + readonly [symbol] = true + + async setRegistrar (registrar: Registrar): Promise { + this.registrar = registrar + } + + /** + * Notify about peer disconnected event + */ + disconnect (peerId: PeerId): void { + this.onDisconnect(peerId) + } +} + +export function createTopology (init: TopologyInit): Topology { + return new TopologyImpl(init) +} diff --git a/packages/topology/tsconfig.json b/packages/topology/tsconfig.json new file mode 100644 index 0000000000..c1017faac2 --- /dev/null +++ b/packages/topology/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-registrar" + } + ] +} diff --git a/packages/tracked-map/.gitignore b/packages/tracked-map/.gitignore new file mode 100644 index 0000000000..1531bdf9de --- /dev/null +++ b/packages/tracked-map/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.docs +.coverage +package-lock.json +yarn.lock diff --git a/packages/tracked-map/CHANGELOG.md b/packages/tracked-map/CHANGELOG.md new file mode 100644 index 0000000000..4d32eb15c7 --- /dev/null +++ b/packages/tracked-map/CHANGELOG.md @@ -0,0 +1,139 @@ +## [3.0.3](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.2...v3.0.3) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6447152](https://github.com/libp2p/js-libp2p-tracked-map/commit/6447152525c274482d6f8e78ef857e68b57d96bd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([80163d7](https://github.com/libp2p/js-libp2p-tracked-map/commit/80163d7b950106a653441171f7fc455980935f12)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5fc1905](https://github.com/libp2p/js-libp2p-tracked-map/commit/5fc1905f868da61cdc6f75d5ef108653eb20072e)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([cb4e22d](https://github.com/libp2p/js-libp2p-tracked-map/commit/cb4e22de6b601c52fbab09e384173590e2ecc0d8)) +* Update .github/workflows/stale.yml [skip ci] ([bda0a65](https://github.com/libp2p/js-libp2p-tracked-map/commit/bda0a656569ae5a6e47cf2d58fa83916d4b2af19)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#30](https://github.com/libp2p/js-libp2p-tracked-map/issues/30)) ([42a4d8b](https://github.com/libp2p/js-libp2p-tracked-map/commit/42a4d8be71ea05c435895cf3f139d3fbfee95708)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.1...v3.0.2) (2022-12-16) + + +### Documentation + +* update docs ([6ad441d](https://github.com/libp2p/js-libp2p-tracked-map/commit/6ad441df9be1a516a17d2c175218f11af204e559)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-tracked-map/compare/v3.0.0...v3.0.1) (2022-12-16) + + +### Documentation + +* publish api docs ([#20](https://github.com/libp2p/js-libp2p-tracked-map/issues/20)) ([61446e1](https://github.com/libp2p/js-libp2p-tracked-map/commit/61446e15a05988ab3e18eafeac0adf9cb153414f)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.2...v3.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires @libp2p/interface-metrics v4 + +### Bug Fixes + +* update @libp2p/interface-metrics to v4 ([#12](https://github.com/libp2p/js-libp2p-tracked-map/issues/12)) ([6a97551](https://github.com/libp2p/js-libp2p-tracked-map/commit/6a97551c3c3c718c63f7ba3102b83b7ac1e08d8f)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([fb388de](https://github.com/libp2p/js-libp2p-tracked-map/commit/fb388de359071a8c66ae8e17cca14f06a878aa20)) +* update project config ([#13](https://github.com/libp2p/js-libp2p-tracked-map/issues/13)) ([e58d7df](https://github.com/libp2p/js-libp2p-tracked-map/commit/e58d7dfefe80daebee042b4a5925f847ae6ded09)) + +## [2.0.2](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.1...v2.0.2) (2022-08-12) + + +### Dependencies + +* update interface deps ([#9](https://github.com/libp2p/js-libp2p-tracked-map/issues/9)) ([424b9e8](https://github.com/libp2p/js-libp2p-tracked-map/commit/424b9e81db1d1005fcfdfd599f590e7ff1c30a68)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-tracked-map/compare/v2.0.0...v2.0.1) (2022-07-01) + + +### Trivial Changes + +* update @libp2p/interface-metrics ([#7](https://github.com/libp2p/js-libp2p-tracked-map/issues/7)) ([de16bc7](https://github.com/libp2p/js-libp2p-tracked-map/commit/de16bc7776b52b6eee8e5d4a14aa9cff484a21ae)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-tracked-map/compare/v1.0.8...v2.0.0) (2022-06-28) + + +### ⚠ BREAKING CHANGES + +* accepts a @libp2p/interface-metrics ComponentMetricsTracker + +### Bug Fixes + +* update deps ([#5](https://github.com/libp2p/js-libp2p-tracked-map/issues/5)) ([a134743](https://github.com/libp2p/js-libp2p-tracked-map/commit/a1347439d95346f5361ec030e159d53e22402914)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-tracked-map/compare/v1.0.7...v1.0.8) (2022-06-09) + + +### Trivial Changes + +* fix project config ([#1](https://github.com/libp2p/js-libp2p-tracked-map/issues/1)) ([eb3d2ea](https://github.com/libp2p/js-libp2p-tracked-map/commit/eb3d2eaf094d51b4724d040325ac4e7bb3daa74b)) + +## [@libp2p/tracked-map-v1.0.7](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.6...@libp2p/tracked-map-v1.0.7) (2022-05-20) + + +### Bug Fixes + +* update interfaces ([#215](https://github.com/libp2p/js-libp2p-interfaces/issues/215)) ([72e6890](https://github.com/libp2p/js-libp2p-interfaces/commit/72e6890826dadbd6e7cbba5536bde350ca4286e6)) + +## [@libp2p/tracked-map-v1.0.6](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.5...@libp2p/tracked-map-v1.0.6) (2022-05-10) + + +### Trivial Changes + +* **deps:** bump sinon from 13.0.2 to 14.0.0 ([#211](https://github.com/libp2p/js-libp2p-interfaces/issues/211)) ([8859f70](https://github.com/libp2p/js-libp2p-interfaces/commit/8859f70943c0bcdb210f54a338ae901739e5e6f2)) + +## [@libp2p/tracked-map-v1.0.5](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.4...@libp2p/tracked-map-v1.0.5) (2022-04-08) + + +### Bug Fixes + +* swap protobufjs for protons ([#191](https://github.com/libp2p/js-libp2p-interfaces/issues/191)) ([d72b30c](https://github.com/libp2p/js-libp2p-interfaces/commit/d72b30cfca4b9145e0b31db28e8fa3329a180e83)) + + +### Trivial Changes + +* update aegir ([#192](https://github.com/libp2p/js-libp2p-interfaces/issues/192)) ([41c1494](https://github.com/libp2p/js-libp2p-interfaces/commit/41c14941e8b67d6601a90b4d48a2776573d55e60)) + +## [@libp2p/tracked-map-v1.0.4](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.3...@libp2p/tracked-map-v1.0.4) (2022-03-15) + + +### Bug Fixes + +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) + +## [@libp2p/tracked-map-v1.0.3](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.2...@libp2p/tracked-map-v1.0.3) (2022-02-27) + + +### Bug Fixes + +* rename crypto to connection-encrypter ([#179](https://github.com/libp2p/js-libp2p-interfaces/issues/179)) ([d197f55](https://github.com/libp2p/js-libp2p-interfaces/commit/d197f554d7cdadb3b05ed2d6c69fda2c4362b1eb)) + +## [@libp2p/tracked-map-v1.0.2](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.1...@libp2p/tracked-map-v1.0.2) (2022-02-27) + + +### Bug Fixes + +* update package config and add connection gater interface ([#178](https://github.com/libp2p/js-libp2p-interfaces/issues/178)) ([c6079a6](https://github.com/libp2p/js-libp2p-interfaces/commit/c6079a6367f004788062df3e30ad2e26330d947b)) + +## [@libp2p/tracked-map-v1.0.1](https://github.com/libp2p/js-libp2p-interfaces/compare/@libp2p/tracked-map-v1.0.0...@libp2p/tracked-map-v1.0.1) (2022-02-10) + + +### Bug Fixes + +* remove node event emitters ([#161](https://github.com/libp2p/js-libp2p-interfaces/issues/161)) ([221fb6a](https://github.com/libp2p/js-libp2p-interfaces/commit/221fb6a024430dc56288d73d8b8ce1aa88427701)) + +## @libp2p/tracked-map-v1.0.0 (2022-02-05) + + +### Features + +* add tracked-map ([#156](https://github.com/libp2p/js-libp2p-interfaces/issues/156)) ([c17730f](https://github.com/libp2p/js-libp2p-interfaces/commit/c17730f8bca172db85507740eaba81b3cf514d04)) diff --git a/packages/tracked-map/LICENSE b/packages/tracked-map/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/tracked-map/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/tracked-map/LICENSE-APACHE b/packages/tracked-map/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/tracked-map/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/tracked-map/LICENSE-MIT b/packages/tracked-map/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/tracked-map/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/tracked-map/README.md b/packages/tracked-map/README.md new file mode 100644 index 0000000000..0766685453 --- /dev/null +++ b/packages/tracked-map/README.md @@ -0,0 +1,63 @@ +# @libp2p/tracked-map + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Allows tracking of statistics while libp2p is running + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Description + +A map that reports it's size to the libp2p [Metrics](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/metrics#readme) system. + +If metrics are disabled a regular map is used. + +## Example + +```JavaScript +import { trackedMap } from '@libp2p/tracked-map' + +const map = trackedMap({ metrics }) + +map.set('key', 'value') +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/tracked-map/package.json b/packages/tracked-map/package.json new file mode 100644 index 0000000000..427c7da0de --- /dev/null +++ b/packages/tracked-map/package.json @@ -0,0 +1,62 @@ +{ + "name": "@libp2p/tracked-map", + "version": "3.0.3", + "description": "Allows tracking of statistics while libp2p is running", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/tracked-map#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-metrics": "^4.0.0" + }, + "devDependencies": { + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "sinon": "^15.1.0", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/tracked-map/src/index.ts b/packages/tracked-map/src/index.ts new file mode 100644 index 0000000000..6cf3fa322c --- /dev/null +++ b/packages/tracked-map/src/index.ts @@ -0,0 +1,65 @@ +import type { Metric, Metrics } from '@libp2p/interface-metrics' + +export interface TrackedMapInit { + name: string + metrics: Metrics +} + +class TrackedMap extends Map { + private readonly metric: Metric + + constructor (init: TrackedMapInit) { + super() + + const { name, metrics } = init + + this.metric = metrics.registerMetric(name) + this.updateComponentMetric() + } + + set (key: K, value: V): this { + super.set(key, value) + this.updateComponentMetric() + return this + } + + delete (key: K): boolean { + const deleted = super.delete(key) + this.updateComponentMetric() + return deleted + } + + clear (): void { + super.clear() + this.updateComponentMetric() + } + + private updateComponentMetric (): void { + this.metric.update(this.size) + } +} + +export interface CreateTrackedMapInit { + /** + * The metric name to use + */ + name: string + + /** + * A metrics implementation + */ + metrics?: Metrics +} + +export function trackedMap (config: CreateTrackedMapInit): Map { + const { name, metrics } = config + let map: Map + + if (metrics != null) { + map = new TrackedMap({ name, metrics }) + } else { + map = new Map() + } + + return map +} diff --git a/packages/tracked-map/test/index.spec.ts b/packages/tracked-map/test/index.spec.ts new file mode 100644 index 0000000000..4450f41df8 --- /dev/null +++ b/packages/tracked-map/test/index.spec.ts @@ -0,0 +1,93 @@ +import { expect } from 'aegir/chai' +import { stubInterface } from 'sinon-ts' +import { trackedMap } from '../src/index.js' +import type { Metric, Metrics } from '@libp2p/interface-metrics' +import type { SinonStubbedInstance } from 'sinon' + +describe('tracked-map', () => { + let metrics: SinonStubbedInstance + + beforeEach(() => { + metrics = stubInterface() + }) + + it('should return a map with metrics', () => { + const name = 'system_component_metric' + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + const map = trackedMap({ + name, + metrics + }) + + expect(map).to.be.an.instanceOf(Map) + expect(metrics.registerMetric.calledWith(name)).to.be.true() + }) + + it('should return a map without metrics', () => { + const name = 'system_component_metric' + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + const map = trackedMap({ + name + }) + + expect(map).to.be.an.instanceOf(Map) + expect(metrics.registerMetric.called).to.be.false() + }) + + it('should track metrics', () => { + const name = 'system_component_metric' + let value = 0 + let callCount = 0 + + const metric = stubInterface() + // @ts-expect-error the wrong overload is selected + metrics.registerMetric.withArgs(name).returns(metric) + + metric.update.callsFake((v) => { + if (typeof v === 'number') { + value = v + } + + callCount++ + }) + + const map = trackedMap({ + name, + metrics + }) + + expect(map).to.be.an.instanceOf(Map) + expect(callCount).to.equal(1) + + map.set('key1', 'value1') + + expect(value).to.equal(1) + expect(callCount).to.equal(2) + + map.set('key1', 'value2') + + expect(value).to.equal(1) + expect(callCount).to.equal(3) + + map.set('key2', 'value3') + + expect(value).to.equal(2) + expect(callCount).to.equal(4) + + map.delete('key2') + + expect(value).to.equal(1) + expect(callCount).to.equal(5) + + map.clear() + + expect(value).to.equal(0) + expect(callCount).to.equal(6) + }) +}) diff --git a/packages/tracked-map/tsconfig.json b/packages/tracked-map/tsconfig.json new file mode 100644 index 0000000000..c60dfc7151 --- /dev/null +++ b/packages/tracked-map/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-metrics" + } + ] +} diff --git a/packages/transport-tcp/.aegir.js b/packages/transport-tcp/.aegir.js new file mode 100644 index 0000000000..35647b8d49 --- /dev/null +++ b/packages/transport-tcp/.aegir.js @@ -0,0 +1,9 @@ + +export default { + build: { + config: { + platform: 'node' + }, + bundlesizeMax: '31KB' + } +} diff --git a/packages/transport-tcp/CHANGELOG.md b/packages/transport-tcp/CHANGELOG.md new file mode 100644 index 0000000000..67385fe0a7 --- /dev/null +++ b/packages/transport-tcp/CHANGELOG.md @@ -0,0 +1,908 @@ +## [7.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.2...v7.0.3) (2023-06-15) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#279](https://github.com/libp2p/js-libp2p-tcp/issues/279)) ([3ed1235](https://github.com/libp2p/js-libp2p-tcp/commit/3ed12353aa48b5a933f80042846a8f1c2337fa47)) + +## [7.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.1...v7.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6dcfdb3](https://github.com/libp2p/js-libp2p-tcp/commit/6dcfdb304010f7002670549e51834e01559a9cfc)) +* Update .github/workflows/stale.yml [skip ci] ([862af6c](https://github.com/libp2p/js-libp2p-tcp/commit/862af6cc96f2c2bac9821db3af6035e601746913)) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 11.0.3 to 12.0.1 ([#274](https://github.com/libp2p/js-libp2p-tcp/issues/274)) ([147610f](https://github.com/libp2p/js-libp2p-tcp/commit/147610f8053dc504eaccf899ae0099eca622a263)) + +## [7.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v7.0.0...v7.0.1) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-transport from 3.0.0 to 4.0.0 ([#269](https://github.com/libp2p/js-libp2p-tcp/issues/269)) ([1b06059](https://github.com/libp2p/js-libp2p-tcp/commit/1b06059b4dc5164367ebde1194434426ee4893d1)) + +## [7.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.2...v7.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* update stream types (#266) + +### Dependencies + +* update stream types ([#266](https://github.com/libp2p/js-libp2p-tcp/issues/266)) ([d2b69b6](https://github.com/libp2p/js-libp2p-tcp/commit/d2b69b6b08b40e76e0bda1796ebe641cf2ca4c6e)) + +## [6.2.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.1...v6.2.2) (2023-04-17) + + +### Dependencies + +* **dev:** bump it-pipe from 2.0.5 to 3.0.1 ([#261](https://github.com/libp2p/js-libp2p-tcp/issues/261)) ([f8b4bf7](https://github.com/libp2p/js-libp2p-tcp/commit/f8b4bf757e650bda232f1f6f21506212427db9f4)) + +## [6.2.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.2.0...v6.2.1) (2023-04-14) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#260](https://github.com/libp2p/js-libp2p-tcp/issues/260)) ([c0f3ec2](https://github.com/libp2p/js-libp2p-tcp/commit/c0f3ec286b19a9c6cd65731fca3d76c488cbf0a9)) + +## [6.2.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.6...v6.2.0) (2023-04-12) + + +### Features + +* add socket backlog option ([#263](https://github.com/libp2p/js-libp2p-tcp/issues/263)) ([8dba9c7](https://github.com/libp2p/js-libp2p-tcp/commit/8dba9c72e8cb2b5de545a9d1f25eb9988ace4cad)) + +## [6.1.6](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.5...v6.1.6) (2023-04-12) + + +### Bug Fixes + +* on MultiaddrConnection close() only create timer if needed ([#262](https://github.com/libp2p/js-libp2p-tcp/issues/262)) ([3637489](https://github.com/libp2p/js-libp2p-tcp/commit/36374895366eb82e321b021d3768a369c59ab3ba)) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#265](https://github.com/libp2p/js-libp2p-tcp/issues/265)) ([d2ef2d0](https://github.com/libp2p/js-libp2p-tcp/commit/d2ef2d09374b889907d5e9e68ffd566add31bf87)) + +## [6.1.5](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.4...v6.1.5) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#258](https://github.com/libp2p/js-libp2p-tcp/issues/258)) ([97e785f](https://github.com/libp2p/js-libp2p-tcp/commit/97e785f1d9d770d894d283acf532e94f4c479ccd)) + +## [6.1.4](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.3...v6.1.4) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#257](https://github.com/libp2p/js-libp2p-tcp/issues/257)) ([2e8e534](https://github.com/libp2p/js-libp2p-tcp/commit/2e8e53417323d4e9f01d44879e948f449b157b9d)) + +## [6.1.3](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.2...v6.1.3) (2023-03-17) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([05bd31c](https://github.com/libp2p/js-libp2p-tcp/commit/05bd31c1cb224995f474af1541670ce5ca1fed09)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([bbd4c2a](https://github.com/libp2p/js-libp2p-tcp/commit/bbd4c2ac11e61e9da8e9cccf2eae5b70d7908ad4)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([6dd008a](https://github.com/libp2p/js-libp2p-tcp/commit/6dd008ad2b3338c57903f22559ee9f741a7c7a8b)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#256](https://github.com/libp2p/js-libp2p-tcp/issues/256)) ([048e9aa](https://github.com/libp2p/js-libp2p-tcp/commit/048e9aad8167069f0fb579c0710b56942347af67)) + +## [6.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.1...v6.1.2) (2023-02-01) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 8.0.5 to 9.1.1 ([#246](https://github.com/libp2p/js-libp2p-tcp/issues/246)) ([6f01bd1](https://github.com/libp2p/js-libp2p-tcp/commit/6f01bd1859eca7f5eccfb79b459424dd25e210a2)) +* **dev:** bump aegir from 37.12.1 to 38.1.0 ([#243](https://github.com/libp2p/js-libp2p-tcp/issues/243)) ([e7eea28](https://github.com/libp2p/js-libp2p-tcp/commit/e7eea28df2c315a62c8281955bd5cb3a6c3816d0)) + +## [6.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.1.0...v6.1.1) (2023-01-26) + + +### Bug Fixes + +* update log message when socket closes ([#249](https://github.com/libp2p/js-libp2p-tcp/issues/249)) ([f95bbf1](https://github.com/libp2p/js-libp2p-tcp/commit/f95bbf11aa59a184106ea1b1288442160dd0b373)) + +## [6.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.9...v6.1.0) (2023-01-21) + + +### Features + +* close server on maxConnections ([#218](https://github.com/libp2p/js-libp2p-tcp/issues/218)) ([bff54fa](https://github.com/libp2p/js-libp2p-tcp/commit/bff54fa5d40be44f421924e21b11d8c37fc53b1e)) + + +### Bug Fixes + +* specify host explicitly in node tests ([#247](https://github.com/libp2p/js-libp2p-tcp/issues/247)) ([d7e5a69](https://github.com/libp2p/js-libp2p-tcp/commit/d7e5a69d917850b756a0fd47760552e0b2fb0feb)) + +## [6.0.9](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.8...v6.0.9) (2023-01-17) + + +### Bug Fixes + +* increase default socket close timeout ([#242](https://github.com/libp2p/js-libp2p-tcp/issues/242)) ([a64ba41](https://github.com/libp2p/js-libp2p-tcp/commit/a64ba41f485f3bde28b58827c8a2ce5bf94f711a)), closes [#239](https://github.com/libp2p/js-libp2p-tcp/issues/239) + + +### Trivial Changes + +* replace err-code with CodeError ([#240](https://github.com/libp2p/js-libp2p-tcp/issues/240)) ([5c44562](https://github.com/libp2p/js-libp2p-tcp/commit/5c445628e97a1462c64a48253efd9ccb3441c399)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) + +## [6.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.7...v6.0.8) (2022-12-16) + + +### Documentation + +* fix build badge ([#238](https://github.com/libp2p/js-libp2p-tcp/issues/238)) ([8a94ced](https://github.com/libp2p/js-libp2p-tcp/commit/8a94cedc6e8806b597c650209b76b5ce38231146)) + +## [6.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.6...v6.0.7) (2022-12-15) + + +### Bug Fixes + +* publish tsdocs for this module ([#236](https://github.com/libp2p/js-libp2p-tcp/issues/236)) ([b4f88e7](https://github.com/libp2p/js-libp2p-tcp/commit/b4f88e7bfbe865eb00cfb1d99a4231b072b458a5)) + +## [6.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.5...v6.0.6) (2022-12-13) + + +### Bug Fixes + +* remove abortable-iterator and close socket directly on abort ([#220](https://github.com/libp2p/js-libp2p-tcp/issues/220)) ([28fe750](https://github.com/libp2p/js-libp2p-tcp/commit/28fe7500fa99c91f4f81d73671e885955b5d7e4a)) + +## [6.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.4...v6.0.5) (2022-12-06) + + +### Dependencies + +* **dev:** bump sinon from 14.0.2 to 15.0.0 ([#233](https://github.com/libp2p/js-libp2p-tcp/issues/233)) ([72a79ab](https://github.com/libp2p/js-libp2p-tcp/commit/72a79ab81d79daaeb8a77656e98a19b70f132595)) + +## [6.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.3...v6.0.4) (2022-11-22) + + +### Bug Fixes + +* use labels to differentiate interfaces for metrics ([#230](https://github.com/libp2p/js-libp2p-tcp/issues/230)) ([6c4c316](https://github.com/libp2p/js-libp2p-tcp/commit/6c4c316d080cde679c11a784c22284d6e1912b94)) + +## [6.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.2...v6.0.3) (2022-11-22) + + +### Bug Fixes + +* make metrics interface a dep instead of a dev dep ([#231](https://github.com/libp2p/js-libp2p-tcp/issues/231)) ([876ca13](https://github.com/libp2p/js-libp2p-tcp/commit/876ca132aa2b307315148628681cddfa0828b3ac)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.1...v6.0.2) (2022-11-17) + + +### Bug Fixes + +* update metric names to follow prometheus naming guide ([#228](https://github.com/libp2p/js-libp2p-tcp/issues/228)) ([24c5b37](https://github.com/libp2p/js-libp2p-tcp/commit/24c5b37ab64429972f29af6ae4516c18232d1ff3)) + + +### Trivial Changes + +* add test for filtering unix socket address ([#229](https://github.com/libp2p/js-libp2p-tcp/issues/229)) ([efcfbb2](https://github.com/libp2p/js-libp2p-tcp/commit/efcfbb28a77192a489834c8b8ad832337539d62b)), closes [#132](https://github.com/libp2p/js-libp2p-tcp/issues/132) + +## [6.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v6.0.0...v6.0.1) (2022-11-16) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.1 ([#225](https://github.com/libp2p/js-libp2p-tcp/issues/225)) ([a271056](https://github.com/libp2p/js-libp2p-tcp/commit/a271056c8d8d179dd95399f9621d790a0f18b84a)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.2...v6.0.0) (2022-11-05) + + +### ⚠ BREAKING CHANGES + +* requires metrics interface v4 + +### Features + +* add metrics ([#223](https://github.com/libp2p/js-libp2p-tcp/issues/223)) ([c004357](https://github.com/libp2p/js-libp2p-tcp/commit/c0043577777181545eef925b50e28743cfd7a29d)), closes [#217](https://github.com/libp2p/js-libp2p-tcp/issues/217) + +## [5.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.1...v5.0.2) (2022-11-05) + + +### Bug Fixes + +* handle listen error ([#224](https://github.com/libp2p/js-libp2p-tcp/issues/224)) ([4125e9e](https://github.com/libp2p/js-libp2p-tcp/commit/4125e9eaa4d531dbcb0f2777149d1ca8fa9460a5)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v5.0.0...v5.0.1) (2022-10-17) + + +### Trivial Changes + +* **deps-dev:** bump it-all from 1.0.6 to 2.0.0 ([#222](https://github.com/libp2p/js-libp2p-tcp/issues/222)) ([fddebdf](https://github.com/libp2p/js-libp2p-tcp/commit/fddebdff3ab2056da78f9ec665e5005a659e9045)) + +## [5.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v4.1.0...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#219](https://github.com/libp2p/js-libp2p-tcp/issues/219)) ([be2dbc3](https://github.com/libp2p/js-libp2p-tcp/commit/be2dbc3f674e9bce534dc92d93ad2739ed6d2bef)) + +## [4.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.2...v4.1.0) (2022-10-11) + + +### Features + +* add server.maxConnections option ([#213](https://github.com/libp2p/js-libp2p-tcp/issues/213)) ([99e88a4](https://github.com/libp2p/js-libp2p-tcp/commit/99e88a4d3122c46f06f69cfbe3f72a2279e2329f)) + +## [4.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.1...v4.0.2) (2022-10-11) + + +### Bug Fixes + +* port listener to ES6 class syntax ([#214](https://github.com/libp2p/js-libp2p-tcp/issues/214)) ([af7b8e2](https://github.com/libp2p/js-libp2p-tcp/commit/af7b8e2bf48ec0c9f01e087e76bc9570dca05783)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v4.0.0...v4.0.1) (2022-10-07) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.0 ([#216](https://github.com/libp2p/js-libp2p-tcp/issues/216)) ([f224a5a](https://github.com/libp2p/js-libp2p-tcp/commit/f224a5a0e8b497317b2b9410bb60433647a3185a)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.2...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* **deps:** bump @libp2p/interface-transport from 1.0.4 to 2.0.0 (#215) + +### Trivial Changes + +* **deps:** bump @libp2p/interface-transport from 1.0.4 to 2.0.0 ([#215](https://github.com/libp2p/js-libp2p-tcp/issues/215)) ([1adf73d](https://github.com/libp2p/js-libp2p-tcp/commit/1adf73db4e88e0c196766588a2972a3a6e28e69a)) + +## [3.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.1...v3.1.2) (2022-09-24) + + +### Bug Fixes + +* expose extra options from net.connect for dial and listen ([#211](https://github.com/libp2p/js-libp2p-tcp/issues/211)) ([6401a87](https://github.com/libp2p/js-libp2p-tcp/commit/6401a87080c77c36cf80634ee3cbe7c1c6fa89bb)) + +## [3.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v3.1.0...v3.1.1) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([5096725](https://github.com/libp2p/js-libp2p-tcp/commit/5096725d84fc0197cf0e055bfd6954a125001b47)) + +## [3.1.0](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.8...v3.1.0) (2022-09-14) + + +### Features + +* Unix domain sockets ([#208](https://github.com/libp2p/js-libp2p-tcp/issues/208)) ([223f79b](https://github.com/libp2p/js-libp2p-tcp/commit/223f79b53091c33d4588b3c5e5adf28bcb929d97)), closes [#132](https://github.com/libp2p/js-libp2p-tcp/issues/132) + +## [3.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.7...v3.0.8) (2022-09-12) + + +### Trivial Changes + +* fix readme example ([#207](https://github.com/libp2p/js-libp2p-tcp/issues/207)) ([9f7cf76](https://github.com/libp2p/js-libp2p-tcp/commit/9f7cf761af41c12ca0a23c7df9b305ae876439f8)) + +## [3.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.6...v3.0.7) (2022-09-12) + + +### Bug Fixes + +* handle address being undefined ([#209](https://github.com/libp2p/js-libp2p-tcp/issues/209)) ([64ed009](https://github.com/libp2p/js-libp2p-tcp/commit/64ed0090ae9d6295496cd69e82b879627ea2069c)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.5...v3.0.6) (2022-09-01) + + +### Bug Fixes + +* add socket keepalive ([#205](https://github.com/libp2p/js-libp2p-tcp/issues/205)) ([9ac799b](https://github.com/libp2p/js-libp2p-tcp/commit/9ac799b0c057d209920997a30ca219ea96000131)), closes [/github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go#L85](https://github.com/libp2p//github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go/issues/L85) [/github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go#L191](https://github.com/libp2p//github.com/libp2p/go-libp2p/blob/master/p2p/transport/tcp/tcp.go/issues/L191) + +## [3.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.4...v3.0.5) (2022-08-31) + + +### Bug Fixes + +* destroy sockets on close ([#204](https://github.com/libp2p/js-libp2p-tcp/issues/204)) ([e8b8f2e](https://github.com/libp2p/js-libp2p-tcp/commit/e8b8f2eaf547640f2566b18a8d061912965f2a55)), closes [#201](https://github.com/libp2p/js-libp2p-tcp/issues/201) + +## [3.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.3...v3.0.4) (2022-08-30) + + +### Bug Fixes + +* add tests for ipv6 wildcard support ([#203](https://github.com/libp2p/js-libp2p-tcp/issues/203)) ([71c974d](https://github.com/libp2p/js-libp2p-tcp/commit/71c974deaf40a4ab2eabc7d3e404d865a6892b20)), closes [#100](https://github.com/libp2p/js-libp2p-tcp/issues/100) + +## [3.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.2...v3.0.3) (2022-08-10) + + +### Bug Fixes + +* update all deps ([#199](https://github.com/libp2p/js-libp2p-tcp/issues/199)) ([e3b1344](https://github.com/libp2p/js-libp2p-tcp/commit/e3b13441199ef84912d8ba14f7e42235313b12d6)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.1...v3.0.2) (2022-07-13) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 2.1.0 to 3.0.1 ([#193](https://github.com/libp2p/js-libp2p-tcp/issues/193)) ([0571339](https://github.com/libp2p/js-libp2p-tcp/commit/0571339387dad55739fa2d831d39d5165620ecbb)) +* **deps:** bump @libp2p/utils from 2.0.1 to 3.0.0 ([#192](https://github.com/libp2p/js-libp2p-tcp/issues/192)) ([0c757ff](https://github.com/libp2p/js-libp2p-tcp/commit/0c757ff95b13bdf1ffb3285fa4b10d0988e8e42b)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v3.0.0...v3.0.1) (2022-06-27) + + +### Bug Fixes + +* add explicit return type to dial ([#191](https://github.com/libp2p/js-libp2p-tcp/issues/191)) ([cdb0932](https://github.com/libp2p/js-libp2p-tcp/commit/cdb09323188e77dee06a186ae43ba544faea5f86)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v2.0.1...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* the registry API has changed + +### Trivial Changes + +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#187](https://github.com/libp2p/js-libp2p-tcp/issues/187)) ([fa59390](https://github.com/libp2p/js-libp2p-tcp/commit/fa593907832f19b77f86e9ae45389fee3d6228f8)) +* update deps ([#189](https://github.com/libp2p/js-libp2p-tcp/issues/189)) ([719f3f5](https://github.com/libp2p/js-libp2p-tcp/commit/719f3f55dc932afdf7d83c9d9a2cbf43146002a2)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v2.0.0...v2.0.1) (2022-06-16) + + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#186](https://github.com/libp2p/js-libp2p-tcp/issues/186)) ([5de2104](https://github.com/libp2p/js-libp2p-tcp/commit/5de2104f187e9f414b52fe2fee7580b78f2050af)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.11...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#184](https://github.com/libp2p/js-libp2p-tcp/issues/184)) ([2924414](https://github.com/libp2p/js-libp2p-tcp/commit/2924414995356ce179da315fde8fda0958959e2d)) + +### [1.0.11](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.10...v1.0.11) (2022-05-23) + + +### Bug Fixes + +* update interfaces ([#182](https://github.com/libp2p/js-libp2p-tcp/issues/182)) ([4ce3476](https://github.com/libp2p/js-libp2p-tcp/commit/4ce34767fe29355a5ff0c8069caa33b923ea9966)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.9...v1.0.10) (2022-05-23) + + +### Trivial Changes + +* **deps-dev:** bump sinon from 13.0.2 to 14.0.0 ([#179](https://github.com/libp2p/js-libp2p-tcp/issues/179)) ([ed4c6bd](https://github.com/libp2p/js-libp2p-tcp/commit/ed4c6bd4ed8b9b01e86cbb5de0206b77683f1b93)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.8...v1.0.9) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#178](https://github.com/libp2p/js-libp2p-tcp/issues/178)) ([2b1e875](https://github.com/libp2p/js-libp2p-tcp/commit/2b1e8751493aefd3b9149e660357f73377ab5d0b)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.7...v1.0.8) (2022-04-07) + + +### Trivial Changes + +* update aegir ([#177](https://github.com/libp2p/js-libp2p-tcp/issues/177)) ([f54d6b4](https://github.com/libp2p/js-libp2p-tcp/commit/f54d6b4f631f26b248231330b4b8f512996625b3)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.6...v1.0.7) (2022-04-05) + + +### Trivial Changes + +* update README to the latest changes ([#169](https://github.com/libp2p/js-libp2p-tcp/issues/169)) ([129bf19](https://github.com/libp2p/js-libp2p-tcp/commit/129bf1958be3c2001b1f84646b6a7ccba6e20b52)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.5...v1.0.6) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#172](https://github.com/libp2p/js-libp2p-tcp/issues/172)) ([d72f629](https://github.com/libp2p/js-libp2p-tcp/commit/d72f629f7f89853bd849fb9123a99a407140d977)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.4...v1.0.5) (2022-02-21) + + +### Bug Fixes + +* update to latest interfaces ([#170](https://github.com/libp2p/js-libp2p-tcp/issues/170)) ([d8840e8](https://github.com/libp2p/js-libp2p-tcp/commit/d8840e881d5025b5c704eeb461a7e78685e53a87)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.3...v1.0.4) (2022-02-10) + + +### Bug Fixes + +* update to latest interfaces ([#168](https://github.com/libp2p/js-libp2p-tcp/issues/168)) ([c19462f](https://github.com/libp2p/js-libp2p-tcp/commit/c19462fccd2f8f1038530a0b3aea834fc9aab04d)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.2...v1.0.3) (2022-02-06) + + +### Bug Fixes + +* publish from root dir ([#167](https://github.com/libp2p/js-libp2p-tcp/issues/167)) ([4be1369](https://github.com/libp2p/js-libp2p-tcp/commit/4be13699a40e0b825c464e27ae3a6e6743e45c0e)) +* update to latest interfaces ([#164](https://github.com/libp2p/js-libp2p-tcp/issues/164)) ([7edad3c](https://github.com/libp2p/js-libp2p-tcp/commit/7edad3c4ae31d2bcc2ccebe2b5597da246c733c7)) + + +### Trivial Changes + +* update readme ([#166](https://github.com/libp2p/js-libp2p-tcp/issues/166)) ([2a0b609](https://github.com/libp2p/js-libp2p-tcp/commit/2a0b6098bb4739758e66f15a81ac42c922ea57b5)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.1...v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add dependabot ([#154](https://github.com/libp2p/js-libp2p-tcp/issues/154)) ([02974b3](https://github.com/libp2p/js-libp2p-tcp/commit/02974b3e41a1618a9ffb8e4a03edecc1c7fcd1f5)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-tcp/compare/v1.0.0...v1.0.1) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#155](https://github.com/libp2p/js-libp2p-tcp/issues/155)) ([def9ad7](https://github.com/libp2p/js-libp2p-tcp/commit/def9ad759d39da21639358b06bd847ab30b3cb7b)) + +## [0.17.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.17.1...v0.17.2) (2021-09-03) + + +### Bug Fixes + +* ts declaration export ([#150](https://github.com/libp2p/js-libp2p-tcp/issues/150)) ([d165fe5](https://github.com/libp2p/js-libp2p-tcp/commit/d165fe57960e2bb4a5324c372ef459c31dee1cf5)) + + + +## [0.17.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.17.0...v0.17.1) (2021-07-08) + + + +# [0.17.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.16.0...v0.17.0) (2021-07-07) + + +### chore + +* update deps ([#147](https://github.com/libp2p/js-libp2p-tcp/issues/147)) ([b3e315a](https://github.com/libp2p/js-libp2p-tcp/commit/b3e315a6988cd4be7978e8922f275e525463bc0c)) + + +### BREAKING CHANGES + +* uses new majors of multiaddr and mafmt + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.4...v0.16.0) (2021-06-10) + + +### Features + +* add types ([#145](https://github.com/libp2p/js-libp2p-tcp/issues/145)) ([3249e02](https://github.com/libp2p/js-libp2p-tcp/commit/3249e0292b2ef5d818fe428ce61f689b25060d85)) + + + +## [0.15.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.2...v0.15.4) (2021-04-12) + + +### Bug Fixes + +* hanging close promise ([#140](https://github.com/libp2p/js-libp2p-tcp/issues/140)) ([3813100](https://github.com/libp2p/js-libp2p-tcp/commit/381310043852a9213f1abb62f5f0a7046d806286)) + + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.2...v0.15.3) (2021-02-03) + + +### Bug Fixes + +* hanging close promise ([#140](https://github.com/libp2p/js-libp2p-tcp/issues/140)) ([3813100](https://github.com/libp2p/js-libp2p-tcp/commit/3813100)) + + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.2...v0.15.2) (2020-12-28) + + +### Bug Fixes + +* catch error from maConn.close ([#128](https://github.com/libp2p/js-libp2p-tcp/issues/128)) ([0fe0815](https://github.com/libp2p/js-libp2p-tcp/commit/0fe0815)) +* catch thrown maConn errors in listener ([#122](https://github.com/libp2p/js-libp2p-tcp/issues/122)) ([86db568](https://github.com/libp2p/js-libp2p-tcp/commit/86db568)), closes [#121](https://github.com/libp2p/js-libp2p-tcp/issues/121) +* intermittent error when asking for interfaces ([#137](https://github.com/libp2p/js-libp2p-tcp/issues/137)) ([af9804e](https://github.com/libp2p/js-libp2p-tcp/commit/af9804e)) +* remove use of assert module ([#123](https://github.com/libp2p/js-libp2p-tcp/issues/123)) ([6272876](https://github.com/libp2p/js-libp2p-tcp/commit/6272876)) +* transport should not handle connection if upgradeInbound throws ([#119](https://github.com/libp2p/js-libp2p-tcp/issues/119)) ([21f8747](https://github.com/libp2p/js-libp2p-tcp/commit/21f8747)) + + +### Chores + +* update deps ([#134](https://github.com/libp2p/js-libp2p-tcp/issues/134)) ([d9f9912](https://github.com/libp2p/js-libp2p-tcp/commit/d9f9912)) + + +### BREAKING CHANGES + +* - The multiaddr dep used by this module returns Uint8Arrays and may + not be compatible with previous versions + +* chore: update utils + +* chore: remove gh dep url + + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.15.0...v0.15.1) (2020-08-11) + + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.6...v0.15.0) (2020-08-07) + + +### Chores + +* update deps ([#134](https://github.com/libp2p/js-libp2p-tcp/issues/134)) ([d9f9912](https://github.com/libp2p/js-libp2p-tcp/commit/d9f9912)) + + +### BREAKING CHANGES + +* - The multiaddr dep used by this module returns Uint8Arrays and may + not be compatible with previous versions + +* chore: update utils + +* chore: remove gh dep url + + + + +## [0.14.6](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.5...v0.14.6) (2020-07-17) + + + + +## [0.14.5](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.4...v0.14.5) (2020-04-28) + + +### Bug Fixes + +* catch error from maConn.close ([#128](https://github.com/libp2p/js-libp2p-tcp/issues/128)) ([0fe0815](https://github.com/libp2p/js-libp2p-tcp/commit/0fe0815)) + + + + +## [0.14.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.3...v0.14.4) (2020-02-24) + + +### Bug Fixes + +* catch thrown maConn errors in listener ([#122](https://github.com/libp2p/js-libp2p-tcp/issues/122)) ([86db568](https://github.com/libp2p/js-libp2p-tcp/commit/86db568)), closes [#121](https://github.com/libp2p/js-libp2p-tcp/issues/121) +* remove use of assert module ([#123](https://github.com/libp2p/js-libp2p-tcp/issues/123)) ([6272876](https://github.com/libp2p/js-libp2p-tcp/commit/6272876)) + + + + +## [0.14.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.2...v0.14.3) (2019-12-20) + + +### Bug Fixes + +* transport should not handle connection if upgradeInbound throws ([#119](https://github.com/libp2p/js-libp2p-tcp/issues/119)) ([21f8747](https://github.com/libp2p/js-libp2p-tcp/commit/21f8747)) + + + + +## [0.14.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.1...v0.14.2) (2019-12-06) + + +### Bug Fixes + +* **log:** log the bound port and host ([#117](https://github.com/libp2p/js-libp2p-tcp/issues/117)) ([7702646](https://github.com/libp2p/js-libp2p-tcp/commit/7702646)) + + +### Features + +* add path multiaddr support ([#118](https://github.com/libp2p/js-libp2p-tcp/issues/118)) ([d76a1f2](https://github.com/libp2p/js-libp2p-tcp/commit/d76a1f2)) + + + + +## [0.14.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.14.0...v0.14.1) (2019-09-20) + + +### Bug Fixes + +* ensure timeline.close is set ([#113](https://github.com/libp2p/js-libp2p-tcp/issues/113)) ([605ee27](https://github.com/libp2p/js-libp2p-tcp/commit/605ee27)) + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.13.1...v0.14.0) (2019-09-16) + + +### Features + +* change api to async / await ([#112](https://github.com/libp2p/js-libp2p-tcp/issues/112)) ([cf7d1b8](https://github.com/libp2p/js-libp2p-tcp/commit/cf7d1b8)) + + +### BREAKING CHANGES + +* All places in the API that used callbacks are now replaced with async/await. The API has also been updated according to the latest `interface-transport` version, https://github.com/libp2p/interface-transport/tree/v0.6.0#api. + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.13.0...v0.13.1) (2019-08-08) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.12.1...v0.13.0) (2018-09-12) + + +### Features + +* add support for dialing over dns ([eba0b48](https://github.com/libp2p/js-libp2p-tcp/commit/eba0b48)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.12.0...v0.12.1) (2018-07-31) + + +### Bug Fixes + +* invalid ip address and daemon can be crashed by remote user ([4b04b17](https://github.com/libp2p/js-libp2p-tcp/commit/4b04b17)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.6...v0.12.0) (2018-04-05) + + +### Features + +* add class-is module ([ded1f68](https://github.com/libp2p/js-libp2p-tcp/commit/ded1f68)) + + + + +## [0.11.6](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.5...v0.11.6) (2018-02-20) + + + + +## [0.11.5](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.4...v0.11.5) (2018-02-07) + + + + +## [0.11.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.3...v0.11.4) (2018-02-07) + + + + +## [0.11.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.2...v0.11.3) (2018-02-07) + + +### Bug Fixes + +* clearing timeout when closes ([#87](https://github.com/libp2p/js-libp2p-tcp/issues/87)) ([f8f5266](https://github.com/libp2p/js-libp2p-tcp/commit/f8f5266)) + + + + +## [0.11.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.1...v0.11.2) (2018-01-12) + + +### Bug Fixes + +* missing dependency debug, fixes [#84](https://github.com/libp2p/js-libp2p-tcp/issues/84) ([74a88f6](https://github.com/libp2p/js-libp2p-tcp/commit/74a88f6)) + + + + +## [0.11.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.11.0...v0.11.1) (2017-10-13) + + +### Features + +* relay filtering ([11c4f45](https://github.com/libp2p/js-libp2p-tcp/commit/11c4f45)) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.2...v0.11.0) (2017-09-03) + + +### Features + +* p2p addrs situation ([#82](https://github.com/libp2p/js-libp2p-tcp/issues/82)) ([a54bb83](https://github.com/libp2p/js-libp2p-tcp/commit/a54bb83)) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.1...v0.10.2) (2017-07-22) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.10.0...v0.10.1) (2017-04-13) + + +### Bug Fixes + +* catch errors on incomming sockets ([e204517](https://github.com/libp2p/js-libp2p-tcp/commit/e204517)) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.4...v0.10.0) (2017-03-27) + + +### Bug Fixes + +* **dial:** proper error handling on dial ([#77](https://github.com/libp2p/js-libp2p-tcp/issues/77)) ([4d4f295](https://github.com/libp2p/js-libp2p-tcp/commit/4d4f295)) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.3...v0.9.4) (2017-03-21) + + + + +## [0.9.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.2...v0.9.3) (2017-02-09) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.1...v0.9.2) (2017-02-09) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.9.0...v0.9.1) (2016-11-03) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.8.1...v0.9.0) (2016-11-03) + + +### Bug Fixes + +* **deps:** remove unused pull dep ([06689e3](https://github.com/libp2p/js-libp2p-tcp/commit/06689e3)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.8.0...v0.8.1) (2016-09-06) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.4...v0.8.0) (2016-09-06) + + +### Features + +* **deps:** update to published deps ([da8ee21](https://github.com/libp2p/js-libp2p-tcp/commit/da8ee21)) +* **pull:** migration to pull-streams ([5e89a26](https://github.com/libp2p/js-libp2p-tcp/commit/5e89a26)) +* **readme:** add pull-streams documentation ([d9f65e0](https://github.com/libp2p/js-libp2p-tcp/commit/d9f65e0)) + + + + +## [0.7.4](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.3...v0.7.4) (2016-08-03) + + + + +## [0.7.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.2...v0.7.3) (2016-06-26) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.1...v0.7.2) (2016-06-23) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.7.0...v0.7.1) (2016-06-23) + + +### Bug Fixes + +* error was passed in duplicate ([9ac5cca](https://github.com/libp2p/js-libp2p-tcp/commit/9ac5cca)) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.2...v0.7.0) (2016-06-22) + + + + +## [0.6.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.1...v0.6.2) (2016-06-01) + + +### Bug Fixes + +* address cr ([2ed01e8](https://github.com/libp2p/js-libp2p-tcp/commit/2ed01e8)) +* destroy hanging connections after timeout ([4a12169](https://github.com/libp2p/js-libp2p-tcp/commit/4a12169)) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.6.0...v0.6.1) (2016-05-29) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.3...v0.6.0) (2016-05-22) + + + + +## [0.5.3](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.2...v0.5.3) (2016-05-22) + + + + +## [0.5.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.1...v0.5.2) (2016-05-09) + + + + +## [0.5.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.5.0...v0.5.1) (2016-05-08) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.4.0...v0.5.0) (2016-04-25) + + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.3.0...v0.4.0) (2016-03-14) + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.2.1...v0.3.0) (2016-03-10) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.2.0...v0.2.1) (2016-03-04) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.2...v0.2.0) (2016-03-04) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.1...v0.1.2) (2015-10-29) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-tcp/compare/v0.1.0...v0.1.1) (2015-09-17) + + + + +# 0.1.0 (2015-09-16) diff --git a/packages/transport-tcp/LICENSE b/packages/transport-tcp/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-tcp/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-tcp/LICENSE-APACHE b/packages/transport-tcp/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-tcp/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-tcp/LICENSE-MIT b/packages/transport-tcp/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-tcp/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/transport-tcp/README.md b/packages/transport-tcp/README.md new file mode 100644 index 0000000000..83fe40f6fc --- /dev/null +++ b/packages/transport-tcp/README.md @@ -0,0 +1,87 @@ +# @libp2p/tcp + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> A TCP transport for libp2p + +## Table of contents + +- [Install](#install) +- [Usage](#usage) +- [API Docs](#api-docs) +- [License](#license) +- [Contribution](#contribution) + +## Install + +```console +$ npm i @libp2p/tcp +``` + +## Usage + +```js +import { tcp } from '@libp2p/tcp' +import { multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import all from 'it-all' + +// A simple upgrader that just returns the MultiaddrConnection +const upgrader = { + upgradeInbound: async maConn => maConn, + upgradeOutbound: async maConn => maConn +} + +const transport = tcp()() + +const listener = transport.createListener({ + upgrader, + handler: (socket) => { + console.log('new connection opened') + pipe( + ['hello', ' ', 'World!'], + socket + ) + } +}) + +const addr = multiaddr('/ip4/127.0.0.1/tcp/9090') +await listener.listen(addr) +console.log('listening') + +const socket = await transport.dial(addr, { upgrader }) +const values = await pipe( + socket, + all +) +console.log(`Value: ${values.toString()}`) + +// Close connection after reading +await listener.close() +``` + +Outputs: + +```sh +listening +new connection opened +Value: hello World! +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json new file mode 100644 index 0000000000..3169f9a5b3 --- /dev/null +++ b/packages/transport-tcp/package.json @@ -0,0 +1,77 @@ +{ + "name": "@libp2p/tcp", + "version": "7.0.3", + "description": "A TCP transport for libp2p", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-tcp#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS", + "TCP", + "libp2p", + "network", + "p2p", + "peer", + "peer-to-peer" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t node -t electron-main", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "@types/sinon": "^10.0.15", + "stream-to-it": "^0.2.2" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/interface-transport-compliance-tests": "^4.0.0", + "aegir": "^39.0.10", + "it-all": "^3.0.1", + "it-pipe": "^3.0.1", + "p-defer": "^4.0.0", + "sinon": "^15.1.0", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-tcp/src/constants.ts b/packages/transport-tcp/src/constants.ts new file mode 100644 index 0000000000..8402e3a703 --- /dev/null +++ b/packages/transport-tcp/src/constants.ts @@ -0,0 +1,10 @@ +// p2p multi-address code +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 +export const CODE_UNIX = 400 + +// Time to wait for a connection to close gracefully before destroying it manually +export const CLOSE_TIMEOUT = 2000 + +// Close the socket if there is no activity after this long in ms +export const SOCKET_TIMEOUT = 5 * 60000 // 5 mins diff --git a/packages/transport-tcp/src/index.ts b/packages/transport-tcp/src/index.ts new file mode 100644 index 0000000000..71b50114e2 --- /dev/null +++ b/packages/transport-tcp/src/index.ts @@ -0,0 +1,252 @@ +import net from 'net' +import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport' +import { AbortError, CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import * as mafmt from '@multiformats/mafmt' +import { CODE_CIRCUIT, CODE_P2P, CODE_UNIX } from './constants.js' +import { type CloseServerOnMaxConnectionsOpts, TCPListener } from './listener.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { multiaddrToNetConfig } from './utils.js' +import type { Connection } from '@libp2p/interface-connection' +import type { CounterGroup, Metrics } from '@libp2p/interface-metrics' +import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr' +import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net' + +const log = logger('libp2p:tcp') + +export interface TCPOptions { + /** + * An optional number in ms that is used as an inactivity timeout after which the socket will be closed + */ + inboundSocketInactivityTimeout?: number + + /** + * An optional number in ms that is used as an inactivity timeout after which the socket will be closed + */ + outboundSocketInactivityTimeout?: number + + /** + * When closing a socket, wait this long for it to close gracefully before it is closed more forcibly + */ + socketCloseTimeout?: number + + /** + * Set this property to reject connections when the server's connection count gets high. + * https://nodejs.org/api/net.html#servermaxconnections + */ + maxConnections?: number + + /** + * Parameter to specify the maximum length of the queue of pending connections + * https://nodejs.org/dist/latest-v18.x/docs/api/net.html#serverlisten + */ + backlog?: number + + /** + * Close server (stop listening for new connections) if connections exceed a limit. + * Open server (start listening for new connections) if connections fall below a limit. + */ + closeServerOnMaxConnections?: CloseServerOnMaxConnectionsOpts +} + +/** + * Expose a subset of net.connect options + */ +export interface TCPSocketOptions extends AbortOptions { + noDelay?: boolean + keepAlive?: boolean + keepAliveInitialDelay?: number + allowHalfOpen?: boolean +} + +export interface TCPDialOptions extends DialOptions, TCPSocketOptions { + +} + +export interface TCPCreateListenerOptions extends CreateListenerOptions, TCPSocketOptions { + +} + +export interface TCPComponents { + metrics?: Metrics +} + +export interface TCPMetrics { + dialerEvents: CounterGroup +} + +class TCP implements Transport { + private readonly opts: TCPOptions + private readonly metrics?: TCPMetrics + private readonly components: TCPComponents + + constructor (components: TCPComponents, options: TCPOptions = {}) { + this.opts = options + this.components = components + + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', { + label: 'event', + help: 'Total count of TCP dialer events by type' + }) + } + } + } + + readonly [symbol] = true + + readonly [Symbol.toStringTag] = '@libp2p/tcp' + + async dial (ma: Multiaddr, options: TCPDialOptions): Promise { + options.keepAlive = options.keepAlive ?? true + + // options.signal destroys the socket before 'connect' event + const socket = await this._connect(ma, options) + + // Avoid uncaught errors caused by unstable connections + socket.on('error', err => { + log('socket error', err) + }) + + const maConn = toMultiaddrConnection(socket, { + remoteAddr: ma, + socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout, + socketCloseTimeout: this.opts.socketCloseTimeout, + metrics: this.metrics?.dialerEvents + }) + + const onAbort = (): void => { + maConn.close().catch(err => { + log.error('Error closing maConn after abort', err) + }) + } + options.signal?.addEventListener('abort', onAbort, { once: true }) + + log('new outbound connection %s', maConn.remoteAddr) + const conn = await options.upgrader.upgradeOutbound(maConn) + log('outbound connection %s upgraded', maConn.remoteAddr) + + options.signal?.removeEventListener('abort', onAbort) + + if (options.signal?.aborted === true) { + conn.close().catch(err => { + log.error('Error closing conn after abort', err) + }) + + throw new AbortError() + } + + return conn + } + + async _connect (ma: Multiaddr, options: TCPDialOptions): Promise { + if (options.signal?.aborted === true) { + throw new AbortError() + } + + return new Promise((resolve, reject) => { + const start = Date.now() + const cOpts = multiaddrToNetConfig(ma) as (IpcSocketConnectOpts & TcpSocketConnectOpts) + const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}` + + log('dialing %j', cOpts) + const rawSocket = net.connect(cOpts) + + const onError = (err: Error): void => { + err.message = `connection error ${cOptsStr}: ${err.message}` + this.metrics?.dialerEvents.increment({ error: true }) + + done(err) + } + + const onTimeout = (): void => { + log('connection timeout %s', cOptsStr) + this.metrics?.dialerEvents.increment({ timeout: true }) + + const err = new CodeError(`connection timeout after ${Date.now() - start}ms`, 'ERR_CONNECT_TIMEOUT') + // Note: this will result in onError() being called + rawSocket.emit('error', err) + } + + const onConnect = (): void => { + log('connection opened %j', cOpts) + this.metrics?.dialerEvents.increment({ connect: true }) + done() + } + + const onAbort = (): void => { + log('connection aborted %j', cOpts) + this.metrics?.dialerEvents.increment({ abort: true }) + rawSocket.destroy() + done(new AbortError()) + } + + const done = (err?: any): void => { + rawSocket.removeListener('error', onError) + rawSocket.removeListener('timeout', onTimeout) + rawSocket.removeListener('connect', onConnect) + + if (options.signal != null) { + options.signal.removeEventListener('abort', onAbort) + } + + if (err != null) { + reject(err); return + } + + resolve(rawSocket) + } + + rawSocket.on('error', onError) + rawSocket.on('timeout', onTimeout) + rawSocket.on('connect', onConnect) + + if (options.signal != null) { + options.signal.addEventListener('abort', onAbort) + } + }) + } + + /** + * Creates a TCP listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound`. + */ + createListener (options: TCPCreateListenerOptions): Listener { + return new TCPListener({ + ...options, + maxConnections: this.opts.maxConnections, + backlog: this.opts.backlog, + closeServerOnMaxConnections: this.opts.closeServerOnMaxConnections, + socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout, + socketCloseTimeout: this.opts.socketCloseTimeout, + metrics: this.components.metrics + }) + } + + /** + * Takes a list of `Multiaddr`s and returns only valid TCP addresses + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + return multiaddrs.filter(ma => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + if (ma.protoCodes().includes(CODE_UNIX)) { + return true + } + + return mafmt.TCP.matches(ma.decapsulateCode(CODE_P2P)) + }) + } +} + +export function tcp (init: TCPOptions = {}): (components?: TCPComponents) => Transport { + return (components: TCPComponents = {}) => { + return new TCP(components, init) + } +} diff --git a/packages/transport-tcp/src/listener.ts b/packages/transport-tcp/src/listener.ts new file mode 100644 index 0000000000..016f9e4228 --- /dev/null +++ b/packages/transport-tcp/src/listener.ts @@ -0,0 +1,334 @@ +import net from 'net' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { CODE_P2P } from './constants.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { + getMultiaddrs, + multiaddrToNetConfig, + type NetConfig +} from './utils.js' +import type { TCPCreateListenerOptions } from './index.js' +import type { MultiaddrConnection, Connection } from '@libp2p/interface-connection' +import type { CounterGroup, MetricGroup, Metrics } from '@libp2p/interface-metrics' +import type { Upgrader, Listener, ListenerEvents } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:tcp:listener') + +/** + * Attempts to close the given maConn. If a failure occurs, it will be logged + */ +async function attemptClose (maConn: MultiaddrConnection): Promise { + try { + await maConn.close() + } catch (err) { + log.error('an error occurred closing the connection', err) + } +} + +export interface CloseServerOnMaxConnectionsOpts { + /** Server listens once connection count is less than `listenBelow` */ + listenBelow: number + /** Close server once connection count is greater than or equal to `closeAbove` */ + closeAbove: number + onListenError?: (err: Error) => void +} + +interface Context extends TCPCreateListenerOptions { + handler?: (conn: Connection) => void + upgrader: Upgrader + socketInactivityTimeout?: number + socketCloseTimeout?: number + maxConnections?: number + backlog?: number + metrics?: Metrics + closeServerOnMaxConnections?: CloseServerOnMaxConnectionsOpts +} + +const SERVER_STATUS_UP = 1 +const SERVER_STATUS_DOWN = 0 + +export interface TCPListenerMetrics { + status: MetricGroup + errors: CounterGroup + events: CounterGroup +} + +type Status = { started: false } | { + started: true + listeningAddr: Multiaddr + peerId: string | null + netConfig: NetConfig +} + +export class TCPListener extends EventEmitter implements Listener { + private readonly server: net.Server + /** Keep track of open connections to destroy in case of timeout */ + private readonly connections = new Set() + private status: Status = { started: false } + private metrics?: TCPListenerMetrics + private addr: string + + constructor (private readonly context: Context) { + super() + + context.keepAlive = context.keepAlive ?? true + + this.addr = 'unknown' + this.server = net.createServer(context, this.onSocket.bind(this)) + + // https://nodejs.org/api/net.html#servermaxconnections + // If set reject connections when the server's connection count gets high + // Useful to prevent too resource exhaustion via many open connections on high bursts of activity + if (context.maxConnections !== undefined) { + this.server.maxConnections = context.maxConnections + } + + if (context.closeServerOnMaxConnections != null) { + // Sanity check options + if (context.closeServerOnMaxConnections.closeAbove < context.closeServerOnMaxConnections.listenBelow) { + throw Error('closeAbove must be >= listenBelow') + } + } + + this.server + .on('listening', () => { + if (context.metrics != null) { + // we are listening, register metrics for our port + const address = this.server.address() + + if (address == null) { + this.addr = 'unknown' + } else if (typeof address === 'string') { + // unix socket + this.addr = address + } else { + this.addr = `${address.address}:${address.port}` + } + + context.metrics?.registerMetricGroup('libp2p_tcp_inbound_connections_total', { + label: 'address', + help: 'Current active connections in TCP listener', + calculate: () => { + return { + [this.addr]: this.connections.size + } + } + }) + + this.metrics = { + status: context.metrics.registerMetricGroup('libp2p_tcp_listener_status_info', { + label: 'address', + help: 'Current status of the TCP listener socket' + }), + errors: context.metrics.registerMetricGroup('libp2p_tcp_listener_errors_total', { + label: 'address', + help: 'Total count of TCP listener errors by type' + }), + events: context.metrics.registerMetricGroup('libp2p_tcp_listener_events_total', { + label: 'address', + help: 'Total count of TCP listener events by type' + }) + } + + this.metrics?.status.update({ + [this.addr]: SERVER_STATUS_UP + }) + } + + this.dispatchEvent(new CustomEvent('listening')) + }) + .on('error', err => { + this.metrics?.errors.increment({ [`${this.addr} listen_error`]: true }) + this.dispatchEvent(new CustomEvent('error', { detail: err })) + }) + .on('close', () => { + this.metrics?.status.update({ + [this.addr]: SERVER_STATUS_DOWN + }) + this.dispatchEvent(new CustomEvent('close')) + }) + } + + private onSocket (socket: net.Socket): void { + // Avoid uncaught errors caused by unstable connections + socket.on('error', err => { + log('socket error', err) + this.metrics?.events.increment({ [`${this.addr} error`]: true }) + }) + + let maConn: MultiaddrConnection + try { + maConn = toMultiaddrConnection(socket, { + listeningAddr: this.status.started ? this.status.listeningAddr : undefined, + socketInactivityTimeout: this.context.socketInactivityTimeout, + socketCloseTimeout: this.context.socketCloseTimeout, + metrics: this.metrics?.events, + metricPrefix: `${this.addr} ` + }) + } catch (err) { + log.error('inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_to_connection`]: true }) + return + } + + log('new inbound connection %s', maConn.remoteAddr) + try { + this.context.upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection upgraded %s', maConn.remoteAddr) + this.connections.add(maConn) + + socket.once('close', () => { + this.connections.delete(maConn) + + if ( + this.context.closeServerOnMaxConnections != null && + this.connections.size < this.context.closeServerOnMaxConnections.listenBelow + ) { + // The most likely case of error is if the port taken by this application is binded by + // another process during the time the server if closed. In that case there's not much + // we can do. netListen() will be called again every time a connection is dropped, which + // acts as an eventual retry mechanism. onListenError allows the consumer act on this. + this.netListen().catch(e => { + log.error('error attempting to listen server once connection count under limit', e) + this.context.closeServerOnMaxConnections?.onListenError?.(e as Error) + }) + } + }) + + if (this.context.handler != null) { + this.context.handler(conn) + } + + if ( + this.context.closeServerOnMaxConnections != null && + this.connections.size >= this.context.closeServerOnMaxConnections.closeAbove + ) { + this.netClose() + } + + this.dispatchEvent(new CustomEvent('connection', { detail: conn })) + }) + .catch(async err => { + log.error('inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_upgrade`]: true }) + + await attemptClose(maConn) + }) + .catch(err => { + log.error('closing inbound connection failed', err) + }) + } catch (err) { + log.error('inbound connection failed', err) + + attemptClose(maConn) + .catch(err => { + log.error('closing inbound connection failed', err) + this.metrics?.errors.increment({ [`${this.addr} inbound_closing_failed`]: true }) + }) + } + } + + getAddrs (): Multiaddr[] { + if (!this.status.started) { + return [] + } + + let addrs: Multiaddr[] = [] + const address = this.server.address() + const { listeningAddr, peerId } = this.status + + if (address == null) { + return [] + } + + if (typeof address === 'string') { + addrs = [listeningAddr] + } else { + try { + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (listeningAddr.toString().startsWith('/ip4')) { + addrs = addrs.concat(getMultiaddrs('ip4', address.address, address.port)) + } else if (address.family === 'IPv6') { + addrs = addrs.concat(getMultiaddrs('ip6', address.address, address.port)) + } + } catch (err) { + log.error('could not turn %s:%s into multiaddr', address.address, address.port, err) + } + } + + return addrs.map(ma => peerId != null ? ma.encapsulate(`/p2p/${peerId}`) : ma) + } + + async listen (ma: Multiaddr): Promise { + if (this.status.started) { + throw Error('server is already listening') + } + + const peerId = ma.getPeerId() + const listeningAddr = peerId == null ? ma.decapsulateCode(CODE_P2P) : ma + const { backlog } = this.context + + this.status = { + started: true, + listeningAddr, + peerId, + netConfig: multiaddrToNetConfig(listeningAddr, { backlog }) + } + + await this.netListen() + } + + async close (): Promise { + await Promise.all( + Array.from(this.connections.values()).map(async maConn => { await attemptClose(maConn) }) + ) + + // netClose already checks if server.listening + this.netClose() + } + + private async netListen (): Promise { + if (!this.status.started || this.server.listening) { + return + } + + const netConfig = this.status.netConfig + + await new Promise((resolve, reject) => { + // NOTE: 'listening' event is only fired on success. Any error such as port already binded, is emitted via 'error' + this.server.once('error', reject) + this.server.listen(netConfig, resolve) + }) + + log('Listening on %s', this.server.address()) + } + + private netClose (): void { + if (!this.status.started || !this.server.listening) { + return + } + + log('Closing server on %s', this.server.address()) + + // NodeJS implementation tracks listening status with `this._handle` property. + // - Server.close() sets this._handle to null immediately. If this._handle is null, ERR_SERVER_NOT_RUNNING is thrown + // - Server.listening returns `this._handle !== null` https://github.com/nodejs/node/blob/386d761943bb1b217fba27d6b80b658c23009e60/lib/net.js#L1675 + // - Server.listen() if `this._handle !== null` throws ERR_SERVER_ALREADY_LISTEN + // + // NOTE: Both listen and close are technically not async actions, so it's not necessary to track + // states 'pending-close' or 'pending-listen' + + // From docs https://nodejs.org/api/net.html#serverclosecallback + // Stops the server from accepting new connections and keeps existing connections. + // 'close' event is emitted only emitted when all connections are ended. + // The optional callback will be called once the 'close' event occurs. + // + // NOTE: Since we want to keep existing connections and have checked `!this.server.listening` it's not necessary + // to pass a callback to close. + this.server.close() + } +} diff --git a/packages/transport-tcp/src/socket-to-conn.ts b/packages/transport-tcp/src/socket-to-conn.ts new file mode 100644 index 0000000000..993776a831 --- /dev/null +++ b/packages/transport-tcp/src/socket-to-conn.ts @@ -0,0 +1,196 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' +// @ts-expect-error no types +import toIterable from 'stream-to-it' +import { CLOSE_TIMEOUT, SOCKET_TIMEOUT } from './constants.js' +import { multiaddrToNetConfig } from './utils.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { CounterGroup } from '@libp2p/interface-metrics' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Socket } from 'net' + +const log = logger('libp2p:tcp:socket') + +interface ToConnectionOptions { + listeningAddr?: Multiaddr + remoteAddr?: Multiaddr + localAddr?: Multiaddr + socketInactivityTimeout?: number + socketCloseTimeout?: number + metrics?: CounterGroup + metricPrefix?: string +} + +/** + * Convert a socket into a MultiaddrConnection + * https://github.com/libp2p/interface-transport#multiaddrconnection + */ +export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => { + const metrics = options.metrics + const metricPrefix = options.metricPrefix ?? '' + const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT + const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT + + // Check if we are connected on a unix path + if (options.listeningAddr?.getPath() != null) { + options.remoteAddr = options.listeningAddr + } + + if (options.remoteAddr?.getPath() != null) { + options.localAddr = options.remoteAddr + } + + let remoteAddr: Multiaddr + + if (options.remoteAddr != null) { + remoteAddr = options.remoteAddr + } else { + if (socket.remoteAddress == null || socket.remotePort == null) { + // this can be undefined if the socket is destroyed (for example, if the client disconnected) + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketremoteaddress + throw new CodeError('Could not determine remote address or port', 'ERR_NO_REMOTE_ADDRESS') + } + + remoteAddr = toMultiaddr(socket.remoteAddress, socket.remotePort) + } + + const lOpts = multiaddrToNetConfig(remoteAddr) + const lOptsStr = lOpts.path ?? `${lOpts.host ?? ''}:${lOpts.port ?? ''}` + const { sink, source } = toIterable.duplex(socket) + + // by default there is no timeout + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback + socket.setTimeout(inactivityTimeout, () => { + log('%s socket read timeout', lOptsStr) + metrics?.increment({ [`${metricPrefix}timeout`]: true }) + + // only destroy with an error if the remote has not sent the FIN message + let err: Error | undefined + if (socket.readable) { + err = new CodeError('Socket read timeout', 'ERR_SOCKET_READ_TIMEOUT') + } + + // if the socket times out due to inactivity we must manually close the connection + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout + socket.destroy(err) + }) + + socket.once('close', () => { + log('%s socket close', lOptsStr) + metrics?.increment({ [`${metricPrefix}close`]: true }) + + // In instances where `close` was not explicitly called, + // such as an iterable stream ending, ensure we have set the close + // timeline + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + }) + + socket.once('end', () => { + // the remote sent a FIN packet which means no more data will be sent + // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end + log('%s socket end', lOptsStr) + metrics?.increment({ [`${metricPrefix}end`]: true }) + }) + + const maConn: MultiaddrConnection = { + async sink (source) { + try { + await sink(source) + } catch (err: any) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log(err) + } + } + + // we have finished writing, send the FIN message + socket.end() + }, + + source, + + // If the remote address was passed, use it - it may have the peer ID encapsulated + remoteAddr, + + timeline: { open: Date.now() }, + + async close () { + if (socket.destroyed) { + log('%s socket was already destroyed when trying to close', lOptsStr) + return + } + + log('%s closing socket', lOptsStr) + await new Promise((resolve, reject) => { + const start = Date.now() + + let timeout: NodeJS.Timeout | undefined + + socket.once('close', () => { + log('%s socket closed', lOptsStr) + // socket completely closed + if (timeout !== undefined) { + clearTimeout(timeout) + } + resolve() + }) + socket.once('error', (err: Error) => { + log('%s socket error', lOptsStr, err) + + // error closing socket + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + + if (socket.destroyed) { + if (timeout !== undefined) { + clearTimeout(timeout) + } + } + + reject(err) + }) + + // shorten inactivity timeout + socket.setTimeout(closeTimeout) + + // close writable end of the socket + socket.end() + + if (socket.writableLength > 0) { + // Attempt to end the socket. If it takes longer to close than the + // timeout, destroy it manually. + timeout = setTimeout(() => { + if (socket.destroyed) { + log('%s is already destroyed', lOptsStr) + resolve() + } else { + log('%s socket close timeout after %dms, destroying it manually', lOptsStr, Date.now() - start) + + // will trigger 'error' and 'close' events that resolves promise + socket.destroy(new CodeError('Socket close timeout', 'ERR_SOCKET_CLOSE_TIMEOUT')) + } + }, closeTimeout).unref() + // there are outgoing bytes waiting to be sent + socket.once('drain', () => { + log('%s socket drained', lOptsStr) + + // all bytes have been sent we can destroy the socket (maybe) before the timeout + socket.destroy() + }) + } else { + // nothing to send, destroy immediately, no need the timeout + socket.destroy() + } + }) + } + } + + return maConn +} diff --git a/packages/transport-tcp/src/utils.ts b/packages/transport-tcp/src/utils.ts new file mode 100644 index 0000000000..7db5975031 --- /dev/null +++ b/packages/transport-tcp/src/utils.ts @@ -0,0 +1,53 @@ +import os from 'os' +import path from 'path' +import { multiaddr } from '@multiformats/multiaddr' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { ListenOptions, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net' + +const ProtoFamily = { ip4: 'IPv4', ip6: 'IPv6' } + +export type NetConfig = ListenOptions | (IpcSocketConnectOpts & TcpSocketConnectOpts) + +export function multiaddrToNetConfig (addr: Multiaddr, config: NetConfig = {}): NetConfig { + const listenPath = addr.getPath() + + // unix socket listening + if (listenPath != null) { + if (os.platform() === 'win32') { + // Use named pipes on Windows systems. + return { path: path.join('\\\\.\\pipe\\', listenPath) } + } else { + return { path: listenPath } + } + } + + // tcp listening + return { ...addr.toOptions(), ...config } +} + +export function getMultiaddrs (proto: 'ip4' | 'ip6', ip: string, port: number): Multiaddr[] { + const toMa = (ip: string): Multiaddr => multiaddr(`/${proto}/${ip}/tcp/${port}`) + return (isAnyAddr(ip) ? getNetworkAddrs(ProtoFamily[proto]) : [ip]).map(toMa) +} + +export function isAnyAddr (ip: string): boolean { + return ['0.0.0.0', '::'].includes(ip) +} + +const networks = os.networkInterfaces() + +function getNetworkAddrs (family: string): string[] { + const addresses: string[] = [] + + for (const [, netAddrs] of Object.entries(networks)) { + if (netAddrs != null) { + for (const netAddr of netAddrs) { + if (netAddr.family === family) { + addresses.push(netAddr.address) + } + } + } + } + + return addresses +} diff --git a/packages/transport-tcp/test/compliance.spec.ts b/packages/transport-tcp/test/compliance.spec.ts new file mode 100644 index 0000000000..7c6ba85808 --- /dev/null +++ b/packages/transport-tcp/test/compliance.spec.ts @@ -0,0 +1,42 @@ +import net from 'net' +import tests from '@libp2p/interface-transport-compliance-tests' +import { multiaddr } from '@multiformats/multiaddr' +import sinon from 'sinon' +import { tcp } from '../src/index.js' + +describe('interface-transport compliance', () => { + tests({ + async setup () { + const transport = tcp()() + const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/9091'), + multiaddr('/ip4/127.0.0.1/tcp/9092'), + multiaddr('/ip4/127.0.0.1/tcp/9093'), + multiaddr('/ip6/::/tcp/9094') + ] + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay (delayMs: number) { + const netConnect = net.connect + sinon.replace(net, 'connect', (opts: any) => { + const socket = netConnect(opts) + const socketEmit = socket.emit.bind(socket) + sinon.replace(socket, 'emit', (...args: [string]) => { + const time = args[0] === 'connect' ? delayMs : 0 + setTimeout(() => socketEmit(...args), time) + return true + }) + return socket + }) + }, + restore () { + sinon.restore() + } + } + + return { transport, addrs, connector } + }, + async teardown () {} + }) +}) diff --git a/packages/transport-tcp/test/connection.spec.ts b/packages/transport-tcp/test/connection.spec.ts new file mode 100644 index 0000000000..67b1d23597 --- /dev/null +++ b/packages/transport-tcp/test/connection.spec.ts @@ -0,0 +1,91 @@ +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Transport, Upgrader } from '@libp2p/interface-transport' + +describe('valid localAddr and remoteAddr', () => { + let transport: Transport + let upgrader: Upgrader + + beforeEach(() => { + transport = tcp()() + upgrader = mockUpgrader({ + events: new EventEmitter() + }) + }) + + const ma = multiaddr('/ip4/127.0.0.1/tcp/0') + + it('should resolve port 0', async () => { + // Create a Promise that resolves when a connection is handled + let handled: (conn: Connection) => void + const handlerPromise = new Promise(resolve => { handled = resolve }) + + const handler = (conn: Connection): void => { handled(conn) } + + // Create a listener with the handler + const listener = transport.createListener({ + handler, + upgrader + }) + + // Listen on the multi-address + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + await transport.dial(localAddrs[0], { + upgrader + }) + + // Wait for the incoming dial to be handled + await handlerPromise + + // Close the listener + await listener.close() + }) + + it('should handle multiple simultaneous closes', async () => { + // Create a Promise that resolves when a connection is handled + let handled: (conn: Connection) => void + const handlerPromise = new Promise(resolve => { handled = resolve }) + + const handler = (conn: Connection): void => { handled(conn) } + + // Create a listener with the handler + const listener = transport.createListener({ + handler, + upgrader + }) + + // Listen on the multi-address + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + const dialerConn = await transport.dial(localAddrs[0], { + upgrader + }) + + // Wait for the incoming dial to be handled + await handlerPromise + + // Close the dialer with two simultaneous calls to `close` + await Promise.race([ + new Promise((resolve, reject) => setTimeout(() => { reject(new Error('Timed out waiting for connection close')) }, 500)), + await Promise.all([ + dialerConn.close(), + dialerConn.close() + ]) + ]) + + await listener.close() + }) +}) diff --git a/packages/transport-tcp/test/filter.spec.ts b/packages/transport-tcp/test/filter.spec.ts new file mode 100644 index 0000000000..ce345a4079 --- /dev/null +++ b/packages/transport-tcp/test/filter.spec.ts @@ -0,0 +1,42 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { Transport } from '@libp2p/interface-transport' + +describe('filter addrs', () => { + const base = '/ip4/127.0.0.1' + const ipfs = '/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw' + const unix = '/tmp/some/file.sock' + + let transport: Transport + + before(() => { + transport = tcp()() + }) + + it('filter valid addrs for this transport', () => { + const ma1 = multiaddr(base + '/tcp/9090') + const ma2 = multiaddr(base + '/udp/9090') + const ma3 = multiaddr(base + '/tcp/9090/http') + const ma4 = multiaddr(base + '/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma5 = multiaddr(base + '/tcp/9090/http' + ipfs) + const ma6 = multiaddr('/ip4/127.0.0.1/tcp/9090/p2p-circuit' + ipfs) + const ma7 = multiaddr('/dns4/libp2p.io/tcp/9090') + const ma8 = multiaddr('/dnsaddr/libp2p.io/tcp/9090') + const ma9 = multiaddr('/unix' + unix) + + const valid = transport.filter([ma1, ma2, ma3, ma4, ma5, ma6, ma7, ma8, ma9]) + expect(valid.length).to.equal(5) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma4) + expect(valid[4]).to.deep.equal(ma9) + }) + + it('filter a single addr for this transport', () => { + const ma1 = multiaddr(base + '/tcp/9090') + + const valid = transport.filter([ma1]) + expect(valid.length).to.equal(1) + expect(valid[0]).to.eql(ma1) + }) +}) diff --git a/packages/transport-tcp/test/listen-dial.spec.ts b/packages/transport-tcp/test/listen-dial.spec.ts new file mode 100644 index 0000000000..f1c54b852e --- /dev/null +++ b/packages/transport-tcp/test/listen-dial.spec.ts @@ -0,0 +1,389 @@ +import os from 'os' +import path from 'path' +import { mockRegistrar, mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pipe } from 'it-pipe' +import pDefer from 'p-defer' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { tcp } from '../src/index.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { Transport, Upgrader } from '@libp2p/interface-transport' + +const isCI = process.env.CI + +describe('listen', () => { + let transport: Transport + let listener: any + let upgrader: Upgrader + + beforeEach(() => { + transport = tcp()() + upgrader = mockUpgrader({ + events: new EventEmitter() + }) + }) + + afterEach(async () => { + try { + if (listener != null) { + await listener.close() + } + } catch { + // some tests close the listener so ignore errors + } + }) + + it('listen on path', async () => { + const mh = multiaddr(`/unix/${path.resolve(os.tmpdir(), `/tmp/p2pd-${Date.now()}.sock`)}`) + + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('listen on port 0', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('errors when listening on busy port', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const listener2 = transport.createListener({ + upgrader + }) + + const mh2 = listener.getAddrs()[0] + await expect(listener2.listen(mh2)).to.eventually.be.rejected() + .with.property('code', 'EADDRINUSE') + }) + + it('listen on IPv6 addr', async () => { + if (isCI != null) { + return + } + const mh = multiaddr('/ip6/::/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('listen on any Interface', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + }) + + it('getAddrs', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + expect(multiaddrs[0]).to.deep.equal(mh) + }) + + it('getAddrs on port 0 listen', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + }) + + it('getAddrs from listening on 0.0.0.0', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toString().indexOf('0.0.0.0')).to.equal(-1) + }) + + it('getAddrs from listening on 0.0.0.0 and port 0', async () => { + const mh = multiaddr('/ip4/0.0.0.0/tcp/0') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toString().indexOf('0.0.0.0')).to.equal(-1) + }) + + it('getAddrs from listening on ip6 \'::\'', async () => { + const mh = multiaddr('/ip6/::/tcp/9090') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length > 0).to.equal(true) + expect(multiaddrs[0].toOptions().host).to.not.equal('::') + }) + + it('getAddrs preserves IPFS Id', async () => { + const mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = transport.createListener({ + upgrader + }) + await listener.listen(mh) + + const multiaddrs = listener.getAddrs() + expect(multiaddrs.length).to.equal(1) + expect(multiaddrs[0]).to.deep.equal(mh) + }) +}) + +describe('dial', () => { + const protocol = '/echo/1.0.0' + let transport: Transport + let upgrader: Upgrader + + beforeEach(async () => { + const registrar = mockRegistrar() + void registrar.handle(protocol, (evt) => { + void pipe( + evt.stream, + evt.stream + ) + }) + upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() + }) + + transport = tcp()() + }) + + it('dial on IPv4', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial on IPv6', async () => { + if (isCI != null) { + return + } + + const ma = multiaddr('/ip6/::/tcp/9090') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial on path', async () => { + const ma = multiaddr(`/unix/${path.resolve(os.tmpdir(), `/tmp/p2pd-${Date.now()}.sock`)}`) + + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + await listener.close() + }) + + it('dial and destroy on listener', async () => { + let handled: () => void + const handledPromise = new Promise(resolve => { handled = resolve }) + + const ma = multiaddr('/ip6/::/tcp/9090') + + const listener = transport.createListener({ + handler: (conn) => { + // let multistream select finish before closing + setTimeout(() => { + void conn.close() + .then(() => { handled() }) + }, 100) + }, + upgrader + }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + + const conn = await transport.dial(addrs[0], { + upgrader + }) + const stream = await conn.newStream([protocol]) + pipe(stream) + + await handledPromise + await conn.close() + await listener.close() + }) + + it('dial and destroy on dialer', async () => { + if (isCI != null) { + return + } + + let handled: () => void + const handledPromise = new Promise(resolve => { handled = resolve }) + + const ma = multiaddr('/ip6/::/tcp/9090') + + const listener = transport.createListener({ + handler: () => { + handled() + }, + upgrader + }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + const conn = await transport.dial(addrs[0], { + upgrader + }) + + await conn.close() + await handledPromise + await listener.close() + }) + + it('dials on IPv4 with IPFS Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const conn = await transport.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + + const values = await pipe( + [uint8ArrayFromString('hey')], + stream, + async (source) => all(source) + ) + expect(values[0].subarray()).to.equalBytes(uint8ArrayFromString('hey')) + + await conn.close() + await listener.close() + }) + + it('aborts during dial', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const maConnPromise = pDefer() + + // @ts-expect-error missing return value + upgrader.upgradeOutbound = async (maConn) => { + maConnPromise.resolve(maConn) + + // take a long time to give us time to abort the dial + await new Promise((resolve) => { + setTimeout(() => { resolve() }, 100) + }) + } + + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const abortController = new AbortController() + + // abort once the upgrade process has started + void maConnPromise.promise.then(() => { abortController.abort() }) + + await expect(transport.dial(ma, { + upgrader, + signal: abortController.signal + })).to.eventually.be.rejected('The operation was aborted') + + await expect(maConnPromise.promise).to.eventually.have.nested.property('timeline.close') + .that.is.ok('did not gracefully close maConn') + + await listener.close() + }) + + it('aborts before dial', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = transport.createListener({ + upgrader + }) + await listener.listen(ma) + + const abortController = new AbortController() + abortController.abort() + + await expect(transport.dial(ma, { + upgrader, + signal: abortController.signal + })).to.eventually.be.rejected('The operation was aborted') + + await listener.close() + }) +}) diff --git a/packages/transport-tcp/test/max-connections-close.spec.ts b/packages/transport-tcp/test/max-connections-close.spec.ts new file mode 100644 index 0000000000..422a9f8b87 --- /dev/null +++ b/packages/transport-tcp/test/max-connections-close.spec.ts @@ -0,0 +1,121 @@ +import net from 'node:net' +import { promisify } from 'util' +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { TCPListener } from '../src/listener.js' + +describe('close server on maxConnections', () => { + const afterEachCallbacks: Array<() => Promise | any> = [] + afterEach(async () => { + await Promise.all(afterEachCallbacks.map(fn => fn())) + afterEachCallbacks.length = 0 + }) + + it('reject dial of connection above closeAbove', async () => { + const listenBelow = 2 + const closeAbove = 3 + const port = 9900 + + const seenRemoteConnections = new Set() + const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = trasnport.createListener({ upgrader }) as TCPListener + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + + listener.addEventListener('connection', (conn) => { + seenRemoteConnections.add(conn.detail.remoteAddr.toString()) + }) + + function createSocket (): net.Socket { + const socket = net.connect({ host: '127.0.0.1', port }) + + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.unshift(async () => { + if (!socket.destroyed) { + socket.destroy() + await new Promise((resolve) => socket.on('close', resolve)) + } + }) + + return socket + } + + async function assertConnectedSocket (i: number): Promise { + const socket = createSocket() + + await new Promise((resolve, reject) => { + socket.once('connect', () => { + resolve() + }) + socket.once('error', (err) => { + err.message = `Socket[${i}] ${err.message}` + reject(err) + }) + }) + + return socket + } + + async function assertRefusedSocket (i: number): Promise { + const socket = createSocket() + + await new Promise((resolve, reject) => { + socket.once('connect', () => { + reject(Error(`Socket[${i}] connected but was expected to reject`)) + }) + socket.once('error', (err) => { + if (err.message.includes('ECONNREFUSED')) { + resolve() + } else { + err.message = `Socket[${i}] unexpected error ${err.message}` + reject(err) + } + }) + }) + } + + async function assertServerConnections (connections: number): Promise { + // Expect server connections but allow time for sockets to connect or disconnect + for (let i = 0; i < 100; i++) { + // eslint-disable-next-line @typescript-eslint/dot-notation + if (listener['connections'].size === connections) { + return + } else { + await promisify(setTimeout)(10) + } + } + // eslint-disable-next-line @typescript-eslint/dot-notation + expect(listener['connections'].size).equals(connections, 'Wrong server connections') + } + + const socket1 = await assertConnectedSocket(1) + const socket2 = await assertConnectedSocket(2) + const socket3 = await assertConnectedSocket(3) + await assertServerConnections(3) + // Limit reached, server should be closed here + await assertRefusedSocket(4) + await assertRefusedSocket(5) + // Destroy sockets to be have connections < listenBelow + socket1.destroy() + socket2.destroy() + await assertServerConnections(1) + // Attempt to connect more sockets + const socket6 = await assertConnectedSocket(6) + const socket7 = await assertConnectedSocket(7) + await assertServerConnections(3) + // Limit reached, server should be closed here + await assertRefusedSocket(8) + + expect(socket3.destroyed).equals(false, 'socket3 must not destroyed') + expect(socket6.destroyed).equals(false, 'socket6 must not destroyed') + expect(socket7.destroyed).equals(false, 'socket7 must not destroyed') + }) +}) diff --git a/packages/transport-tcp/test/max-connections.spec.ts b/packages/transport-tcp/test/max-connections.spec.ts new file mode 100644 index 0000000000..4e1634b01f --- /dev/null +++ b/packages/transport-tcp/test/max-connections.spec.ts @@ -0,0 +1,82 @@ +import net from 'node:net' +import { promisify } from 'node:util' +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' + +describe('maxConnections', () => { + const afterEachCallbacks: Array<() => Promise | any> = [] + afterEach(async () => { + await Promise.all(afterEachCallbacks.map(fn => fn())) + afterEachCallbacks.length = 0 + }) + + it('reject dial of connection above maxConnections', async () => { + const maxConnections = 2 + const socketCount = 4 + const port = 9900 + + const seenRemoteConnections = new Set() + const transport = tcp({ maxConnections })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = transport.createListener({ upgrader }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + + listener.addEventListener('connection', (conn) => { + seenRemoteConnections.add(conn.detail.remoteAddr.toString()) + }) + + const sockets: net.Socket[] = [] + + for (let i = 0; i < socketCount; i++) { + const socket = net.connect({ host: '127.0.0.1', port }) + sockets.push(socket) + + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.unshift(async () => { + if (!socket.destroyed) { + socket.destroy() + await new Promise((resolve) => socket.on('close', resolve)) + } + }) + + // Wait for connection so the order of sockets is stable, sockets expected to be alive are always [0,1] + await new Promise((resolve, reject) => { + socket.on('connect', () => { + resolve() + }) + socket.on('error', (err) => { + reject(err) + }) + }) + } + + // With server.maxConnections the TCP socket is created and the initial handshake is completed + // Then in the server handler NodeJS javascript code will call socket.emit('drop') if over the limit + // https://github.com/nodejs/node/blob/fddc701d3c0eb4520f2af570876cc987ae6b4ba2/lib/net.js#L1706 + + // Wait for some time for server to drop all sockets above limit + await promisify(setTimeout)(250) + + expect(seenRemoteConnections.size).equals(maxConnections, 'wrong serverConnections') + + for (let i = 0; i < socketCount; i++) { + const socket = sockets[i] + + if (i < maxConnections) { + // Assert socket connected + expect(socket.destroyed).equals(false, `socket ${i} under limit must not be destroyed`) + } else { + // Assert socket ended + expect(socket.destroyed).equals(true, `socket ${i} above limit must be destroyed`) + } + } + }) +}) diff --git a/packages/transport-tcp/test/socket-to-conn.spec.ts b/packages/transport-tcp/test/socket-to-conn.spec.ts new file mode 100644 index 0000000000..e4f8dbfd49 --- /dev/null +++ b/packages/transport-tcp/test/socket-to-conn.spec.ts @@ -0,0 +1,428 @@ +import { createServer, Socket, type Server, type ServerOpts, type SocketConstructorOpts } from 'net' +import os from 'os' +import { expect } from 'aegir/chai' +import defer from 'p-defer' +import { toMultiaddrConnection } from '../src/socket-to-conn.js' + +async function setup (opts?: { server?: ServerOpts, client?: SocketConstructorOpts }): Promise<{ server: Server, serverSocket: Socket, clientSocket: Socket }> { + const serverListening = defer() + + const server = createServer(opts?.server) + server.listen(0, () => { + serverListening.resolve() + }) + + await serverListening.promise + + const serverSocket = defer() + const clientSocket = defer() + + server.once('connection', (socket) => { + serverSocket.resolve(socket) + }) + + const address = server.address() + + if (address == null || typeof address === 'string') { + throw new Error('Wrong socket type') + } + + const client = new Socket(opts?.client) + client.once('connect', () => { + clientSocket.resolve(client) + }) + client.connect(address.port, address.address) + + return { + server, + serverSocket: await serverSocket.promise, + clientSocket: await clientSocket.promise + } +} + +describe('socket-to-conn', () => { + let server: Server + let clientSocket: Socket + let serverSocket: Socket + + afterEach(async () => { + if (serverSocket != null) { + serverSocket.destroy() + } + + if (clientSocket != null) { + clientSocket.destroy() + } + + if (server != null) { + server.close() + } + }) + + it('should destroy a socket that is closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when client socket is closed + const clientClosed = defer() + + // promise that is resolved when client socket errors + const clientErrored = defer() + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('close', () => { + clientClosed.resolve(true) + }) + clientSocket.once('error', err => { + clientErrored.resolve(err) + }) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for writing + clientSocket.end() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket that is forcibly closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for reading and writing immediately + clientSocket.destroy() + + // client closed the connection - error code is platform specific + if (os.platform() === 'linux') { + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + } else { + await expect(serverErrored.promise).to.eventually.have.property('code', 'ECONNRESET') + } + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket that is half-closed by the client', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + client: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // close the client for writing + clientSocket.end() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote stopped sending us data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket after sinking', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + await inboundMaConn.sink([ + Uint8Array.from([0, 1, 2, 3]) + ]) + + // server socket should no longer be writable + expect(serverSocket.writable).to.be.false() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote didn't send us any data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_READ_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket when containing MultiaddrConnection is closed', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 10 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + server: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 10 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed but remote keeps sending data', async () => { + ({ server, clientSocket, serverSocket } = await setup({ + server: { + allowHalfOpen: true + } + })) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 500, + socketCloseTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + setInterval(() => { + clientSocket.write(`some data ${Date.now()}`) + }, 10).unref() + + await inboundMaConn.close() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) + + it('should destroy a socket by timeout when containing MultiaddrConnection is closed but closing remote times out', async () => { + ({ server, clientSocket, serverSocket } = await setup()) + + // promise that is resolved when our outgoing socket is closed + const serverClosed = defer() + + // promise that is resolved when our outgoing socket errors + const serverErrored = defer() + + let maConnCloseError: Error | undefined + + const inboundMaConn = toMultiaddrConnection(serverSocket, { + socketInactivityTimeout: 100, + socketCloseTimeout: 100 + }) + expect(inboundMaConn.timeline.open).to.be.ok() + expect(inboundMaConn.timeline.close).to.not.be.ok() + + clientSocket.once('error', () => {}) + + serverSocket.once('close', () => { + serverClosed.resolve(true) + }) + serverSocket.once('error', err => { + serverErrored.resolve(err) + }) + + // send some data between the client and server + clientSocket.write('hello') + serverSocket.write('goodbye') + + // stop reading data + clientSocket.pause() + + // have to write enough data quickly enough to overwhelm the client + while (serverSocket.writableLength < 1024) { + serverSocket.write('goodbyeeeeeeeeeeeeee') + } + + await inboundMaConn.close().catch(err => { + // should throw this error + maConnCloseError = err + }) + + // server socket should no longer be writable + expect(serverSocket.writable).to.be.false() + + // server socket was closed for reading and writing + await expect(serverClosed.promise).to.eventually.be.true() + + // remote didn't read our data + await expect(serverErrored.promise).to.eventually.have.property('code', 'ERR_SOCKET_CLOSE_TIMEOUT') + + // closing should have thrown + expect(maConnCloseError).to.have.property('code', 'ERR_SOCKET_CLOSE_TIMEOUT') + + // the connection closing was recorded + expect(inboundMaConn.timeline.close).to.be.a('number') + + // server socket is destroyed + expect(serverSocket.destroyed).to.be.true() + }) +}) diff --git a/packages/transport-tcp/tsconfig.json b/packages/transport-tcp/tsconfig.json new file mode 100644 index 0000000000..770ba81a1e --- /dev/null +++ b/packages/transport-tcp/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interface-transport-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/transport-webrtc/.aegir.js b/packages/transport-webrtc/.aegir.js new file mode 100644 index 0000000000..f19d27456f --- /dev/null +++ b/packages/transport-webrtc/.aegir.js @@ -0,0 +1,61 @@ + +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + config: { + platform: 'node' + }, + bundlesizeMax: '117KB' + }, + test: { + before: async () => { + const { createLibp2p } = await import('libp2p') + const { circuitRelayServer } = await import('libp2p/circuit-relay') + const { identifyService } = await import('libp2p/identify') + const { webSockets } = await import('@libp2p/websockets') + const { noise } = await import('@chainsafe/libp2p-noise') + const { yamux } = await import('@chainsafe/libp2p-yamux') + + // start a relay node for use in the tests + const relay = await createLibp2p({ + addresses: { + listen: [ + '/ip4/127.0.0.1/tcp/0/ws' + ] + }, + transports: [ + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + services: { + relay: circuitRelayServer({ + reservations: { + maxReservations: Infinity + } + }), + identify: identifyService() + }, + connectionManager: { + minConnections: 0 + } + }) + + const multiaddrs = relay.getMultiaddrs().map(ma => ma.toString()) + + return { + relay, + env: { + RELAY_MULTIADDR: multiaddrs[0] + } + } + }, + after: async (_, before) => { + await before.relay.stop() + } + } +} diff --git a/packages/transport-webrtc/CHANGELOG.md b/packages/transport-webrtc/CHANGELOG.md new file mode 100644 index 0000000000..d4076ee6de --- /dev/null +++ b/packages/transport-webrtc/CHANGELOG.md @@ -0,0 +1,242 @@ +## [2.0.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.9...v2.0.10) (2023-06-12) + + +### Bug Fixes + +* add browser-to-browser test for bi-directional communication ([#172](https://github.com/libp2p/js-libp2p-webrtc/issues/172)) ([1ec3d8a](https://github.com/libp2p/js-libp2p-webrtc/commit/1ec3d8a8b611d5227f430037e2547fd86d115eaa)) + +## [2.0.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.8...v2.0.9) (2023-06-12) + + +### Dependencies + +* **dev:** bump delay from 5.0.0 to 6.0.0 ([#169](https://github.com/libp2p/js-libp2p-webrtc/issues/169)) ([104cbf0](https://github.com/libp2p/js-libp2p-webrtc/commit/104cbf0e2009961656cda530925089dc126b19a8)) + +## [2.0.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.7...v2.0.8) (2023-06-12) + + +### Tests + +* add a test for large transfers ([#175](https://github.com/libp2p/js-libp2p-webrtc/issues/175)) ([0f60060](https://github.com/libp2p/js-libp2p-webrtc/commit/0f60060c9ceaf2bf2142df25f32174112edf6ec9)) + +## [2.0.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.6...v2.0.7) (2023-06-07) + + +### Tests + +* actually run firefox tests on firefox ([#176](https://github.com/libp2p/js-libp2p-webrtc/issues/176)) ([386a607](https://github.com/libp2p/js-libp2p-webrtc/commit/386a6071923e6cb1d89c51b73dada306b7cc243f)) + +## [2.0.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.5...v2.0.6) (2023-06-04) + + +### Documentation + +* update README.md example ([#178](https://github.com/libp2p/js-libp2p-webrtc/issues/178)) ([1264875](https://github.com/libp2p/js-libp2p-webrtc/commit/1264875ebd40b057e70aa47bebde45bfbe80facb)) + +## [2.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.4...v2.0.5) (2023-06-01) + + +### Bug Fixes + +* Update splitAddr function to correctly parse multiaddrs ([#174](https://github.com/libp2p/js-libp2p-webrtc/issues/174)) ([22a7029](https://github.com/libp2p/js-libp2p-webrtc/commit/22a7029caab7601cfc1f1d1051bc218ebe4dfce0)) + +## [2.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.3...v2.0.4) (2023-05-17) + + +### Bug Fixes + +* use abstract stream class from muxer interface module ([#165](https://github.com/libp2p/js-libp2p-webrtc/issues/165)) ([32f68de](https://github.com/libp2p/js-libp2p-webrtc/commit/32f68de455d2f0b136553aa41caf06adaf1f09d1)), closes [#164](https://github.com/libp2p/js-libp2p-webrtc/issues/164) + +## [2.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.2...v2.0.3) (2023-05-17) + + +### Bug Fixes + +* restrict message sizes to 16kb ([#147](https://github.com/libp2p/js-libp2p-webrtc/issues/147)) ([aca4422](https://github.com/libp2p/js-libp2p-webrtc/commit/aca4422f5d4b81576d8c3cc5531cef7b7491abd2)), closes [#144](https://github.com/libp2p/js-libp2p-webrtc/issues/144) [#158](https://github.com/libp2p/js-libp2p-webrtc/issues/158) + +## [2.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.1...v2.0.2) (2023-05-15) + + +### Bug Fixes + +* use transport manager getListeners to get listen addresses ([#166](https://github.com/libp2p/js-libp2p-webrtc/issues/166)) ([2e144f9](https://github.com/libp2p/js-libp2p-webrtc/commit/2e144f977a2025aa3adce1816d5f7d0dc3aaa477)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v2.0.0...v2.0.1) (2023-05-12) + + +### Bug Fixes + +* remove protobuf-ts and split code into two folders ([#162](https://github.com/libp2p/js-libp2p-webrtc/issues/162)) ([64723a7](https://github.com/libp2p/js-libp2p-webrtc/commit/64723a726302edcdc7ec958a759c3c587a184d69)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.2.0...v2.0.0) (2023-05-11) + + +### ⚠ BREAKING CHANGES + +* must be used with libp2p@0.45.x + +### Dependencies + +* update all libp2p deps for compat with libp2p@0.45.x ([#160](https://github.com/libp2p/js-libp2p-webrtc/issues/160)) ([b20875d](https://github.com/libp2p/js-libp2p-webrtc/commit/b20875d9f73e5cad05376db2d1228363dd1bce7d)) + +## [1.2.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.11...v1.2.0) (2023-05-09) + + +### Features + +* export metrics ([#71](https://github.com/libp2p/js-libp2p-webrtc/issues/71)) ([b3cb445](https://github.com/libp2p/js-libp2p-webrtc/commit/b3cb445e226d6d4ddba092cf961d6178d9a19ac1)) + +## [1.1.11](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.10...v1.1.11) (2023-05-06) + + +### Dependencies + +* upgrade transport interface to 4.0.1 ([#150](https://github.com/libp2p/js-libp2p-webrtc/issues/150)) ([dc61fa2](https://github.com/libp2p/js-libp2p-webrtc/commit/dc61fa27a2f53568b1f3b320971de166b5b243f9)) + +## [1.1.10](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.9...v1.1.10) (2023-05-03) + + +### Bug Fixes + +* Fetch local fingerprint from SDP ([#109](https://github.com/libp2p/js-libp2p-webrtc/issues/109)) ([3673d6c](https://github.com/libp2p/js-libp2p-webrtc/commit/3673d6c2637c21e488e684cdff4eedbb7f5b3692)) + +## [1.1.9](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.8...v1.1.9) (2023-04-26) + + +### Documentation + +* update import in README example ([#141](https://github.com/libp2p/js-libp2p-webrtc/issues/141)) ([42275df](https://github.com/libp2p/js-libp2p-webrtc/commit/42275df0727cd729006cbf3fae300fc428c9ca51)) + +## [1.1.8](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.7...v1.1.8) (2023-04-25) + + +### Bug Fixes + +* added peer connection state listener to emit closed events ([#134](https://github.com/libp2p/js-libp2p-webrtc/issues/134)) ([16e8503](https://github.com/libp2p/js-libp2p-webrtc/commit/16e85030e78ed9edb2ebecf81bac3ad33d622111)), closes [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) [#138](https://github.com/libp2p/js-libp2p-webrtc/issues/138) + +## [1.1.7](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.6...v1.1.7) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#135](https://github.com/libp2p/js-libp2p-webrtc/issues/135)) ([2fc8399](https://github.com/libp2p/js-libp2p-webrtc/commit/2fc839912a65c310ca7c8935d1901cc56849a21d)) + +## [1.1.6](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.5...v1.1.6) (2023-04-21) + + +### Bug Fixes + +* readme: Remove confusing section ([#122](https://github.com/libp2p/js-libp2p-webrtc/issues/122)) ([dc78154](https://github.com/libp2p/js-libp2p-webrtc/commit/dc781543b8175c6c40c6745029a4ba53587aef29)) + +## [1.1.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.4...v1.1.5) (2023-04-13) + + +### Dependencies + +* bump it-pipe from 2.0.5 to 3.0.1 ([#111](https://github.com/libp2p/js-libp2p-webrtc/issues/111)) ([7e593a3](https://github.com/libp2p/js-libp2p-webrtc/commit/7e593a34b44b7a2cf4758df2218b3ba9ebacfce9)) +* bump protons-runtime from 4.0.2 to 5.0.0 ([#117](https://github.com/libp2p/js-libp2p-webrtc/issues/117)) ([87cbb19](https://github.com/libp2p/js-libp2p-webrtc/commit/87cbb193e2a45642333498d9317ab17eb527d34d)) +* **dev:** bump protons from 6.1.3 to 7.0.2 ([#119](https://github.com/libp2p/js-libp2p-webrtc/issues/119)) ([fd20f4f](https://github.com/libp2p/js-libp2p-webrtc/commit/fd20f4f7a182a8edca5a511fe747885d24a60652)) + +## [1.1.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.3...v1.1.4) (2023-04-13) + + +### Dependencies + +* Update multiaddr to 12.1.1 and multiformats 11.0.2 ([#123](https://github.com/libp2p/js-libp2p-webrtc/issues/123)) ([e069784](https://github.com/libp2p/js-libp2p-webrtc/commit/e069784229f2495b3cebc2c2a85969f23f0e7acf)) + +## [1.1.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.2...v1.1.3) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#124](https://github.com/libp2p/js-libp2p-webrtc/issues/124)) ([4146761](https://github.com/libp2p/js-libp2p-webrtc/commit/4146761226118268d510c8834f894083ba5408d3)) + +## [1.1.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.1...v1.1.2) (2023-04-11) + + +### Bug Fixes + +* update multiaddr in webrtc connection to include webRTC ([#121](https://github.com/libp2p/js-libp2p-webrtc/issues/121)) ([6ea04db](https://github.com/libp2p/js-libp2p-webrtc/commit/6ea04db9800259963affcb3101ea542de79271c0)) + +## [1.1.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.1.0...v1.1.1) (2023-04-10) + + +### Dependencies + +* bump it-pb-stream from 2.0.4 to 3.2.1 ([#118](https://github.com/libp2p/js-libp2p-webrtc/issues/118)) ([7e2ac67](https://github.com/libp2p/js-libp2p-webrtc/commit/7e2ac6795ea096b3cf5dc2c4077f6f39821e0502)) + +## [1.1.0](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.5...v1.1.0) (2023-04-07) + + +### Features + +* Browser to Browser ([#90](https://github.com/libp2p/js-libp2p-webrtc/issues/90)) ([add5c46](https://github.com/libp2p/js-libp2p-webrtc/commit/add5c467a2d02058933e6e11751af0c850568eaf)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.4...v1.0.5) (2023-03-30) + + +### Bug Fixes + +* correction package.json exports types path ([#103](https://github.com/libp2p/js-libp2p-webrtc/issues/103)) ([c78851f](https://github.com/libp2p/js-libp2p-webrtc/commit/c78851fe71f6a6ca79a146a7022e818378ea6721)) + + +### Trivial Changes + +* replace err-code with CodeError ([#82](https://github.com/libp2p/js-libp2p-webrtc/issues/82)) ([cfa6494](https://github.com/libp2p/js-libp2p-webrtc/commit/cfa6494c43c4edb977e70abe81a260bf0e03de73)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([f0ae5e7](https://github.com/libp2p/js-libp2p-webrtc/commit/f0ae5e78a0469bd1129d7b242e4fb41f0b2ed49e)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([4c8806c](https://github.com/libp2p/js-libp2p-webrtc/commit/4c8806c6d2a1a8eff48f0e2248203d48bd84c065)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.3...v1.0.4) (2023-02-22) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.6 ([#94](https://github.com/libp2p/js-libp2p-webrtc/issues/94)) ([2ee8a5e](https://github.com/libp2p/js-libp2p-webrtc/commit/2ee8a5e4bb03377214ff3c12744c2e153a3f69b4)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([7e0b1c0](https://github.com/libp2p/js-libp2p-webrtc/commit/7e0b1c00b28cae7249a506f06f18bf3537bf3476)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.2...v1.0.3) (2023-01-30) + + +### Tests + +* add stream transition test ([#72](https://github.com/libp2p/js-libp2p-webrtc/issues/72)) ([27ec3da](https://github.com/libp2p/js-libp2p-webrtc/commit/27ec3da4ef66cf07c1452c6f987cb55d313c1a03)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.1...v1.0.2) (2023-01-04) + + +### Dependencies + +* bump multiformats from 10.0.3 to 11.0.0 ([#70](https://github.com/libp2p/js-libp2p-webrtc/issues/70)) ([7dafe5a](https://github.com/libp2p/js-libp2p-webrtc/commit/7dafe5a126ca0ce2b6d887f6a84fabe55e36229d)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-webrtc/compare/v1.0.0...v1.0.1) (2023-01-03) + + +### Bug Fixes + +* remove uuid dependency ([#68](https://github.com/libp2p/js-libp2p-webrtc/issues/68)) ([fb14b88](https://github.com/libp2p/js-libp2p-webrtc/commit/fb14b880d1b1b278e1e826bb0d9939db358e6ccc)) + +## 1.0.0 (2022-12-13) + + +### Bug Fixes + +* update project config ([#65](https://github.com/libp2p/js-libp2p-webrtc/issues/65)) ([09c33cc](https://github.com/libp2p/js-libp2p-webrtc/commit/09c33ccfff97059eab001e46a662467dea670ce1)) + + +### Dependencies + +* update libp2p to release version ([dbd0237](https://github.com/libp2p/js-libp2p-webrtc/commit/dbd0237e9f8500ac13948e3a35d912df257968a4)) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([43c70bc](https://github.com/libp2p/js-libp2p-webrtc/commit/43c70bcd3c63388ed44d76703ce9a32e51d9ef30)) + + +### Documentation + +* fix 'browser to server' build config ([#66](https://github.com/libp2p/js-libp2p-webrtc/issues/66)) ([b54132c](https://github.com/libp2p/js-libp2p-webrtc/commit/b54132cecac180f0577a1b7905f79b20207c3647)) diff --git a/packages/transport-webrtc/LICENSE b/packages/transport-webrtc/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-webrtc/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-webrtc/LICENSE-APACHE b/packages/transport-webrtc/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-webrtc/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-webrtc/LICENSE-MIT b/packages/transport-webrtc/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-webrtc/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/transport-webrtc/README.md b/packages/transport-webrtc/README.md new file mode 100644 index 0000000000..6b13043a49 --- /dev/null +++ b/packages/transport-webrtc/README.md @@ -0,0 +1,188 @@ +# @libp2p/webrtc + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> A libp2p transport using WebRTC connections + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## Usage + +```js +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import first from 'it-first' +import { pipe } from 'it-pipe' +import { fromString, toString } from 'uint8arrays' +import { webRTC } from '@libp2p/webrtc' + +const node = await createLibp2p({ + transports: [webRTC()], + connectionEncryption: [noise()], +}); + +await node.start() + +const ma = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') +const stream = await node.dialProtocol(ma, ['/my-protocol/1.0.0']) +const message = `Hello js-libp2p-webrtc\n` +const response = await pipe([fromString(message)], stream, async (source) => await first(source)) +const responseDecoded = toString(response.slice(0, response.length)) +``` + +## Examples + +Examples can be found in the [examples folder](examples/README.md). + +## Interfaces + +### Transport + +![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-transport](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-transport/img/badge.png) + +Browsers can usually only `dial`, but `listen` is supported in the WebRTC +transport when paired with another listener like CircuitV2, where you listen on +a relayed connection. Take a look at [index.js](examples/browser-to-browser/index.js) for +an example. + +### Connection + +![https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/interface-connection](https://raw.githubusercontent.com/libp2p/js-libp2p-interfaces/master/packages/interface-connection/img/badge.png) + +```js +interface MultiaddrConnection extends Duplex { + close: (err?: Error) => Promise + remoteAddr: Multiaddr + timeline: MultiaddrConnectionTimeline +} + +class WebRTCMultiaddrConnection implements MultiaddrConnection { } +``` + +## Development + +Contributions are welcome! The libp2p implementation in JavaScript is a work in progress. As such, there's a few things you can do right now to help out: + +- [Check out the existing issues](//github.com/little-bear-labs/js-libp2p-webrtc/issues). +- **Perform code reviews**. +- **Add tests**. There can never be enough tests. +- Go through the modules and **check out existing issues**. This is especially useful for modules in active development. Some knowledge of IPFS/libp2p may be required, as well as the infrastructure behind it - for instance, you may need to read up on p2p and more complex operations like muxing to be able to help technically. + +Please be aware that all interactions related to libp2p 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. + +This module leans heavily on (Aegir)\[] for most of the `package.json` scripts. + +### Build + +The build script is a wrapper to `aegir build`. To build this package: + +```shell +npm run build +``` + +The build will be located in the `/dist` folder. + +### Protocol Buffers + +There is also `npm run generate:proto` script that uses protoc to populate the generated code directory `proto_ts` based on `*.proto` files in src. Don't forget to run this step before `build` any time you make a change to any of the `*.proto` files. + +### Test + +To run all tests: + +```shell +npm test +``` + +To run tests for Chrome only: + +```shell +npm run test:chrome +``` + +To run tests for Firefox only: + +```shell +npm run test:firefox +``` + +### Lint + +Aegir is also used to lint the code, which follows the [Standard](https://github.com/standard/standard) JS linter. +The VS Code plugin for this standard is located at . +To lint this repo: + +```shell +npm run lint +``` + +You can also auto-fix when applicable: + +```shell +npm run lint:fix +``` + +### Clean + +```shell +npm run clean +``` + +### Check Dependencies + +```shell +npm run deps-check +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-webrtc/examples/README.md b/packages/transport-webrtc/examples/README.md new file mode 100644 index 0000000000..8b78f83bbe --- /dev/null +++ b/packages/transport-webrtc/examples/README.md @@ -0,0 +1,4 @@ +# Examples + +* [Browser to Server Echo](browser-to-server/README.md): connect to a go-libp2p-webrtc server with a browser + diff --git a/packages/transport-webrtc/examples/browser-to-browser/README.md b/packages/transport-webrtc/examples/browser-to-browser/README.md new file mode 100644 index 0000000000..866513c1a6 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/README.md @@ -0,0 +1,61 @@ +# js-libp2p-webrtc Browser to Browser + +This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. + +## Build the `@libp2p/webrtc` package + +Build the `@libp2p/webrtc` package by calling `npm i && npm run build` in the repository root. + +## Running the Relay Server + +For browsers to communicate, we first need to run the LibP2P relay server: + +```shell +npm run relay +``` + +Copy one of the multiaddresses in the output. + +## Running the Example + +In a separate console tab, install dependencies and start the Vite server: + +```shell +npm i && npm run start +``` + +The browser window will automatically open. Let's call this `Browser A`. +Using the copied multiaddress from the Go or NodeJS relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button. +`Browser A` is now connected to the relay server. +Copy the multiaddress located after the `Listening on` message. + +Now open a second browser with the url `http://localhost:5173/`. Let's call this `Browser B`. +Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button. +`Browser B` is now connected to `Browser A`. +Copy the multiaddress located after the `Listening on` message. + +Using the copied multiaddress from `Listening on` section in `Browser B`, paste it into the `Remote MultiAddress` input in `Browser A` and click the `Connect` button. +`Browser A` is now connected to `Browser B`. + +The peers are now connected to each other. Enter a message and click the `Send` button in either/both browsers and see the echo'd messages. + +The output should look like: + +`Browser A` +```text +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk' +Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9' +Sending message 'helloa' +Received message 'helloa' +Received message 'hellob' +``` + +`Browser B` +```text +Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC' +Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9 +Received message 'helloa' +Sending message 'hellob' +Received message 'hellob' +``` diff --git a/packages/transport-webrtc/examples/browser-to-browser/index.html b/packages/transport-webrtc/examples/browser-to-browser/index.html new file mode 100644 index 0000000000..8da321d9d0 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/index.html @@ -0,0 +1,49 @@ + + + + + + js-libp2p WebRTC + + + +
+
+ + + +
+
+ + + +
+
+

Active Connections:

+
    +
    +
    +

    Listening addresses:

    +
      +
      +
      +
      + + + diff --git a/packages/transport-webrtc/examples/browser-to-browser/index.js b/packages/transport-webrtc/examples/browser-to-browser/index.js new file mode 100644 index 0000000000..36edf09efd --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/index.js @@ -0,0 +1,133 @@ +import { multiaddr, protocols } from "@multiformats/multiaddr" +import { pipe } from "it-pipe" +import { fromString, toString } from "uint8arrays" +import { webRTC } from "@libp2p/webrtc" +import { webSockets } from "@libp2p/websockets" +import * as filters from "@libp2p/websockets/filters" +import { pushable } from "it-pushable" +import { mplex } from "@libp2p/mplex" +import { createLibp2p } from "libp2p" +import { circuitRelayTransport } from 'libp2p/circuit-relay' +import { noise } from "@chainsafe/libp2p-noise" +import { identifyService } from 'libp2p/identify' + +const WEBRTC_CODE = protocols('webrtc').code + +const output = document.getElementById("output") +const sendSection = document.getElementById("send-section") +const appendOutput = (line) => { + const div = document.createElement("div") + div.appendChild(document.createTextNode(line)) + output.append(div) +} +const clean = (line) => line.replaceAll("\n", "") +const sender = pushable() + +const node = await createLibp2p({ + addresses: { + listen: [ + '/webrtc' + ] + }, + transports: [ + webSockets({ + filter: filters.all, + }), + webRTC(), + circuitRelayTransport({ + discoverRelays: 1, + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + connectionGater: { + denyDialMultiaddr: () => { + // by default we refuse to dial local addresses from the browser since they + // are usually sent by remote peers broadcasting undialable multiaddrs but + // here we are explicitly connecting to a local node so do not deny dialing + // any discovered address + return false + } + }, + services: { + identify: identifyService() + } +}) + +await node.start() + +// handle the echo protocol +await node.handle("/echo/1.0.0", ({ stream }) => { + pipe( + stream, + async function* (source) { + for await (const buf of source) { + const incoming = toString(buf.subarray()) + appendOutput(`Received message '${clean(incoming)}'`) + yield buf + } + }, + stream + ) +}) + +function updateConnList() { + // Update connections list + const connListEls = node.getConnections() + .map((connection) => { + if (connection.remoteAddr.protoCodes().includes(WEBRTC_CODE)) { + sendSection.style.display = "block" + } + + const el = document.createElement("li") + el.textContent = connection.remoteAddr.toString() + return el + }) + document.getElementById("connections").replaceChildren(...connListEls) +} + +node.addEventListener("connection:open", (event) => { + updateConnList() +}) +node.addEventListener("connection:close", (event) => { + updateConnList() +}) + +node.addEventListener("self:peer:update", (event) => { + // Update multiaddrs list + const multiaddrs = node.getMultiaddrs() + .map((ma) => { + const el = document.createElement("li") + el.textContent = ma.toString() + return el + }) + document.getElementById("multiaddrs").replaceChildren(...multiaddrs) +}) + +const isWebrtc = (ma) => { + return ma.protoCodes().includes(WEBRTC_CODE) +} + +window.connect.onclick = async () => { + const ma = multiaddr(window.peer.value) + appendOutput(`Dialing '${ma}'`) + const connection = await node.dial(ma) + + if (isWebrtc(ma)) { + const outgoing_stream = await connection.newStream(["/echo/1.0.0"]) + pipe(sender, outgoing_stream, async (src) => { + for await (const buf of src) { + const response = toString(buf.subarray()) + appendOutput(`Received message '${clean(response)}'`) + } + }) + } + + appendOutput(`Connected to '${ma}'`) +} + +window.send.onclick = async () => { + const message = `${window.message.value}\n` + appendOutput(`Sending message '${clean(message)}'`) + sender.push(fromString(message)) +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/package.json b/packages/transport-webrtc/examples/browser-to-browser/package.json new file mode 100644 index 0000000000..94368cbe64 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/package.json @@ -0,0 +1,27 @@ +{ + "name": "js-libp2p-webrtc-private-to-private", + "version": "1.0.0", + "description": "Connect a browser to another browser", + "type": "module", + "scripts": { + "start": "vite", + "build": "vite build", + "relay": "node relay.js", + "test:firefox": "npm run build && playwright test --browser=firefox tests", + "test:chrome": "npm run build && playwright test tests", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.0", + "@libp2p/websockets": "^6.0.1", + "@libp2p/mplex": "^8.0.1", + "@libp2p/webrtc": "file:../../", + "@multiformats/multiaddr": "^12.0.0", + "it-pushable": "^3.1.0", + "libp2p": "^0.45.0", + "vite": "^4.2.1" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0" + } +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/relay.js b/packages/transport-webrtc/examples/browser-to-browser/relay.js new file mode 100644 index 0000000000..15754c2bc9 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/relay.js @@ -0,0 +1,26 @@ +import { mplex } from "@libp2p/mplex" +import { createLibp2p } from "libp2p" +import { noise } from "@chainsafe/libp2p-noise" +import { circuitRelayServer } from 'libp2p/circuit-relay' +import { webSockets } from '@libp2p/websockets' +import * as filters from '@libp2p/websockets/filters' +import { identifyService } from 'libp2p/identify' + +const server = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0/ws'] + }, + transports: [ + webSockets({ + filter: filters.all + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + services: { + identify: identifyService(), + relay: circuitRelayServer() + } +}) + +console.log("p2p addr: ", server.getMultiaddrs().map((ma) => ma.toString())) diff --git a/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js b/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js new file mode 100644 index 0000000000..703ae688cc --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/tests/test.spec.js @@ -0,0 +1,129 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { createLibp2p } from 'libp2p' +import { circuitRelayServer } from 'libp2p/circuit-relay' +import { webSockets } from '@libp2p/websockets' +import * as filters from '@libp2p/websockets/filters' +import { mplex } from '@libp2p/mplex' +import { noise } from '@chainsafe/libp2p-noise' +import { identifyService } from 'libp2p/identify' + +// Setup +const test = setup() + +// DOM +const connectBtn = '#connect' +const connectAddr = '#peer' +const messageInput = '#message' +const sendBtn = '#send' +const output = '#output' +const listeningAddresses = '#multiaddrs' + +let url + +// we spawn a js libp2p relay +async function spawnRelay() { + const relayNode = await createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0/ws'] + }, + transports: [ + webSockets({ + filter: filters.all + }), + ], + connectionEncryption: [noise()], + streamMuxers: [mplex()], + services: { + identify: identifyService(), + relay: circuitRelayServer() + } + }) + + const relayNodeAddr = relayNode.getMultiaddrs()[0].toString() + + return { relayNode, relayNodeAddr } +} + +test.describe('browser to browser example:', () => { + let relayNode + let relayNodeAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ servers }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const r = await spawnRelay() + relayNode = r.relayNode + relayNodeAddr = r.relayNodeAddr + console.log('Server addr:', relayNodeAddr) + url = servers[0].url + }, {}) + + test.afterAll(() => { + relayNode.stop() + }) + + test.beforeEach(async ({ page }) => { + await page.goto(url) + }) + + test('should connect to a relay node', async ({ page: pageA, context }) => { + // load second page + const pageB = await context.newPage() + await pageB.goto(url) + + // connect both pages to the relay + const relayedAddressA = await dialRelay(pageA, relayNodeAddr) + const relayedAddressB = await dialRelay(pageB, relayNodeAddr) + + // dial first page from second page over relay + await dialPeerOverRelay(pageA, relayedAddressB) + await dialPeerOverRelay(pageB, relayedAddressA) + + // stop the relay + await relayNode.stop() + + await echoMessagePeer(pageB, 'hello B') + + await echoMessagePeer(pageA, 'hello A') + }) +}) + +async function echoMessagePeer (page, message) { + // send the message to the peer over webRTC + await page.fill(messageInput, message) + await page.click(sendBtn) + + // check the message was echoed back + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Sending message '${message}'`) + await expect(outputLocator).toContainText(`Received message '${message}'`) +} + +async function dialRelay (page, address) { + // add the go libp2p multiaddress to the input field and submit + await page.fill(connectAddr, address) + await page.click(connectBtn) + + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Dialing '${address}'`) + await expect(outputLocator).toContainText(`Connected to '${address}'`) + + const multiaddrsLocator = page.locator(listeningAddresses) + await expect(multiaddrsLocator).toHaveText(/webrtc/) + + const multiaddrs = await page.textContent(listeningAddresses) + const addr = multiaddrs.split(address).filter(str => str.includes('webrtc')).pop() + + return address + addr +} + +async function dialPeerOverRelay (page, address) { + // add the go libp2p multiaddr to the input field and submit + await page.fill(connectAddr, address) + await page.click(connectBtn) + + const outputLocator = page.locator(output) + await expect(outputLocator).toContainText(`Dialing '${address}'`) + await expect(outputLocator).toContainText(`Connected to '${address}'`) +} diff --git a/packages/transport-webrtc/examples/browser-to-browser/vite.config.js b/packages/transport-webrtc/examples/browser-to-browser/vite.config.js new file mode 100644 index 0000000000..353f32b6ef --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-browser/vite.config.js @@ -0,0 +1,11 @@ +export default { + build: { + target: 'es2022' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2022', supported: { bigint: true } } + }, + server: { + open: true + } +} \ No newline at end of file diff --git a/packages/transport-webrtc/examples/browser-to-server/README.md b/packages/transport-webrtc/examples/browser-to-server/README.md new file mode 100644 index 0000000000..fb3d997503 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/README.md @@ -0,0 +1,34 @@ +# js-libp2p-webrtc Browser to Server + +This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. + +## Running the Go Server + +To run the Go LibP2P WebRTC server: + +```shell +npm run go-libp2p-server +``` + +Copy the multiaddress in the output. + +## Running the Example + +In a separate console tab, install dependencies and start the Vite server: + +```shell +npm i && npm run start +``` + +The browser window will automatically open. +Using the copied multiaddress from the Go server, paste it into the `Server MultiAddress` input and click the `Connect` button. +Once the peer is connected, click the message section will appear. Enter a message and click the `Send` button. + +The output should look like: + +```text +Dialing /ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb +Peer connected '/ip4/10.0.1.5/udp/54375/webrtc/certhash/uEiADy8JubdWrAzseyzfXFyCpdRN02eWZg86tjCrTCA5dbQ/p2p/12D3KooWEG7N4bnZfFBNZE7WG6xm2P4Sr6sonMwyD4HCAqApEthb' +Sending message 'hello' +Received message 'hello' +``` \ No newline at end of file diff --git a/packages/transport-webrtc/examples/browser-to-server/index.html b/packages/transport-webrtc/examples/browser-to-server/index.html new file mode 100644 index 0000000000..24ff11f5bd --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/index.html @@ -0,0 +1,41 @@ + + + + + + js-libp2p WebRTC + + + +
      +
      + + + +
      +
      + + + +
      +
      +
      + + + diff --git a/packages/transport-webrtc/examples/browser-to-server/index.js b/packages/transport-webrtc/examples/browser-to-server/index.js new file mode 100644 index 0000000000..5b9c14d03c --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/index.js @@ -0,0 +1,62 @@ +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import { pipe } from "it-pipe"; +import { fromString, toString } from "uint8arrays"; +import { webRTCDirect } from '@libp2p/webrtc' +import { pushable } from 'it-pushable'; + +let stream; +const output = document.getElementById('output') +const sendSection = document.getElementById('send-section') +const appendOutput = (line) => { + const div = document.createElement("div") + div.appendChild(document.createTextNode(line)) + output.append(div) +} +const clean = (line) => line.replaceAll('\n', '') +const sender = pushable() + +const node = await createLibp2p({ + transports: [webRTCDirect()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: () => { + // by default we refuse to dial local addresses from the browser since they + // are usually sent by remote peers broadcasting undialable multiaddrs but + // here we are explicitly connecting to a local node so do not deny dialing + // any discovered address + return false + } + } +}); + +await node.start() + +node.addEventListener('peer:connect', (connection) => { + appendOutput(`Peer connected '${node.getConnections().map(c => c.remoteAddr.toString())}'`) + sendSection.style.display = 'block' +}) + +window.connect.onclick = async () => { + // TODO!!(ckousik): hack until webrtc is renamed in Go. Remove once + // complete + let candidateMa = window.peer.value + candidateMa = candidateMa.replace(/\/webrtc\/certhash/, "/webrtc-direct/certhash") + const ma = multiaddr(candidateMa) + + appendOutput(`Dialing '${ma}'`) + stream = await node.dialProtocol(ma, ['/echo/1.0.0']) + pipe(sender, stream, async (src) => { + for await(const buf of src) { + const response = toString(buf.subarray()) + appendOutput(`Received message '${clean(response)}'`) + } + }) +} + +window.send.onclick = async () => { + const message = `${window.message.value}\n` + appendOutput(`Sending message '${clean(message)}'`) + sender.push(fromString(message)) +} diff --git a/packages/transport-webrtc/examples/browser-to-server/package.json b/packages/transport-webrtc/examples/browser-to-server/package.json new file mode 100644 index 0000000000..22e88e114e --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/package.json @@ -0,0 +1,25 @@ +{ + "name": "js-libp2p-webrtc-browser-to-server", + "version": "1.0.0", + "description": "Connect a browser to a server", + "type": "module", + "scripts": { + "start": "vite", + "build": "vite build", + "go-libp2p-server": "cd ../go-libp2p-server && go run ./main.go", + "test:chrome": "npm run build && playwright test tests", + "test:firefox": "npm run build && playwright test --browser firefox tests", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.0", + "@libp2p/webrtc": "file:../../", + "@multiformats/multiaddr": "^12.0.0", + "it-pushable": "^3.1.0", + "libp2p": "^0.45.0", + "vite": "^4.2.1" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0" + } +} diff --git a/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js b/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js new file mode 100644 index 0000000000..dcdd25d348 --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/tests/test.spec.js @@ -0,0 +1,94 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' + +// Setup +const test = setup() + +async function spawnGoLibp2p() { + if (!existsSync('../../examples/go-libp2p-server/go-libp2p-server')) { + await new Promise((resolve, reject) => { + exec('go build', + { cwd: '../../examples/go-libp2p-server' }, + (error, stdout, stderr) => { + if (error) { + throw (`exec error: ${error}`) + } + resolve() + }) + }) + } + + const server = spawn('./go-libp2p-server', [], { cwd: '../../examples/go-libp2p-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log(`stderr: ${data}`, typeof data) + }) + const serverAddr = await (new Promise(resolve => { + server.stdout.on('data', (data) => { + console.log(`stdout: ${data}`, typeof data) + const addr = String(data).match(/p2p addr: ([^\s]*)/) + if (addr !== null && addr.length > 0) { + resolve(addr[1]) + } + }) + })) + return { server, serverAddr } +} + +test.describe('bundle ipfs with parceljs:', () => { + // DOM + const connectBtn = '#connect' + const connectAddr = '#peer' + const messageInput = '#message' + const sendBtn = '#send' + const output = '#output' + + let server + let serverAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const s = await spawnGoLibp2p() + server = s.server + serverAddr = s.serverAddr + console.log('Server addr:', serverAddr) + }, {}) + + test.afterAll(() => { + server.kill('SIGINT') + }) + + test.beforeEach(async ({ servers, page }) => { + await page.goto(servers[0].url) + }) + + test('should connect to a go-libp2p node over webrtc', async ({ page }) => { + const message = 'hello' + + // add the go libp2p multiaddress to the input field and submit + await page.fill(connectAddr, serverAddr) + await page.click(connectBtn) + + // send the relay message to the go libp2p server + await page.fill(messageInput, message) + await page.click(sendBtn) + + await page.waitForSelector('#output:has(div)') + + // Expected output: + // + // Dialing '${serverAddr}' + // Peer connected '${serverAddr}' + // Sending message '${message}' + // Received message '${message}' + const connections = await page.textContent(output) + + expect(connections).toContain(`Dialing '${serverAddr}'`) + expect(connections).toContain(`Peer connected '${serverAddr}'`) + + expect(connections).toContain(`Sending message '${message}'`) + expect(connections).toContain(`Received message '${message}'`) + }) +}) diff --git a/packages/transport-webrtc/examples/browser-to-server/vite.config.js b/packages/transport-webrtc/examples/browser-to-server/vite.config.js new file mode 100644 index 0000000000..9b2e2a7b1f --- /dev/null +++ b/packages/transport-webrtc/examples/browser-to-server/vite.config.js @@ -0,0 +1,8 @@ +export default { + build: { + target: 'es2022' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2022', supported: { bigint: true } } + }, +} \ No newline at end of file diff --git a/packages/transport-webrtc/examples/go-libp2p-server/.gitignore b/packages/transport-webrtc/examples/go-libp2p-server/.gitignore new file mode 100644 index 0000000000..baadac2c80 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/.gitignore @@ -0,0 +1 @@ +go-libp2p-server \ No newline at end of file diff --git a/packages/transport-webrtc/examples/go-libp2p-server/go.mod b/packages/transport-webrtc/examples/go-libp2p-server/go.mod new file mode 100644 index 0000000000..0742d25556 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/go.mod @@ -0,0 +1,117 @@ +module github.com/libp2p/js-libp2p-webrtc/examples/go-libp2p-server + +go 1.18 + +// TODO: Remove this once webrtc is merged into Go libp2p +replace github.com/libp2p/go-libp2p => github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3 + +require github.com/libp2p/go-libp2p v0.26.3 + +require ( + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.2 // indirect + github.com/flynn/noise v1.0.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/huin/goupnp v1.1.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.1.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-reuseport v0.2.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.52 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.0 // 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-multiaddr v0.9.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.8.1 // indirect + github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/onsi/ginkgo/v2 v2.9.1 // indirect + github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.5 // indirect + github.com/pion/dtls/v2 v2.1.5 // indirect + github.com/pion/ice/v2 v2.2.13 // indirect + github.com/pion/interceptor v0.1.12 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns v0.0.5 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.10 // indirect + github.com/pion/rtp v1.7.13 // indirect + github.com/pion/sctp v1.8.6 // indirect + github.com/pion/sdp/v3 v3.0.6 // indirect + github.com/pion/srtp/v2 v2.0.11 // indirect + github.com/pion/stun v0.4.0 // indirect + github.com/pion/transport v0.14.1 // indirect + github.com/pion/transport/v2 v2.0.0 // indirect + github.com/pion/turn/v2 v2.0.9 // indirect + github.com/pion/udp v0.1.1 // indirect + github.com/pion/webrtc/v3 v3.1.51 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.2.1 // indirect + github.com/quic-go/qtls-go1-20 v0.1.1 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + github.com/quic-go/webtransport-go v0.5.2 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/dig v1.16.1 // indirect + go.uber.org/fx v1.19.2 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + lukechampine.com/blake3 v1.1.7 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/packages/transport-webrtc/examples/go-libp2p-server/go.sum b/packages/transport-webrtc/examples/go-libp2p-server/go.sum new file mode 100644 index 0000000000..1faf17ae57 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/go.sum @@ -0,0 +1,1174 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= +github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221203041831-ce31453925ec/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= +github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goupnp v1.1.0 h1:gEe0Dp/lZmPZiDFzJJaOfUpOvv2MKUkoBX8lDrn9vKU= +github.com/huin/goupnp v1.1.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ds-badger v0.3.0/go.mod h1:1ke6mXNqeV8K3y5Ak2bAA0osoTfmxUdupVCGm4QUIek= +github.com/ipfs/go-ds-leveldb v0.5.0/go.mod h1:d3XG9RUDzQ6V4SHi8+Xgj9j1XuEk1z82lquxrVbml/Q= +github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= +github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= +github.com/ipfs/go-log/v2 v2.5.0/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= +github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= +github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3 h1:PbtmtrIDY1Us9qeGJdHO1nfp0Jik1KZIP64/KYK43YI= +github.com/libp2p/go-libp2p v0.26.1-0.20230404184453-257fbfba50c3/go.mod h1:PwdLfPiWNhYkb96Wqc2uDFd/+0SGE07/IvjAoFiYG70= +github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-mplex v0.7.0/go.mod h1:rW8ThnRcYWft/Jb2jeORBmPd6xuG3dGxWN/W168L9EU= +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-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k= +github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ= +github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= +github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base32 v0.0.4/go.mod h1:jNLFzjPZtp3aIARHbJRZIaPuspdH0J6q39uUM5pnABM= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= +github.com/multiformats/go-multiaddr v0.6.0/go.mod h1:F4IpaKZuPP360tOMn2Tpyu0At8w23aRyVqeK0DbFeGM= +github.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sggzwC/NcqbDQ= +github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= +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.5.0/go.mod h1:DiY2HFaEp5EhEXb/iYzVAunmyX/aSFMxq2KMKfWEues= +github.com/multiformats/go-multicodec v0.7.0/go.mod h1:GUC8upxSBE4oG+q3kWZRw/+6yC1BqO550bjhWsJbZlw= +github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= +github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= +github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= +github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= +github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c= +github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= +github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M= +github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164= +github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8= +github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= +github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= +github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= +github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= +github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0= +github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= +github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= +github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= +github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o= +github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M= +github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v3 v3.1.51 h1:uU9vHdY63O3uRFJiDskH0qFJ+219bAH28qOt5csSWcM= +github.com/pion/webrtc/v3 v3.1.51/go.mod h1:sbRNshM9l0zRDQgZRP9K5RTzlsdBmqmyO8KbxngG8jQ= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= +github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= +github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= +github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk= +github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= +go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= +go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +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= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +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.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= +lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +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= diff --git a/packages/transport-webrtc/examples/go-libp2p-server/main.go b/packages/transport-webrtc/examples/go-libp2p-server/main.go new file mode 100644 index 0000000000..a5f99e6f25 --- /dev/null +++ b/packages/transport-webrtc/examples/go-libp2p-server/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "os" + "os/signal" + "syscall" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + webrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" +) + +var listenerIp = net.IPv4(127, 0, 0, 1) + +func init() { + ifaces, err := net.Interfaces() + if err != nil { + return + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + return + } + for _, addr := range addrs { + // bind to private non-loopback ip + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.IsPrivate() { + if ipnet.IP.To4() != nil { + listenerIp = ipnet.IP.To4() + return + } + } + } + } +} + +func echoHandler(stream network.Stream) { + for { + reader := bufio.NewReader(stream) + str, err := reader.ReadString('\n') + log.Printf("err: %s", err) + if err != nil { + return + } + log.Printf("echo: %s", str) + _, err = stream.Write([]byte(str)) + if err != nil { + log.Printf("err: %v", err) + return + } + } +} + +func main() { + host := createHost() + host.SetStreamHandler("/echo/1.0.0", echoHandler) + defer host.Close() + remoteInfo := peer.AddrInfo{ + ID: host.ID(), + Addrs: host.Network().ListenAddresses(), + } + + remoteAddrs, _ := peer.AddrInfoToP2pAddrs(&remoteInfo) + fmt.Println("p2p addr: ", remoteAddrs[0]) + + fmt.Println("press Ctrl+C to quit") + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) + <-ch +} + +func createHost() host.Host { + h, err := libp2p.New( + libp2p.Transport(webrtc.New), + libp2p.ListenAddrStrings( + fmt.Sprintf("/ip4/%s/udp/0/webrtc-direct", listenerIp), + ), + libp2p.DisableRelay(), + libp2p.Ping(true), + ) + if err != nil { + panic(err) + } + + return h +} diff --git a/packages/transport-webrtc/package.json b/packages/transport-webrtc/package.json new file mode 100644 index 0000000000..3a5543d2ad --- /dev/null +++ b/packages/transport-webrtc/package.json @@ -0,0 +1,95 @@ +{ + "name": "@libp2p/webrtc", + "version": "2.0.10", + "description": "A libp2p transport using WebRTC connections", + "author": "", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webrtc#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto", + "build": "aegir build", + "test": "aegir test -t browser", + "test:chrome": "aegir test -t browser --cov", + "test:firefox": "aegir test -t browser -- --browser firefox", + "lint": "aegir lint", + "lint:fix": "aegir lint --fix", + "clean": "aegir clean", + "dep-check": "aegir dep-check -i protons" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-metrics": "^4.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-registrar": "^2.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "abortable-iterator": "^5.0.1", + "detect-browser": "^5.3.0", + "it-length-prefixed": "^9.0.1", + "it-pb-stream": "^4.0.1", + "it-pipe": "^3.0.1", + "it-pushable": "^3.1.3", + "it-stream-types": "^2.0.1", + "it-to-buffer": "^4.0.2", + "multiformats": "^11.0.2", + "multihashes": "^4.0.3", + "p-defer": "^4.0.0", + "p-event": "^6.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "@chainsafe/libp2p-yamux": "^4.0.1", + "@libp2p/interface-libp2p": "^3.0.0", + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/peer-id-factory": "^2.0.0", + "@libp2p/websockets": "^6.0.0", + "@types/sinon": "^10.0.15", + "aegir": "^39.0.10", + "delay": "^6.0.0", + "it-length": "^3.0.2", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "libp2p": "^0.45.0", + "protons": "^7.0.2", + "sinon": "^15.1.0", + "sinon-ts": "^1.0.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-webrtc/src/error.ts b/packages/transport-webrtc/src/error.ts new file mode 100644 index 0000000000..360b4e3d73 --- /dev/null +++ b/packages/transport-webrtc/src/error.ts @@ -0,0 +1,122 @@ +import { CodeError } from '@libp2p/interfaces/errors' +import type { Direction } from '@libp2p/interface-connection' + +export enum codes { + ERR_ALREADY_ABORTED = 'ERR_ALREADY_ABORTED', + ERR_DATA_CHANNEL = 'ERR_DATA_CHANNEL', + ERR_CONNECTION_CLOSED = 'ERR_CONNECTION_CLOSED', + ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED', + ERR_INVALID_MULTIADDR = 'ERR_INVALID_MULTIADDR', + ERR_INVALID_FINGERPRINT = 'ERR_INVALID_FINGERPRINT', + ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', + ERR_NOT_IMPLEMENTED = 'ERR_NOT_IMPLEMENTED', + ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS', + ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS', +} + +export class WebRTCTransportError extends CodeError { + constructor (msg: string, code?: string) { + super(`WebRTC transport error: ${msg}`, code ?? '') + this.name = 'WebRTCTransportError' + } +} + +export class ConnectionClosedError extends WebRTCTransportError { + constructor (state: RTCPeerConnectionState, msg: string) { + super(`peerconnection moved to state: ${state}: ${msg}`, codes.ERR_CONNECTION_CLOSED) + this.name = 'WebRTC/ConnectionClosed' + } +} + +export function connectionClosedError (state: RTCPeerConnectionState, msg: string): ConnectionClosedError { + return new ConnectionClosedError(state, msg) +} + +export class DataChannelError extends WebRTCTransportError { + constructor (streamLabel: string, msg: string) { + super(`[stream: ${streamLabel}] data channel error: ${msg}`, codes.ERR_DATA_CHANNEL) + this.name = 'WebRTC/DataChannelError' + } +} + +export function dataChannelError (streamLabel: string, msg: string): DataChannelError { + return new DataChannelError(streamLabel, msg) +} + +export class InappropriateMultiaddrError extends WebRTCTransportError { + constructor (msg: string) { + super(`There was a problem with the Multiaddr which was passed in: ${msg}`, codes.ERR_INVALID_MULTIADDR) + this.name = 'WebRTC/InappropriateMultiaddrError' + } +} + +export function inappropriateMultiaddr (msg: string): InappropriateMultiaddrError { + return new InappropriateMultiaddrError(msg) +} + +export class InvalidArgumentError extends WebRTCTransportError { + constructor (msg: string) { + super(`There was a problem with a provided argument: ${msg}`, codes.ERR_INVALID_PARAMETERS) + this.name = 'WebRTC/InvalidArgumentError' + } +} + +export function invalidArgument (msg: string): InvalidArgumentError { + return new InvalidArgumentError(msg) +} + +export class InvalidFingerprintError extends WebRTCTransportError { + constructor (fingerprint: string, source: string) { + super(`Invalid fingerprint "${fingerprint}" within ${source}`, codes.ERR_INVALID_FINGERPRINT) + this.name = 'WebRTC/InvalidFingerprintError' + } +} + +export function invalidFingerprint (fingerprint: string, source: string): InvalidFingerprintError { + return new InvalidFingerprintError(fingerprint, source) +} + +export class OperationAbortedError extends WebRTCTransportError { + constructor (context: string, abortReason: string) { + super(`Signalled to abort because (${abortReason}}) ${context}`, codes.ERR_ALREADY_ABORTED) + this.name = 'WebRTC/OperationAbortedError' + } +} + +export function operationAborted (context: string, reason: string): OperationAbortedError { + return new OperationAbortedError(context, reason) +} + +export class OverStreamLimitError extends WebRTCTransportError { + constructor (msg: string) { + const code = msg.startsWith('inbound') ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS + super(msg, code) + this.name = 'WebRTC/OverStreamLimitError' + } +} + +export function overStreamLimit (dir: Direction, proto: string): OverStreamLimitError { + return new OverStreamLimitError(`${dir} stream limit reached for protocol - ${proto}`) +} + +export class UnimplementedError extends WebRTCTransportError { + constructor (methodName: string) { + super(`A method (${methodName}) was called though it has been intentionally left unimplemented.`, codes.ERR_NOT_IMPLEMENTED) + this.name = 'WebRTC/UnimplementedError' + } +} + +export function unimplemented (methodName: string): UnimplementedError { + return new UnimplementedError(methodName) +} + +export class UnsupportedHashAlgorithmError extends WebRTCTransportError { + constructor (algo: string) { + super(`unsupported hash algorithm: ${algo}`, codes.ERR_HASH_NOT_SUPPORTED) + this.name = 'WebRTC/UnsupportedHashAlgorithmError' + } +} + +export function unsupportedHashAlgorithm (algorithm: string): UnsupportedHashAlgorithmError { + return new UnsupportedHashAlgorithmError(algorithm) +} diff --git a/packages/transport-webrtc/src/index.ts b/packages/transport-webrtc/src/index.ts new file mode 100644 index 0000000000..b35a16ced4 --- /dev/null +++ b/packages/transport-webrtc/src/index.ts @@ -0,0 +1,31 @@ +import { WebRTCTransport } from './private-to-private/transport.js' +import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirectTransportComponents } from './private-to-public/transport.js' +import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js' +import type { Transport } from '@libp2p/interface-transport' + +/** + * @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration + * @param init.dataChannel - DataChannel configurations + * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) + * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) + * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) + * @returns + */ +function webRTCDirect (init?: WebRTCTransportDirectInit): (components: WebRTCDirectTransportComponents) => Transport { + return (components: WebRTCDirectTransportComponents) => new WebRTCDirectTransport(components, init) +} + +/** + * @param {WebRTCTransportInit} init - WebRTC transport configuration + * @param {RTCConfiguration} init.rtcConfiguration - RTCConfiguration + * @param init.dataChannel - DataChannel configurations + * @param {number} init.dataChannel.maxMessageSize - Max message size that can be sent through the DataChannel. Larger messages will be chunked into smaller messages below this size (default 16kb) + * @param {number} init.dataChannel.maxBufferedAmount - Max buffered amount a DataChannel can have (default 16mb) + * @param {number} init.dataChannel.bufferedAmountLowEventTimeout - If max buffered amount is reached, this is the max time that is waited before the buffer is cleared (default 30 seconds) + * @returns + */ +function webRTC (init?: WebRTCTransportInit): (components: WebRTCTransportComponents) => Transport { + return (components: WebRTCTransportComponents) => new WebRTCTransport(components, init) +} + +export { webRTC, webRTCDirect } diff --git a/packages/transport-webrtc/src/maconn.ts b/packages/transport-webrtc/src/maconn.ts new file mode 100644 index 0000000000..3ce4e3c458 --- /dev/null +++ b/packages/transport-webrtc/src/maconn.ts @@ -0,0 +1,85 @@ +import { logger } from '@libp2p/logger' +import { nopSink, nopSource } from './util.js' +import type { MultiaddrConnection, MultiaddrConnectionTimeline } from '@libp2p/interface-connection' +import type { CounterGroup } from '@libp2p/interface-metrics' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Source, Sink } from 'it-stream-types' + +const log = logger('libp2p:webrtc:connection') + +interface WebRTCMultiaddrConnectionInit { + /** + * WebRTC Peer Connection + */ + peerConnection: RTCPeerConnection + + /** + * The multiaddr address used to communicate with the remote peer + */ + remoteAddr: Multiaddr + + /** + * Holds the relevant events timestamps of the connection + */ + timeline: MultiaddrConnectionTimeline + + /** + * Optional metrics counter group for this connection + */ + metrics?: CounterGroup +} + +export class WebRTCMultiaddrConnection implements MultiaddrConnection { + /** + * WebRTC Peer Connection + */ + readonly peerConnection: RTCPeerConnection + + /** + * The multiaddr address used to communicate with the remote peer + */ + remoteAddr: Multiaddr + + /** + * Holds the lifecycle times of the connection + */ + timeline: MultiaddrConnectionTimeline + + /** + * Optional metrics counter group for this connection + */ + metrics?: CounterGroup + + /** + * The stream source, a no-op as the transport natively supports multiplexing + */ + source: AsyncGenerator = nopSource() + + /** + * The stream destination, a no-op as the transport natively supports multiplexing + */ + sink: Sink, Promise> = nopSink + + constructor (init: WebRTCMultiaddrConnectionInit) { + this.remoteAddr = init.remoteAddr + this.timeline = init.timeline + this.peerConnection = init.peerConnection + + this.peerConnection.onconnectionstatechange = () => { + if (this.peerConnection.connectionState === 'closed' || this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed') { + this.timeline.close = Date.now() + } + } + } + + async close (err?: Error | undefined): Promise { + if (err !== undefined) { + log.error('error closing connection', err) + } + log.trace('closing connection') + + this.timeline.close = Date.now() + this.peerConnection.close() + this.metrics?.increment({ close: true }) + } +} diff --git a/packages/transport-webrtc/src/muxer.ts b/packages/transport-webrtc/src/muxer.ts new file mode 100644 index 0000000000..95f925cf81 --- /dev/null +++ b/packages/transport-webrtc/src/muxer.ts @@ -0,0 +1,165 @@ +import { createStream } from './stream.js' +import { nopSink, nopSource } from './util.js' +import type { DataChannelOpts } from './stream.js' +import type { Stream } from '@libp2p/interface-connection' +import type { CounterGroup } from '@libp2p/interface-metrics' +import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface-stream-muxer' +import type { Source, Sink } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const PROTOCOL = '/webrtc' + +export interface DataChannelMuxerFactoryInit { + /** + * WebRTC Peer Connection + */ + peerConnection: RTCPeerConnection + + /** + * Optional metrics for this data channel muxer + */ + metrics?: CounterGroup + + /** + * Data channel options + */ + dataChannelOptions?: Partial + + /** + * The protocol to use + */ + protocol?: string +} + +export class DataChannelMuxerFactory implements StreamMuxerFactory { + public readonly protocol: string + + /** + * WebRTC Peer Connection + */ + private readonly peerConnection: RTCPeerConnection + private streamBuffer: Stream[] = [] + private readonly metrics?: CounterGroup + private readonly dataChannelOptions?: Partial + + constructor (init: DataChannelMuxerFactoryInit) { + this.peerConnection = init.peerConnection + this.metrics = init.metrics + this.protocol = init.protocol ?? PROTOCOL + this.dataChannelOptions = init.dataChannelOptions + + // store any datachannels opened before upgrade has been completed + this.peerConnection.ondatachannel = ({ channel }) => { + const stream = createStream({ + channel, + direction: 'inbound', + dataChannelOptions: init.dataChannelOptions, + onEnd: () => { + this.streamBuffer = this.streamBuffer.filter(s => s.id !== stream.id) + } + }) + this.streamBuffer.push(stream) + } + } + + createStreamMuxer (init?: StreamMuxerInit): StreamMuxer { + return new DataChannelMuxer({ + ...init, + peerConnection: this.peerConnection, + dataChannelOptions: this.dataChannelOptions, + metrics: this.metrics, + streams: this.streamBuffer, + protocol: this.protocol + }) + } +} + +export interface DataChannelMuxerInit extends DataChannelMuxerFactoryInit, StreamMuxerInit { + streams: Stream[] +} + +/** + * A libp2p data channel stream muxer + */ +export class DataChannelMuxer implements StreamMuxer { + /** + * Array of streams in the data channel + */ + public streams: Stream[] + public protocol: string + + private readonly peerConnection: RTCPeerConnection + private readonly dataChannelOptions?: DataChannelOpts + private readonly metrics?: CounterGroup + + /** + * Close or abort all tracked streams and stop the muxer + */ + close: (err?: Error | undefined) => void = () => { } + + /** + * The stream source, a no-op as the transport natively supports multiplexing + */ + source: AsyncGenerator = nopSource() + + /** + * The stream destination, a no-op as the transport natively supports multiplexing + */ + sink: Sink, Promise> = nopSink + + constructor (readonly init: DataChannelMuxerInit) { + this.streams = init.streams + this.peerConnection = init.peerConnection + this.protocol = init.protocol ?? PROTOCOL + this.metrics = init.metrics + + /** + * Fired when a data channel has been added to the connection has been + * added by the remote peer. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event} + */ + this.peerConnection.ondatachannel = ({ channel }) => { + const stream = createStream({ + channel, + direction: 'inbound', + dataChannelOptions: this.dataChannelOptions, + onEnd: () => { + this.streams = this.streams.filter(s => s.id !== stream.id) + this.metrics?.increment({ stream_end: true }) + init?.onStreamEnd?.(stream) + } + }) + + this.streams.push(stream) + if ((init?.onIncomingStream) != null) { + this.metrics?.increment({ incoming_stream: true }) + init.onIncomingStream(stream) + } + } + + const onIncomingStream = init?.onIncomingStream + if (onIncomingStream != null) { + this.streams.forEach(s => { onIncomingStream(s) }) + } + } + + newStream (): Stream { + // The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label + const channel = this.peerConnection.createDataChannel('') + const stream = createStream({ + channel, + direction: 'outbound', + dataChannelOptions: this.dataChannelOptions, + onEnd: () => { + this.streams = this.streams.filter(s => s.id !== stream.id) + this.metrics?.increment({ stream_end: true }) + this.init?.onStreamEnd?.(stream) + } + }) + this.streams.push(stream) + this.metrics?.increment({ outgoing_stream: true }) + + return stream + } +} diff --git a/packages/transport-webrtc/src/pb/message.proto b/packages/transport-webrtc/src/pb/message.proto new file mode 100644 index 0000000000..9301bd802b --- /dev/null +++ b/packages/transport-webrtc/src/pb/message.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +message Message { + enum Flag { + // The sender will no longer send messages on the stream. + FIN = 0; + + // The sender will no longer read messages on the stream. Incoming data is + // being discarded on receipt. + STOP_SENDING = 1; + + // The sender abruptly terminates the sending part of the stream. The + // receiver can discard any data that it already received on that stream. + RESET = 2; + } + + optional Flag flag = 1; + + optional bytes message = 2; +} diff --git a/packages/transport-webrtc/src/pb/message.ts b/packages/transport-webrtc/src/pb/message.ts new file mode 100644 index 0000000000..a74ca6dd06 --- /dev/null +++ b/packages/transport-webrtc/src/pb/message.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Message { + flag?: Message.Flag + message?: Uint8Array +} + +export namespace Message { + export enum Flag { + FIN = 'FIN', + STOP_SENDING = 'STOP_SENDING', + RESET = 'RESET' + } + + enum __FlagValues { + FIN = 0, + STOP_SENDING = 1, + RESET = 2 + } + + export namespace Flag { + export const codec = (): Codec => { + return enumeration(__FlagValues) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.flag != null) { + w.uint32(8) + Message.Flag.codec().encode(obj.flag, w) + } + + if (obj.message != null) { + w.uint32(18) + w.bytes(obj.message) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.flag = Message.Flag.codec().decode(reader) + break + case 2: + obj.message = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/handler.ts b/packages/transport-webrtc/src/private-to-private/handler.ts new file mode 100644 index 0000000000..176031b4fe --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/handler.ts @@ -0,0 +1,156 @@ +import { logger } from '@libp2p/logger' +import { abortableDuplex } from 'abortable-iterator' +import { pbStream } from 'it-pb-stream' +import pDefer, { type DeferredPromise } from 'p-defer' +import { DataChannelMuxerFactory } from '../muxer.js' +import { Message } from './pb/message.js' +import { readCandidatesUntilConnected, resolveOnConnected } from './util.js' +import type { DataChannelOpts } from '../stream.js' +import type { Stream } from '@libp2p/interface-connection' +import type { IncomingStreamData } from '@libp2p/interface-registrar' +import type { StreamMuxerFactory } from '@libp2p/interface-stream-muxer' + +const DEFAULT_TIMEOUT = 30 * 1000 + +const log = logger('libp2p:webrtc:peer') + +export type IncomingStreamOpts = { rtcConfiguration?: RTCConfiguration, dataChannelOptions?: Partial } & IncomingStreamData + +export async function handleIncomingStream ({ rtcConfiguration, dataChannelOptions, stream: rawStream }: IncomingStreamOpts): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { + const signal = AbortSignal.timeout(DEFAULT_TIMEOUT) + const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) + const pc = new RTCPeerConnection(rtcConfiguration) + const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) + const connectedPromise: DeferredPromise = pDefer() + const answerSentPromise: DeferredPromise = pDefer() + + signal.onabort = () => { connectedPromise.reject() } + // candidate callbacks + pc.onicecandidate = ({ candidate }) => { + answerSentPromise.promise.then( + () => { + stream.write({ + type: Message.Type.ICE_CANDIDATE, + data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' + }) + }, + (err) => { + log.error('cannot set candidate since sending answer failed', err) + } + ) + } + + resolveOnConnected(pc, connectedPromise) + + // read an SDP offer + const pbOffer = await stream.read() + if (pbOffer.type !== Message.Type.SDP_OFFER) { + throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `) + } + const offer = new RTCSessionDescription({ + type: 'offer', + sdp: pbOffer.data + }) + + await pc.setRemoteDescription(offer).catch(err => { + log.error('could not execute setRemoteDescription', err) + throw new Error('Failed to set remoteDescription') + }) + + // create and write an SDP answer + const answer = await pc.createAnswer().catch(err => { + log.error('could not execute createAnswer', err) + answerSentPromise.reject(err) + throw new Error('Failed to create answer') + }) + // write the answer to the remote + stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }) + + await pc.setLocalDescription(answer).catch(err => { + log.error('could not execute setLocalDescription', err) + answerSentPromise.reject(err) + throw new Error('Failed to set localDescription') + }) + + answerSentPromise.resolve() + + // wait until candidates are connected + await readCandidatesUntilConnected(connectedPromise, pc, stream) + + const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') + + return { pc, muxerFactory, remoteAddress } +} + +export interface ConnectOptions { + stream: Stream + signal: AbortSignal + rtcConfiguration?: RTCConfiguration + dataChannelOptions?: Partial +} + +export async function initiateConnection ({ rtcConfiguration, dataChannelOptions, signal, stream: rawStream }: ConnectOptions): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { + const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) + // setup peer connection + const pc = new RTCPeerConnection(rtcConfiguration) + const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) + + const connectedPromise: DeferredPromise = pDefer() + resolveOnConnected(pc, connectedPromise) + + // reject the connectedPromise if the signal aborts + signal.onabort = connectedPromise.reject + // we create the channel so that the peerconnection has a component for which + // to collect candidates. The label is not relevant to connection initiation + // but can be useful for debugging + const channel = pc.createDataChannel('init') + // setup callback to write ICE candidates to the remote + // peer + pc.onicecandidate = ({ candidate }) => { + stream.write({ + type: Message.Type.ICE_CANDIDATE, + data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' + }) + } + // create an offer + const offerSdp = await pc.createOffer() + // write the offer to the stream + stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }) + // set offer as local description + await pc.setLocalDescription(offerSdp).catch(err => { + log.error('could not execute setLocalDescription', err) + throw new Error('Failed to set localDescription') + }) + + // read answer + const answerMessage = await stream.read() + if (answerMessage.type !== Message.Type.SDP_ANSWER) { + throw new Error('remote should send an SDP answer') + } + + const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data }) + await pc.setRemoteDescription(answerSdp).catch(err => { + log.error('could not execute setRemoteDescription', err) + throw new Error('Failed to set remoteDescription') + }) + + await readCandidatesUntilConnected(connectedPromise, pc, stream) + channel.close() + + const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') + + return { pc, muxerFactory, remoteAddress } +} + +function parseRemoteAddress (sdp: string): string { + // 'a=candidate:1746876089 1 udp 2113937151 0614fbad-b...ocal 54882 typ host generation 0 network-cost 999' + const candidateLine = sdp.split('\r\n').filter(line => line.startsWith('a=candidate')).pop() + const candidateParts = candidateLine?.split(' ') + + if (candidateLine == null || candidateParts == null || candidateParts.length < 5) { + log('could not parse remote address from', candidateLine) + return '/webrtc' + } + + return `/dnsaddr/${candidateParts[4]}/${candidateParts[2].toLowerCase()}/${candidateParts[3]}/webrtc` +} diff --git a/packages/transport-webrtc/src/private-to-private/listener.ts b/packages/transport-webrtc/src/private-to-private/listener.ts new file mode 100644 index 0000000000..018e569264 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/listener.ts @@ -0,0 +1,43 @@ +import { EventEmitter } from '@libp2p/interfaces/events' +import { Circuit } from '@multiformats/mafmt' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { ListenerEvents, Listener, TransportManager } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface ListenerOptions { + peerId: PeerId + transportManager: TransportManager +} + +export class WebRTCPeerListener extends EventEmitter implements Listener { + private readonly peerId: PeerId + private readonly transportManager: TransportManager + + constructor (opts: ListenerOptions) { + super() + + this.peerId = opts.peerId + this.transportManager = opts.transportManager + } + + async listen (): Promise { + this.safeDispatchEvent('listening', {}) + } + + getAddrs (): Multiaddr[] { + return this.transportManager + .getListeners() + .filter(l => l !== this) + .map(l => l.getAddrs() + .filter(ma => Circuit.matches(ma)) + .map(ma => { + return ma.encapsulate(`/webrtc/p2p/${this.peerId}`) + }) + ) + .flat() + } + + async close (): Promise { + this.safeDispatchEvent('close', {}) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/pb/message.proto b/packages/transport-webrtc/src/private-to-private/pb/message.proto new file mode 100644 index 0000000000..ac2fa68ca6 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/pb/message.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +message Message { + // Specifies type in `data` field. + enum Type { + // String of `RTCSessionDescription.sdp` + SDP_OFFER = 0; + // String of `RTCSessionDescription.sdp` + SDP_ANSWER = 1; + // String of `RTCIceCandidate.toJSON()` + ICE_CANDIDATE = 2; + } + + optional Type type = 1; + optional string data = 2; +} \ No newline at end of file diff --git a/packages/transport-webrtc/src/private-to-private/pb/message.ts b/packages/transport-webrtc/src/private-to-private/pb/message.ts new file mode 100644 index 0000000000..b0824ed042 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/pb/message.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface Message { + type?: Message.Type + data?: string +} + +export namespace Message { + export enum Type { + SDP_OFFER = 'SDP_OFFER', + SDP_ANSWER = 'SDP_ANSWER', + ICE_CANDIDATE = 'ICE_CANDIDATE' + } + + enum __TypeValues { + SDP_OFFER = 0, + SDP_ANSWER = 1, + ICE_CANDIDATE = 2 + } + + export namespace Type { + export const codec = (): Codec => { + return enumeration(__TypeValues) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.type != null) { + w.uint32(8) + Message.Type.codec().encode(obj.type, w) + } + + if (obj.data != null) { + w.uint32(18) + w.string(obj.data) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = Message.Type.codec().decode(reader) + break + case 2: + obj.data = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Message.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Message => { + return decodeMessage(buf, Message.codec()) + } +} diff --git a/packages/transport-webrtc/src/private-to-private/transport.ts b/packages/transport-webrtc/src/private-to-private/transport.ts new file mode 100644 index 0000000000..196854d24e --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/transport.ts @@ -0,0 +1,184 @@ +import { type CreateListenerOptions, type DialOptions, type Listener, symbol, type Transport, type Upgrader, type TransportManager } from '@libp2p/interface-transport' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr' +import { codes } from '../error.js' +import { WebRTCMultiaddrConnection } from '../maconn.js' +import { initiateConnection, handleIncomingStream } from './handler.js' +import { WebRTCPeerListener } from './listener.js' +import type { DataChannelOpts } from '../stream.js' +import type { Connection } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { IncomingStreamData, Registrar } from '@libp2p/interface-registrar' +import type { Startable } from '@libp2p/interfaces/startable' + +const log = logger('libp2p:webrtc:peer') + +const WEBRTC_TRANSPORT = '/webrtc' +const CIRCUIT_RELAY_TRANSPORT = '/p2p-circuit' +const SIGNALING_PROTO_ID = '/webrtc-signaling/0.0.1' +const WEBRTC_CODE = protocols('webrtc').code + +export interface WebRTCTransportInit { + rtcConfiguration?: RTCConfiguration + dataChannel?: Partial +} + +export interface WebRTCTransportComponents { + peerId: PeerId + registrar: Registrar + upgrader: Upgrader + transportManager: TransportManager +} + +export class WebRTCTransport implements Transport, Startable { + private _started = false + + constructor ( + private readonly components: WebRTCTransportComponents, + private readonly init: WebRTCTransportInit = {} + ) { + } + + isStarted (): boolean { + return this._started + } + + async start (): Promise { + await this.components.registrar.handle(SIGNALING_PROTO_ID, (data: IncomingStreamData) => { + this._onProtocol(data).catch(err => { log.error('failed to handle incoming connect from %p', data.connection.remotePeer, err) }) + }) + this._started = true + } + + async stop (): Promise { + await this.components.registrar.unhandle(SIGNALING_PROTO_ID) + this._started = false + } + + createListener (options: CreateListenerOptions): Listener { + return new WebRTCPeerListener(this.components) + } + + readonly [Symbol.toStringTag] = '@libp2p/webrtc' + + readonly [symbol] = true + + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + const codes = ma.protoCodes() + return codes.includes(WEBRTC_CODE) + }) + } + + /* + * dial connects to a remote via the circuit relay or any other protocol + * and proceeds to upgrade to a webrtc connection. + * multiaddr of the form: /webrtc/p2p/ + * For a circuit relay, this will be of the form + * /p2p//p2p-circuit/webrtc/p2p/ + */ + async dial (ma: Multiaddr, options: DialOptions): Promise { + log.trace('dialing address: ', ma) + const { baseAddr, peerId } = splitAddr(ma) + + if (options.signal == null) { + const controller = new AbortController() + options.signal = controller.signal + } + + const connection = await this.components.transportManager.dial(baseAddr, options) + const signalingStream = await connection.newStream([SIGNALING_PROTO_ID], options) + + try { + const { pc, muxerFactory, remoteAddress } = await initiateConnection({ + stream: signalingStream, + rtcConfiguration: this.init.rtcConfiguration, + dataChannelOptions: this.init.dataChannel, + signal: options.signal + }) + + const result = await options.upgrader.upgradeOutbound( + new WebRTCMultiaddrConnection({ + peerConnection: pc, + timeline: { open: Date.now() }, + remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`) + }), + { + skipProtection: true, + skipEncryption: true, + muxerFactory + } + ) + + // close the stream if SDP has been exchanged successfully + signalingStream.close() + return result + } catch (err) { + // reset the stream in case of any error + signalingStream.reset() + throw err + } finally { + // Close the signaling connection + await connection.close() + } + } + + async _onProtocol ({ connection, stream }: IncomingStreamData): Promise { + try { + const { pc, muxerFactory, remoteAddress } = await handleIncomingStream({ + rtcConfiguration: this.init.rtcConfiguration, + connection, + stream, + dataChannelOptions: this.init.dataChannel + }) + + await this.components.upgrader.upgradeInbound(new WebRTCMultiaddrConnection({ + peerConnection: pc, + timeline: { open: (new Date()).getTime() }, + remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${connection.remotePeer.toString()}`) + }), { + skipEncryption: true, + skipProtection: true, + muxerFactory + }) + } catch (err) { + stream.reset() + throw err + } finally { + // Close the signaling connection + await connection.close() + } + } +} + +export function splitAddr (ma: Multiaddr): { baseAddr: Multiaddr, peerId: PeerId } { + const addrs = ma.toString().split(WEBRTC_TRANSPORT + '/') + if (addrs.length !== 2) { + throw new CodeError('webrtc protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) + } + + if (!addrs[0].includes(CIRCUIT_RELAY_TRANSPORT)) { + throw new CodeError('p2p-circuit protocol was not present in multiaddr', codes.ERR_INVALID_MULTIADDR) + } + + // look for remote peerId + let remoteAddr = multiaddr(addrs[0]) + const destination = multiaddr('/' + addrs[1]) + + const destinationIdString = destination.getPeerId() + if (destinationIdString == null) { + throw new CodeError('destination peer id was missing', codes.ERR_INVALID_MULTIADDR) + } + + const lastProtoInRemote = remoteAddr.protos().pop() + if (lastProtoInRemote === undefined) { + throw new CodeError('invalid multiaddr', codes.ERR_INVALID_MULTIADDR) + } + if (lastProtoInRemote.name !== 'p2p') { + remoteAddr = remoteAddr.encapsulate(`/p2p/${destinationIdString}`) + } + + return { baseAddr: remoteAddr, peerId: peerIdFromString(destinationIdString) } +} diff --git a/packages/transport-webrtc/src/private-to-private/util.ts b/packages/transport-webrtc/src/private-to-private/util.ts new file mode 100644 index 0000000000..e1b669778c --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/util.ts @@ -0,0 +1,59 @@ +import { logger } from '@libp2p/logger' +import { isFirefox } from '../util.js' +import { Message } from './pb/message.js' +import type { DeferredPromise } from 'p-defer' + +interface MessageStream { + read: () => Promise + write: (d: Message) => void | Promise +} + +const log = logger('libp2p:webrtc:peer:util') + +export const readCandidatesUntilConnected = async (connectedPromise: DeferredPromise, pc: RTCPeerConnection, stream: MessageStream): Promise => { + while (true) { + const readResult = await Promise.race([connectedPromise.promise, stream.read()]) + // check if readResult is a message + if (readResult instanceof Object) { + const message = readResult + if (message.type !== Message.Type.ICE_CANDIDATE) { + throw new Error('expected only ice candidates') + } + // end of candidates has been signalled + if (message.data == null || message.data === '') { + log.trace('end-of-candidates received') + break + } + + log.trace('received new ICE candidate: %s', message.data) + try { + await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(message.data))) + } catch (err) { + log.error('bad candidate received: ', err) + throw new Error('bad candidate received') + } + } else { + // connected promise resolved + break + } + } + await connectedPromise.promise +} + +export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredPromise): void { + pc[isFirefox ? 'oniceconnectionstatechange' : 'onconnectionstatechange'] = (_) => { + log.trace('receiver peerConnectionState state: ', pc.connectionState) + switch (isFirefox ? pc.iceConnectionState : pc.connectionState) { + case 'connected': + promise.resolve() + break + case 'failed': + case 'disconnected': + case 'closed': + promise.reject(new Error('RTCPeerConnection was closed')) + break + default: + break + } + } +} diff --git a/packages/transport-webrtc/src/private-to-public/options.ts b/packages/transport-webrtc/src/private-to-public/options.ts new file mode 100644 index 0000000000..838c627ffe --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/options.ts @@ -0,0 +1,4 @@ +import type { CreateListenerOptions, DialOptions } from '@libp2p/interface-transport' + +export interface WebRTCListenerOptions extends CreateListenerOptions {} +export interface WebRTCDialOptions extends DialOptions {} diff --git a/packages/transport-webrtc/src/private-to-public/sdp.ts b/packages/transport-webrtc/src/private-to-public/sdp.ts new file mode 100644 index 0000000000..474455021f --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/sdp.ts @@ -0,0 +1,162 @@ +import { logger } from '@libp2p/logger' +import { bases } from 'multiformats/basics' +import * as multihashes from 'multihashes' +import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from '../error.js' +import { CERTHASH_CODE } from './transport.js' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { HashCode, HashName } from 'multihashes' + +const log = logger('libp2p:webrtc:sdp') + +/** + * Get base2 | identity decoders + */ +// @ts-expect-error - Not easy to combine these types. +export const mbdecoder: any = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b)) + +export function getLocalFingerprint (pc: RTCPeerConnection): string | undefined { + // try to fetch fingerprint from local certificate + const localCert = pc.getConfiguration().certificates?.at(0) + if (localCert == null || localCert.getFingerprints == null) { + log.trace('fetching fingerprint from local SDP') + const localDescription = pc.localDescription + if (localDescription == null) { + return undefined + } + return getFingerprintFromSdp(localDescription.sdp) + } + + log.trace('fetching fingerprint from local certificate') + + if (localCert.getFingerprints().length === 0) { + return undefined + } + + const fingerprint = localCert.getFingerprints()[0].value + if (fingerprint == null) { + throw invalidFingerprint('', 'no fingerprint on local certificate') + } + + return fingerprint +} + +const fingerprintRegex = /^a=fingerprint:(?:\w+-[0-9]+)\s(?(:?[0-9a-fA-F]{2})+)$/m +export function getFingerprintFromSdp (sdp: string): string | undefined { + const searchResult = sdp.match(fingerprintRegex) + return searchResult?.groups?.fingerprint +} +/** + * Get base2 | identity decoders + */ +function ipv (ma: Multiaddr): string { + for (const proto of ma.protoNames()) { + if (proto.startsWith('ip')) { + return proto.toUpperCase() + } + } + + log('Warning: multiaddr does not appear to contain IP4 or IP6, defaulting to IP6', ma) + + return 'IP6' +} + +// Extract the certhash from a multiaddr +export function certhash (ma: Multiaddr): string { + const tups = ma.stringTuples() + const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0] + + if (certhash === undefined || certhash === '') { + throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`) + } + + return certhash +} + +/** + * Convert a certhash into a multihash + */ +export function decodeCerthash (certhash: string): { code: HashCode, name: HashName, length: number, digest: Uint8Array } { + const mbdecoded = mbdecoder.decode(certhash) + return multihashes.decode(mbdecoded) +} + +/** + * Extract the fingerprint from a multiaddr + */ +export function ma2Fingerprint (ma: Multiaddr): string[] { + const mhdecoded = decodeCerthash(certhash(ma)) + const prefix = toSupportedHashFunction(mhdecoded.name) + const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') + const sdp = fingerprint.match(/.{1,2}/g) + + if (sdp == null) { + throw invalidFingerprint(fingerprint, ma.toString()) + } + + return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fingerprint] +} + +/** + * Normalize the hash name from a given multihash has name + */ +export function toSupportedHashFunction (name: multihashes.HashName): string { + switch (name) { + case 'sha1': + return 'sha-1' + case 'sha2-256': + return 'sha-256' + case 'sha2-512': + return 'sha-512' + default: + throw unsupportedHashAlgorithm(name) + } +} + +/** + * Convert a multiaddr into a SDP + */ +function ma2sdp (ma: Multiaddr, ufrag: string): string { + const { host, port } = ma.toOptions() + const ipVersion = ipv(ma) + const [CERTFP] = ma2Fingerprint(ma) + + return `v=0 +o=- 0 0 IN ${ipVersion} ${host} +s=- +c=IN ${ipVersion} ${host} +t=0 0 +a=ice-lite +m=application ${port} UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:${ufrag} +a=ice-pwd:${ufrag} +a=fingerprint:${CERTFP} +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n` +} + +/** + * Create an answer SDP from a multiaddr + */ +export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit { + return { + type: 'answer', + sdp: ma2sdp(ma, ufrag) + } +} + +/** + * Replace (munge) the ufrag and password values in a SDP + */ +export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit { + if (desc.sdp === undefined) { + throw invalidArgument("Can't munge a missing SDP") + } + + desc.sdp = desc.sdp + .replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n') + .replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n') + return desc +} diff --git a/packages/transport-webrtc/src/private-to-public/transport.ts b/packages/transport-webrtc/src/private-to-public/transport.ts new file mode 100644 index 0000000000..c23b715c88 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/transport.ts @@ -0,0 +1,280 @@ +import { noise as Noise } from '@chainsafe/libp2p-noise' +import { type CreateListenerOptions, type Listener, symbol, type Transport } from '@libp2p/interface-transport' +import { logger } from '@libp2p/logger' +import * as p from '@libp2p/peer-id' +import { protocols } from '@multiformats/multiaddr' +import * as multihashes from 'multihashes' +import { concat } from 'uint8arrays/concat' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' +import { dataChannelError, inappropriateMultiaddr, unimplemented, invalidArgument } from '../error.js' +import { WebRTCMultiaddrConnection } from '../maconn.js' +import { DataChannelMuxerFactory } from '../muxer.js' +import { createStream } from '../stream.js' +import { isFirefox } from '../util.js' +import * as sdp from './sdp.js' +import { genUfrag } from './util.js' +import type { WebRTCDialOptions } from './options.js' +import type { DataChannelOpts } from '../stream.js' +import type { Connection } from '@libp2p/interface-connection' +import type { CounterGroup, Metrics } from '@libp2p/interface-metrics' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:webrtc:transport') + +/** + * The time to wait, in milliseconds, for the data channel handshake to complete + */ +const HANDSHAKE_TIMEOUT_MS = 10_000 + +/** + * Created by converting the hexadecimal protocol code to an integer. + * + * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} + */ +export const WEBRTC_CODE: number = protocols('webrtc-direct').code + +/** + * Created by converting the hexadecimal protocol code to an integer. + * + * {@link https://github.com/multiformats/multiaddr/blob/master/protocols.csv} + */ +export const CERTHASH_CODE: number = protocols('certhash').code + +/** + * The peer for this transport + */ +export interface WebRTCDirectTransportComponents { + peerId: PeerId + metrics?: Metrics +} + +export interface WebRTCMetrics { + dialerEvents: CounterGroup +} + +export interface WebRTCTransportDirectInit { + dataChannel?: Partial +} + +export class WebRTCDirectTransport implements Transport { + private readonly metrics?: WebRTCMetrics + private readonly components: WebRTCDirectTransportComponents + private readonly init: WebRTCTransportDirectInit + constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) { + this.components = components + this.init = init + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_dialer_events_total', { + label: 'event', + help: 'Total count of WebRTC dial events by type' + }) + } + } + } + + /** + * Dial a given multiaddr + */ + async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const rawConn = await this._connect(ma, options) + log(`dialing address - ${ma.toString()}`) + return rawConn + } + + /** + * Create transport listeners no supported by browsers + */ + createListener (options: CreateListenerOptions): Listener { + throw unimplemented('WebRTCTransport.createListener') + } + + /** + * Takes a list of `Multiaddr`s and returns only valid addresses for the transport + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(validMa) + } + + /** + * Implement toString() for WebRTCTransport + */ + readonly [Symbol.toStringTag] = '@libp2p/webrtc-direct' + + /** + * Symbol.for('@libp2p/transport') + */ + readonly [symbol] = true + + /** + * Connect to a peer using a multiaddr + */ + async _connect (ma: Multiaddr, options: WebRTCDialOptions): Promise { + const controller = new AbortController() + const signal = controller.signal + + const remotePeerString = ma.getPeerId() + if (remotePeerString === null) { + throw inappropriateMultiaddr("we need to have the remote's PeerId") + } + const theirPeerId = p.peerIdFromString(remotePeerString) + + const remoteCerthash = sdp.decodeCerthash(sdp.certhash(ma)) + + // ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic + // curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384 + // was not supported in Chromium). We use the same hash function as found in the + // multiaddr if it is supported. + const certificate = await RTCPeerConnection.generateCertificate({ + name: 'ECDSA', + namedCurve: 'P-256', + hash: sdp.toSupportedHashFunction(remoteCerthash.name) + } as any) + + const peerConnection = new RTCPeerConnection({ certificates: [certificate] }) + + // create data channel for running the noise handshake. Once the data channel is opened, + // the remote will initiate the noise handshake. This is used to confirm the identity of + // the peer. + const dataChannelOpenPromise = new Promise((resolve, reject) => { + const handshakeDataChannel = peerConnection.createDataChannel('', { negotiated: true, id: 0 }) + const handshakeTimeout = setTimeout(() => { + const error = `Data channel was never opened: state: ${handshakeDataChannel.readyState}` + log.error(error) + this.metrics?.dialerEvents.increment({ open_error: true }) + reject(dataChannelError('data', error)) + }, HANDSHAKE_TIMEOUT_MS) + + handshakeDataChannel.onopen = (_) => { + clearTimeout(handshakeTimeout) + resolve(handshakeDataChannel) + } + + // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/error_event + handshakeDataChannel.onerror = (event: Event) => { + clearTimeout(handshakeTimeout) + const errorTarget = event.target?.toString() ?? 'not specified' + const error = `Error opening a data channel for handshaking: ${errorTarget}` + log.error(error) + // NOTE: We use unknown error here but this could potentially be considered a reset by some standards. + this.metrics?.dialerEvents.increment({ unknown_error: true }) + reject(dataChannelError('data', error)) + } + }) + + const ufrag = 'libp2p+webrtc+v1/' + genUfrag(32) + + // Create offer and munge sdp with ufrag == pwd. This allows the remote to + // respond to STUN messages without performing an actual SDP exchange. + // This is because it can infer the passwd field by reading the USERNAME + // attribute of the STUN message. + const offerSdp = await peerConnection.createOffer() + const mungedOfferSdp = sdp.munge(offerSdp, ufrag) + await peerConnection.setLocalDescription(mungedOfferSdp) + + // construct answer sdp from multiaddr and ufrag + const answerSdp = sdp.fromMultiAddr(ma, ufrag) + await peerConnection.setRemoteDescription(answerSdp) + + // wait for peerconnection.onopen to fire, or for the datachannel to open + const handshakeDataChannel = await dataChannelOpenPromise + + const myPeerId = this.components.peerId + + // Do noise handshake. + // Set the Noise Prologue to libp2p-webrtc-noise: before starting the actual Noise handshake. + // is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order. + const fingerprintsPrologue = this.generateNoisePrologue(peerConnection, remoteCerthash.code, ma) + + // Since we use the default crypto interface and do not use a static key or early data, + // we pass in undefined for these parameters. + const noise = Noise({ prologueBytes: fingerprintsPrologue })() + + const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel }) + const wrappedDuplex = { + ...wrappedChannel, + sink: wrappedChannel.sink.bind(wrappedChannel), + source: (async function * () { + for await (const list of wrappedChannel.source) { + for (const buf of list) { + yield buf + } + } + }()) + } + + // Creating the connection before completion of the noise + // handshake ensures that the stream opening callback is set up + const maConn = new WebRTCMultiaddrConnection({ + peerConnection, + remoteAddr: ma, + timeline: { + open: Date.now() + }, + metrics: this.metrics?.dialerEvents + }) + + const eventListeningName = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange' + + peerConnection.addEventListener(eventListeningName, () => { + switch (peerConnection.connectionState) { + case 'failed': + case 'disconnected': + case 'closed': + maConn.close().catch((err) => { + log.error('error closing connection', err) + }).finally(() => { + // Remove the event listener once the connection is closed + controller.abort() + }) + break + default: + break + } + }, { signal }) + + // Track opened peer connection + this.metrics?.dialerEvents.increment({ peer_connection: true }) + + const muxerFactory = new DataChannelMuxerFactory({ peerConnection, metrics: this.metrics?.dialerEvents, dataChannelOptions: this.init.dataChannel }) + + // For outbound connections, the remote is expected to start the noise handshake. + // Therefore, we need to secure an inbound noise connection from the remote. + await noise.secureInbound(myPeerId, wrappedDuplex, theirPeerId) + + return options.upgrader.upgradeOutbound(maConn, { skipProtection: true, skipEncryption: true, muxerFactory }) + } + + /** + * Generate a noise prologue from the peer connection's certificate. + * noise prologue = bytes('libp2p-webrtc-noise:') + noise-responder fingerprint + noise-initiator fingerprint + */ + private generateNoisePrologue (pc: RTCPeerConnection, hashCode: multihashes.HashCode, ma: Multiaddr): Uint8Array { + if (pc.getConfiguration().certificates?.length === 0) { + throw invalidArgument('no local certificate') + } + + const localFingerprint = sdp.getLocalFingerprint(pc) + if (localFingerprint == null) { + throw invalidArgument('no local fingerprint found') + } + + const localFpString = localFingerprint.trim().toLowerCase().replaceAll(':', '') + const localFpArray = uint8arrayFromString(localFpString, 'hex') + const local = multihashes.encode(localFpArray, hashCode) + const remote: Uint8Array = sdp.mbdecoder.decode(sdp.certhash(ma)) + const prefix = uint8arrayFromString('libp2p-webrtc-noise:') + + return concat([prefix, local, remote]) + } +} + +/** + * Determine if a given multiaddr contains a WebRTC Code (280), + * a Certhash Code (466) and a PeerId + */ +function validMa (ma: Multiaddr): boolean { + const codes = ma.protoCodes() + return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null && !codes.includes(protocols('p2p-circuit').code) +} diff --git a/packages/transport-webrtc/src/private-to-public/util.ts b/packages/transport-webrtc/src/private-to-public/util.ts new file mode 100644 index 0000000000..6ef40af9df --- /dev/null +++ b/packages/transport-webrtc/src/private-to-public/util.ts @@ -0,0 +1,3 @@ + +const charset = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') +export const genUfrag = (len: number): string => [...Array(len)].map(() => charset.at(Math.floor(Math.random() * charset.length))).join('') diff --git a/packages/transport-webrtc/src/stream.ts b/packages/transport-webrtc/src/stream.ts new file mode 100644 index 0000000000..fe5482bca3 --- /dev/null +++ b/packages/transport-webrtc/src/stream.ts @@ -0,0 +1,273 @@ +import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import * as lengthPrefixed from 'it-length-prefixed' +import { type Pushable, pushable } from 'it-pushable' +import { pEvent, TimeoutError } from 'p-event' +import { Uint8ArrayList } from 'uint8arraylist' +import { Message } from './pb/message.js' +import type { Direction, Stream } from '@libp2p/interface-connection' + +const log = logger('libp2p:webrtc:stream') + +export interface DataChannelOpts { + maxMessageSize: number + maxBufferedAmount: number + bufferedAmountLowEventTimeout: number +} + +export interface WebRTCStreamInit extends AbstractStreamInit { + /** + * The network channel used for bidirectional peer-to-peer transfers of + * arbitrary data + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} + */ + channel: RTCDataChannel + + dataChannelOptions?: Partial +} + +// Max message size that can be sent to the DataChannel +const MAX_MESSAGE_SIZE = 16 * 1024 + +// How much can be buffered to the DataChannel at once +const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 + +// How long time we wait for the 'bufferedamountlow' event to be emitted +const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000 + +// protobuf field definition overhead +const PROTOBUF_OVERHEAD = 3 + +class WebRTCStream extends AbstractStream { + /** + * The data channel used to send and receive data + */ + private readonly channel: RTCDataChannel + + /** + * Data channel options + */ + private readonly dataChannelOptions: DataChannelOpts + + /** + * push data from the underlying datachannel to the length prefix decoder + * and then the protobuf decoder. + */ + private readonly incomingData: Pushable + + private messageQueue?: Uint8ArrayList + + constructor (init: WebRTCStreamInit) { + super(init) + + this.channel = init.channel + this.channel.binaryType = 'arraybuffer' + this.incomingData = pushable() + this.messageQueue = new Uint8ArrayList() + this.dataChannelOptions = { + bufferedAmountLowEventTimeout: init.dataChannelOptions?.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT, + maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT, + maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE + } + + // set up initial state + switch (this.channel.readyState) { + case 'open': + break + + case 'closed': + case 'closing': + if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) { + this.stat.timeline.close = Date.now() + } + break + case 'connecting': + // noop + break + + default: + log.error('unknown datachannel state %s', this.channel.readyState) + throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') + } + + // handle RTCDataChannel events + this.channel.onopen = (_evt) => { + this.stat.timeline.open = new Date().getTime() + + if (this.messageQueue != null) { + // send any queued messages + this._sendMessage(this.messageQueue) + .catch(err => { + this.abort(err) + }) + this.messageQueue = undefined + } + } + + this.channel.onclose = (_evt) => { + this.close() + } + + this.channel.onerror = (evt) => { + const err = (evt as RTCErrorEvent).error + this.abort(err) + } + + const self = this + + this.channel.onmessage = async (event: MessageEvent) => { + const { data } = event + + if (data === null || data.byteLength === 0) { + return + } + + this.incomingData.push(new Uint8Array(data, 0, data.byteLength)) + } + + // pipe framed protobuf messages through a length prefixed decoder, and + // surface data from the `Message.message` field through a source. + Promise.resolve().then(async () => { + for await (const buf of lengthPrefixed.decode(this.incomingData)) { + const message = self.processIncomingProtobuf(buf.subarray()) + + if (message != null) { + self.sourcePush(new Uint8ArrayList(message)) + } + } + }) + .catch(err => { + log.error('error processing incoming data channel messages', err) + }) + } + + sendNewStream (): void { + // opening new streams is handled by WebRTC so this is a noop + } + + async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise { + if (checkBuffer && this.channel.bufferedAmount > this.dataChannelOptions.maxBufferedAmount) { + try { + await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout }) + } catch (err: any) { + if (err instanceof TimeoutError) { + this.abort(err) + throw new Error('Timed out waiting for DataChannel buffer to clear') + } + + throw err + } + } + + if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') { + throw new CodeError('Invalid datachannel state - closed or closing', 'ERR_INVALID_STATE') + } + + if (this.channel.readyState === 'open') { + // send message without copying data + for (const buf of data) { + this.channel.send(buf) + } + } else if (this.channel.readyState === 'connecting') { + // queue message for when we are open + if (this.messageQueue == null) { + this.messageQueue = new Uint8ArrayList() + } + + this.messageQueue.append(data) + } else { + log.error('unknown datachannel state %s', this.channel.readyState) + throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') + } + } + + async sendData (data: Uint8ArrayList): Promise { + const msgbuf = Message.encode({ message: data.subarray() }) + const sendbuf = lengthPrefixed.encode.single(msgbuf) + + await this._sendMessage(sendbuf) + } + + async sendReset (): Promise { + await this._sendFlag(Message.Flag.RESET) + } + + async sendCloseWrite (): Promise { + await this._sendFlag(Message.Flag.FIN) + } + + async sendCloseRead (): Promise { + await this._sendFlag(Message.Flag.STOP_SENDING) + } + + /** + * Handle incoming + */ + private processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined { + const message = Message.decode(buffer) + + if (message.flag !== undefined) { + if (message.flag === Message.Flag.FIN) { + // We should expect no more data from the remote, stop reading + this.incomingData.end() + this.closeRead() + } + + if (message.flag === Message.Flag.RESET) { + // Stop reading and writing to the stream immediately + this.reset() + } + + if (message.flag === Message.Flag.STOP_SENDING) { + // The remote has stopped reading + this.closeWrite() + } + } + + return message.message + } + + private async _sendFlag (flag: Message.Flag): Promise { + log.trace('Sending flag: %s', flag.toString()) + const msgbuf = Message.encode({ flag }) + const prefixedBuf = lengthPrefixed.encode.single(msgbuf) + + await this._sendMessage(prefixedBuf, false) + } +} + +export interface WebRTCStreamOptions { + /** + * The network channel used for bidirectional peer-to-peer transfers of + * arbitrary data + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} + */ + channel: RTCDataChannel + + /** + * The stream direction + */ + direction: Direction + + dataChannelOptions?: Partial + + maxMsgSize?: number + + onEnd?: (err?: Error | undefined) => void +} + +export function createStream (options: WebRTCStreamOptions): Stream { + const { channel, direction, onEnd, dataChannelOptions } = options + + return new WebRTCStream({ + id: direction === 'inbound' ? (`i${channel.id}`) : `r${channel.id}`, + direction, + maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD, + dataChannelOptions, + onEnd, + channel + }) +} diff --git a/packages/transport-webrtc/src/util.ts b/packages/transport-webrtc/src/util.ts new file mode 100644 index 0000000000..e26e64dd5f --- /dev/null +++ b/packages/transport-webrtc/src/util.ts @@ -0,0 +1,8 @@ +import { detect } from 'detect-browser' + +const browser = detect() +export const isFirefox = ((browser != null) && browser.name === 'firefox') + +export const nopSource = async function * nop (): AsyncGenerator {} + +export const nopSink = async (_: any): Promise => {} diff --git a/packages/transport-webrtc/test/basics.spec.ts b/packages/transport-webrtc/test/basics.spec.ts new file mode 100644 index 0000000000..ec15c43703 --- /dev/null +++ b/packages/transport-webrtc/test/basics.spec.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { webSockets } from '@libp2p/websockets' +import * as filter from '@libp2p/websockets/filters' +import { WebRTC } from '@multiformats/mafmt' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import map from 'it-map' +import { pipe } from 'it-pipe' +import toBuffer from 'it-to-buffer' +import { createLibp2p } from 'libp2p' +import { circuitRelayTransport } from 'libp2p/circuit-relay' +import { identifyService } from 'libp2p/identify' +import { webRTC } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Libp2p } from '@libp2p/interface-libp2p' + +async function createNode (): Promise { + return createLibp2p({ + addresses: { + listen: [ + '/webrtc', + `${process.env.RELAY_MULTIADDR}/p2p-circuit` + ] + }, + transports: [ + webSockets({ + filter: filter.all + }), + circuitRelayTransport(), + webRTC() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + services: { + identify: identifyService() + }, + connectionGater: { + denyDialMultiaddr: () => false + }, + connectionManager: { + minConnections: 0 + } + }) +} + +describe('basics', () => { + const echo = '/echo/1.0.0' + + let localNode: Libp2p + let remoteNode: Libp2p + + async function connectNodes (): Promise { + const remoteAddr = remoteNode.getMultiaddrs() + .filter(ma => WebRTC.matches(ma)).pop() + + if (remoteAddr == null) { + throw new Error('Remote peer could not listen on relay') + } + + await remoteNode.handle(echo, ({ stream }) => { + void pipe( + stream, + stream + ) + }) + + const connection = await localNode.dial(remoteAddr) + + // disconnect both from relay + await localNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) + await remoteNode.hangUp(multiaddr(process.env.RELAY_MULTIADDR)) + + return connection + } + + beforeEach(async () => { + localNode = await createNode() + remoteNode = await createNode() + }) + + afterEach(async () => { + if (localNode != null) { + await localNode.stop() + } + + if (remoteNode != null) { + await remoteNode.stop() + } + }) + + it('can dial through a relay', async () => { + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo) + + // send and receive some data + const input = new Array(5).fill(0).map(() => new Uint8Array(10)) + const output = await pipe( + input, + stream, + (source) => map(source, list => list.subarray()), + async (source) => toBuffer(source) + ) + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) + + it('can send a large file', async () => { + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo) + + // send and receive some data + const input = new Array(5).fill(0).map(() => new Uint8Array(1024 * 1024)) + const output = await pipe( + input, + stream, + (source) => map(source, list => list.subarray()), + async (source) => toBuffer(source) + ) + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) +}) diff --git a/packages/transport-webrtc/test/listener.spec.ts b/packages/transport-webrtc/test/listener.spec.ts new file mode 100644 index 0000000000..4928ca682c --- /dev/null +++ b/packages/transport-webrtc/test/listener.spec.ts @@ -0,0 +1,40 @@ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { stubInterface } from 'sinon-ts' +import { WebRTCPeerListener } from '../src/private-to-private/listener' +import type { Listener, TransportManager } from '@libp2p/interface-transport' + +describe('webrtc private-to-private listener', () => { + it('should only return relay addresses as webrtc listen addresses', async () => { + const relayedAddress = '/ip4/127.0.0.1/tcp/4034/ws/p2p-circuit' + const otherListenAddress = '/ip4/127.0.0.1/tcp/4001' + const peerId = await createEd25519PeerId() + const transportManager = stubInterface() + + const listener = new WebRTCPeerListener({ + peerId, + transportManager + }) + + const otherListener = stubInterface({ + getAddrs: [multiaddr(otherListenAddress)] + }) + + const relayListener = stubInterface({ + getAddrs: [multiaddr(relayedAddress)] + }) + + transportManager.getListeners.returns([ + listener, + otherListener, + relayListener + ]) + + const addresses = listener.getAddrs() + + expect(addresses.map(ma => ma.toString())).to.deep.equal([ + `${relayedAddress}/webrtc/p2p/${peerId}` + ]) + }) +}) diff --git a/packages/transport-webrtc/test/maconn.browser.spec.ts b/packages/transport-webrtc/test/maconn.browser.spec.ts new file mode 100644 index 0000000000..9a25dc541c --- /dev/null +++ b/packages/transport-webrtc/test/maconn.browser.spec.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { stubObject } from 'sinon-ts' +import { WebRTCMultiaddrConnection } from '../src/maconn.js' +import type { CounterGroup } from '@libp2p/interface-metrics' + +describe('Multiaddr Connection', () => { + it('can open and close', async () => { + const peerConnection = new RTCPeerConnection() + peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) + const remoteAddr = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + const metrics = stubObject({ + increment: () => {}, + reset: () => {} + }) + const maConn = new WebRTCMultiaddrConnection({ + peerConnection, + remoteAddr, + timeline: { + open: (new Date()).getTime() + }, + metrics + }) + + expect(maConn.timeline.close).to.be.undefined + + await maConn.close() + + expect(maConn.timeline.close).to.not.be.undefined + expect(metrics.increment.calledWith({ close: true })).to.be.true + }) +}) diff --git a/packages/transport-webrtc/test/peer.browser.spec.ts b/packages/transport-webrtc/test/peer.browser.spec.ts new file mode 100644 index 0000000000..25fe8284bb --- /dev/null +++ b/packages/transport-webrtc/test/peer.browser.spec.ts @@ -0,0 +1,132 @@ +import { mockConnection, mockMultiaddrConnection, mockRegistrar, mockStream, mockUpgrader } from '@libp2p/interface-mocks' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { detect } from 'detect-browser' +import { pair } from 'it-pair' +import { duplexPair } from 'it-pair/duplex' +import { pbStream } from 'it-pb-stream' +import Sinon from 'sinon' +import { initiateConnection, handleIncomingStream } from '../src/private-to-private/handler' +import { Message } from '../src/private-to-private/pb/message.js' +import { WebRTCTransport, splitAddr } from '../src/private-to-private/transport' + +const browser = detect() + +describe('webrtc basic', () => { + const isFirefox = ((browser != null) && browser.name === 'firefox') + it('should connect', async () => { + const [receiver, initiator] = duplexPair() + const dstPeerId = await createEd25519PeerId() + const connection = mockConnection( + mockMultiaddrConnection(pair(), dstPeerId) + ) + const controller = new AbortController() + const initiatorPeerConnectionPromise = initiateConnection({ stream: mockStream(initiator), signal: controller.signal }) + const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) + await expect(initiatorPeerConnectionPromise).to.be.fulfilled() + await expect(receiverPeerConnectionPromise).to.be.fulfilled() + const [{ pc: pc0 }, { pc: pc1 }] = await Promise.all([initiatorPeerConnectionPromise, receiverPeerConnectionPromise]) + if (isFirefox) { + expect(pc0.iceConnectionState).eq('connected') + expect(pc1.iceConnectionState).eq('connected') + return + } + expect(pc0.connectionState).eq('connected') + expect(pc1.connectionState).eq('connected') + }) +}) + +describe('webrtc receiver', () => { + it('should fail receiving on invalid sdp offer', async () => { + const [receiver, initiator] = duplexPair() + const dstPeerId = await createEd25519PeerId() + const connection = mockConnection( + mockMultiaddrConnection(pair(), dstPeerId) + ) + const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) + const stream = pbStream(initiator).pb(Message) + + stream.write({ type: Message.Type.SDP_OFFER, data: 'bad' }) + await expect(receiverPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) + }) +}) + +describe('webrtc dialer', () => { + it('should fail receiving on invalid sdp answer', async () => { + const [receiver, initiator] = duplexPair() + const controller = new AbortController() + const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) + const stream = pbStream(receiver).pb(Message) + + { + const offerMessage = await stream.read() + expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) + } + + stream.write({ type: Message.Type.SDP_ANSWER, data: 'bad' }) + await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) + }) + + it('should fail on receiving a candidate before an answer', async () => { + const [receiver, initiator] = duplexPair() + const controller = new AbortController() + const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) + const stream = pbStream(receiver).pb(Message) + + const pc = new RTCPeerConnection() + pc.onicecandidate = ({ candidate }) => { + stream.write({ type: Message.Type.ICE_CANDIDATE, data: JSON.stringify(candidate?.toJSON()) }) + } + { + const offerMessage = await stream.read() + expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) + const offer = new RTCSessionDescription({ type: 'offer', sdp: offerMessage.data }) + await pc.setRemoteDescription(offer) + + const answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + } + + await expect(initiatorPeerConnectionPromise).to.be.rejectedWith(/remote should send an SDP answer/) + }) +}) + +describe('webrtc filter', () => { + it('can filter multiaddrs to dial', async () => { + const transport = new WebRTCTransport({ + transportManager: Sinon.stub() as any, + peerId: Sinon.stub() as any, + registrar: mockRegistrar(), + upgrader: mockUpgrader({}) + }, {}) + + const valid = [ + multiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p-circuit/webrtc') + ] + + expect(transport.filter(valid)).length(1) + }) +}) + +describe('webrtc splitAddr', () => { + it('can split a ws relay addr', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/webrtc/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + + const { baseAddr, peerId } = splitAddr(ma) + + expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/tcp/49173/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + expect(peerId.toString()).to.eq('12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') + }) + + it('can split a webrtc-direct relay addr', async () => { + const ma = multiaddr('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/webrtc/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + + const { baseAddr, peerId } = splitAddr(ma) + + expect(baseAddr.toString()).to.eq('/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiBUr89tH2P9paTCPn-AcfVZcgvIvkwns96t4h55IpxFtA/p2p/12D3KooWB64sJqc3T3VCaubQCrfCvvfummrAA9z1vEXHJT77ZNJh/p2p-circuit/p2p/12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + expect(peerId.toString()).to.eq('12D3KooWFNBgv86tcpcYUHQz9FWGTrTmpMgr8feZwQXQySVTo3A7') + }) +}) + +export { } diff --git a/packages/transport-webrtc/test/sdp.spec.ts b/packages/transport-webrtc/test/sdp.spec.ts new file mode 100644 index 0000000000..2c32f7967b --- /dev/null +++ b/packages/transport-webrtc/test/sdp.spec.ts @@ -0,0 +1,81 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import * as underTest from '../src/private-to-public/sdp.js' + +const sampleMultiAddr = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ') +const sampleCerthash = 'uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ' +const sampleSdp = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +c=IN IP4 0.0.0.0 +t=0 0 +a=ice-lite +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:MyUserFragment +a=ice-pwd:MyUserFragment +a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` + +describe('SDP', () => { + it('converts multiaddr with certhash to an answer SDP', async () => { + const ufrag = 'MyUserFragment' + const sdp = underTest.fromMultiAddr(sampleMultiAddr, ufrag) + + expect(sdp.sdp).to.contain(sampleSdp) + }) + + it('extracts certhash from a multiaddr', () => { + const certhash = underTest.certhash(sampleMultiAddr) + + expect(certhash).to.equal(sampleCerthash) + }) + + it('decodes a certhash', () => { + const decoded = underTest.decodeCerthash(sampleCerthash) + + // sha2-256 multihash 0x12 permanent + // https://github.com/multiformats/multicodec/blob/master/table.csv + expect(decoded.name).to.equal('sha2-256') + expect(decoded.code).to.equal(0x12) + expect(decoded.length).to.equal(32) + expect(decoded.digest.toString()).to.equal('114,104,71,205,72,176,94,197,96,77,21,156,191,64,29,111,0,161,35,236,144,23,14,44,209,179,143,210,157,55,229,177') + }) + + it('converts a multiaddr into a fingerprint', () => { + const fingerpint = underTest.ma2Fingerprint(sampleMultiAddr) + expect(fingerpint).to.deep.equal([ + 'SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1', + '726847cd48b05ec5604d159cbf401d6f00a123ec90170e2cd1b38fd29d37e5b1' + ]) + }) + + it('extracts a fingerprint from sdp', () => { + const fingerprint = underTest.getFingerprintFromSdp(sampleSdp) + expect(fingerprint).to.eq('72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1') + }) + + it('munges the ufrag and pwd in a SDP', () => { + const result = underTest.munge({ type: 'answer', sdp: sampleSdp }, 'someotheruserfragmentstring') + const expected = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +c=IN IP4 0.0.0.0 +t=0 0 +a=ice-lite +m=application 56093 UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=setup:passive +a=ice-ufrag:someotheruserfragmentstring +a=ice-pwd:someotheruserfragmentstring +a=fingerprint:SHA-256 72:68:47:CD:48:B0:5E:C5:60:4D:15:9C:BF:40:1D:6F:00:A1:23:EC:90:17:0E:2C:D1:B3:8F:D2:9D:37:E5:B1 +a=sctp-port:5000 +a=max-message-size:100000 +a=candidate:1467250027 1 UDP 1467250027 0.0.0.0 56093 typ host` + + expect(result.sdp).to.equal(expected) + }) +}) diff --git a/packages/transport-webrtc/test/stream.browser.spec.ts b/packages/transport-webrtc/test/stream.browser.spec.ts new file mode 100644 index 0000000000..ee57a61d84 --- /dev/null +++ b/packages/transport-webrtc/test/stream.browser.spec.ts @@ -0,0 +1,150 @@ +import { expect } from 'aegir/chai' +import delay from 'delay' +import * as lengthPrefixed from 'it-length-prefixed' +import { bytes } from 'multiformats' +import { Message } from '../src/pb/message.js' +import { createStream } from '../src/stream' +import type { Stream } from '@libp2p/interface-connection' +const TEST_MESSAGE = 'test_message' + +function setup (): { peerConnection: RTCPeerConnection, dataChannel: RTCDataChannel, stream: Stream } { + const peerConnection = new RTCPeerConnection() + const dataChannel = peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) + const stream = createStream({ channel: dataChannel, direction: 'outbound' }) + + return { peerConnection, dataChannel, stream } +} + +function generatePbByFlag (flag?: Message.Flag): Uint8Array { + const buf = Message.encode({ + flag, + message: bytes.fromString(TEST_MESSAGE) + }) + + return lengthPrefixed.encode.single(buf).subarray() +} + +describe('Stream Stats', () => { + let stream: Stream + + beforeEach(async () => { + ({ stream } = setup()) + }) + + it('can construct', () => { + expect(stream.stat.timeline.close).to.not.exist() + }) + + it('close marks it closed', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.close() + expect(stream.stat.timeline.close).to.be.a('number') + }) + + it('closeRead marks it read-closed only', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeRead() + expect(stream.stat.timeline.close).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('closeWrite marks it write-closed only', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeWrite() + expect(stream.stat.timeline.close).to.not.exist() + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('closeWrite AND closeRead = close', async () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.closeWrite() + stream.closeRead() + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('abort = close', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.abort(new Error('Oh no!')) + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('reset = close', () => { + expect(stream.stat.timeline.close).to.not.exist() + stream.reset() // only resets the write side + expect(stream.stat.timeline.close).to.be.a('number') + expect(stream.stat.timeline.close).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) +}) + +describe('Stream Read Stats Transition By Incoming Flag', () => { + let dataChannel: RTCDataChannel + let stream: Stream + + beforeEach(async () => { + ({ dataChannel, stream } = setup()) + }) + + it('no flag, no transition', () => { + expect(stream.stat.timeline.close).to.not.exist() + const data = generatePbByFlag() + dataChannel.onmessage?.(new MessageEvent('message', { data })) + + expect(stream.stat.timeline.close).to.not.exist() + }) + + it('open to read-close by flag:FIN', async () => { + const data = generatePbByFlag(Message.Flag.FIN) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) + + it('read-close to close by flag:STOP_SENDING', async () => { + const data = generatePbByFlag(Message.Flag.STOP_SENDING) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.not.exist() + }) +}) + +describe('Stream Write Stats Transition By Incoming Flag', () => { + let dataChannel: RTCDataChannel + let stream: Stream + + beforeEach(async () => { + ({ dataChannel, stream } = setup()) + }) + + it('open to write-close by flag:STOP_SENDING', async () => { + const data = generatePbByFlag(Message.Flag.STOP_SENDING) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.be.greaterThanOrEqual(stream.stat.timeline.open) + expect(stream.stat.timeline.closeRead).to.not.exist() + }) + + it('write-close to close by flag:FIN', async () => { + const data = generatePbByFlag(Message.Flag.FIN) + dataChannel.dispatchEvent(new MessageEvent('message', { data })) + + await delay(100) + + expect(stream.stat.timeline.closeWrite).to.not.exist() + expect(stream.stat.timeline.closeRead).to.be.greaterThanOrEqual(stream.stat.timeline.open) + }) +}) diff --git a/packages/transport-webrtc/test/stream.spec.ts b/packages/transport-webrtc/test/stream.spec.ts new file mode 100644 index 0000000000..dd44b1ebab --- /dev/null +++ b/packages/transport-webrtc/test/stream.spec.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +import { expect } from 'aegir/chai' +import length from 'it-length' +import * as lengthPrefixed from 'it-length-prefixed' +import { pushable } from 'it-pushable' +import { Uint8ArrayList } from 'uint8arraylist' +import { Message } from '../src/pb/message.js' +import { createStream } from '../src/stream.js' + +const mockDataChannel = (opts: { send: (bytes: Uint8Array) => void, bufferedAmount?: number }): RTCDataChannel => { + return { + readyState: 'open', + close: () => {}, + addEventListener: (_type: string, _listener: () => void) => {}, + removeEventListener: (_type: string, _listener: () => void) => {}, + ...opts + } as RTCDataChannel +} + +const MAX_MESSAGE_SIZE = 16 * 1024 + +describe('Max message size', () => { + it(`sends messages smaller or equal to ${MAX_MESSAGE_SIZE} bytes in one`, async () => { + const sent: Uint8ArrayList = new Uint8ArrayList() + const data = new Uint8Array(MAX_MESSAGE_SIZE - 5) + const p = pushable() + + // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE + const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })) + expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE) + const webrtcStream = createStream({ + channel: mockDataChannel({ + send: (bytes) => { + sent.append(bytes) + } + }), + direction: 'outbound' + }) + + p.push(data) + p.end() + await webrtcStream.sink(p) + + // length(message) + message + length(FIN) + FIN + expect(length(sent)).to.equal(4) + + for (const buf of sent) { + expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) + } + }) + + it(`sends messages greater than ${MAX_MESSAGE_SIZE} bytes in parts`, async () => { + const sent: Uint8ArrayList = new Uint8ArrayList() + const data = new Uint8Array(MAX_MESSAGE_SIZE) + const p = pushable() + + // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE + 1 + // const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })).subarray() + // expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE + 1) + + const webrtcStream = createStream({ + channel: mockDataChannel({ + send: (bytes) => { + sent.append(bytes) + } + }), + direction: 'outbound' + }) + + p.push(data) + p.end() + await webrtcStream.sink(p) + + expect(length(sent)).to.equal(6) + + for (const buf of sent) { + expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) + } + }) + + it('closes the stream if bufferamountlow timeout', async () => { + const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 + 1 + const timeout = 100 + let closed = false + const webrtcStream = createStream({ + dataChannelOptions: { + bufferedAmountLowEventTimeout: timeout + }, + channel: mockDataChannel({ + send: () => { + throw new Error('Expected to not send') + }, + bufferedAmount: MAX_BUFFERED_AMOUNT + }), + direction: 'outbound', + onEnd: () => { + closed = true + } + }) + + const p = pushable() + p.push(new Uint8Array(1)) + p.end() + + const t0 = Date.now() + + await expect(webrtcStream.sink(p)).to.eventually.be.rejected + .with.property('message', 'Timed out waiting for DataChannel buffer to clear') + const t1 = Date.now() + expect(t1 - t0).greaterThan(timeout) + expect(t1 - t0).lessThan(timeout + 1000) // Some upper bound + expect(closed).true() + }) +}) diff --git a/packages/transport-webrtc/test/transport.browser.spec.ts b/packages/transport-webrtc/test/transport.browser.spec.ts new file mode 100644 index 0000000000..dea6c68fbd --- /dev/null +++ b/packages/transport-webrtc/test/transport.browser.spec.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ + +import { mockMetrics, mockUpgrader } from '@libp2p/interface-mocks' +import { type CreateListenerOptions, symbol } from '@libp2p/interface-transport' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import { expect, assert } from 'aegir/chai' +import { UnimplementedError } from '../src/error.js' +import * as underTest from '../src/private-to-public/transport.js' +import { expectError } from './util.js' +import type { Metrics } from '@libp2p/interface-metrics' + +function ignoredDialOption (): CreateListenerOptions { + const upgrader = mockUpgrader({}) + return { upgrader } +} + +describe('WebRTC Transport', () => { + let metrics: Metrics + let components: underTest.WebRTCDirectTransportComponents + + before(async () => { + metrics = mockMetrics()() + components = { + peerId: await createEd25519PeerId(), + metrics + } + }) + + it('can construct', () => { + const t = new underTest.WebRTCDirectTransport(components) + expect(t.constructor.name).to.equal('WebRTCDirectTransport') + }) + + it('can dial', async () => { + const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + const transport = new underTest.WebRTCDirectTransport(components) + const options = ignoredDialOption() + + // don't await as this isn't an e2e test + transport.dial(ma, options) + }) + + it('createListner throws', () => { + const t = new underTest.WebRTCDirectTransport(components) + try { + t.createListener(ignoredDialOption()) + expect('Should have thrown').to.equal('but did not') + } catch (e) { + expect(e).to.be.instanceOf(UnimplementedError) + } + }) + + it('toString property getter', () => { + const t = new underTest.WebRTCDirectTransport(components) + const s = t[Symbol.toStringTag] + expect(s).to.equal('@libp2p/webrtc-direct') + }) + + it('symbol property getter', () => { + const t = new underTest.WebRTCDirectTransport(components) + const s = t[symbol] + expect(s).to.equal(true) + }) + + it('transport filter filters out invalid multiaddrs', async () => { + const mas: Multiaddr[] = [ + '/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ', + '/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', + '/ip4/1.2.3.4/udp/1234/webrtc-direct/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', + '/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd' + ].map((s) => multiaddr(s)) + const t = new underTest.WebRTCDirectTransport(components) + const result = t.filter(mas) + const expected = + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + + assert.isNotNull(result) + expect(result.constructor.name).to.equal('Array') + expect(result).to.have.length(1) + expect(result[0].equals(expected)).to.be.true() + }) + + it('throws WebRTC transport error when dialing a multiaddr without a PeerId', async () => { + const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + const transport = new underTest.WebRTCDirectTransport(components) + + try { + await transport.dial(ma, ignoredDialOption()) + } catch (error) { + const expected = 'WebRTC transport error: There was a problem with the Multiaddr which was passed in: we need to have the remote\'s PeerId' + expectError(error, expected) + } + }) +}) diff --git a/packages/transport-webrtc/test/util.ts b/packages/transport-webrtc/test/util.ts new file mode 100644 index 0000000000..70c492b6a7 --- /dev/null +++ b/packages/transport-webrtc/test/util.ts @@ -0,0 +1,9 @@ +import { expect } from 'aegir/chai' + +export const expectError = (error: unknown, message: string): void => { + if (error instanceof Error) { + expect(error.message).to.equal(message) + } else { + expect('Did not throw error:').to.equal(message) + } +} diff --git a/packages/transport-webrtc/tsconfig.json b/packages/transport-webrtc/tsconfig.json new file mode 100644 index 0000000000..aa7a3a98ba --- /dev/null +++ b/packages/transport-webrtc/tsconfig.json @@ -0,0 +1,55 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test", + "proto_ts" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-libp2p" + }, + { + "path": "../interface-metrics" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-registrar" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interfaces" + }, + { + "path": "../libp2p" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../transport-websockets" + } + ] +} diff --git a/packages/transport-websockets/.aegir.js b/packages/transport-websockets/.aegir.js new file mode 100644 index 0000000000..19b4335f8f --- /dev/null +++ b/packages/transport-websockets/.aegir.js @@ -0,0 +1,46 @@ +import { pipe } from 'it-pipe' + +/** @type {import('aegir/types').PartialOptions} */ +export default { + test: { + async before () { + const { multiaddr } = await import('@multiformats/multiaddr') + const { mockRegistrar, mockUpgrader } = await import('@libp2p/interface-mocks') + const { EventEmitter } = await import('@libp2p/interfaces/events') + const { webSockets } = await import('./dist/src/index.js') + + const protocol = '/echo/1.0.0' + const registrar = mockRegistrar() + registrar.handle(protocol, ({ stream }) => { + void pipe( + stream, + stream + ) + }) + const upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() + }) + + const ws = webSockets()() + const ma = multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + const listener = ws.createListener({ + upgrader + }) + await listener.listen(ma) + listener.addEventListener('error', (evt) => { + console.error(evt.detail) + }) + + return { + listener + } + }, + async after (_, before) { + await before.listener.close() + } + }, + build: { + bundlesizeMax: '18kB' + } +} diff --git a/packages/transport-websockets/CHANGELOG.md b/packages/transport-websockets/CHANGELOG.md new file mode 100644 index 0000000000..b218a6315a --- /dev/null +++ b/packages/transport-websockets/CHANGELOG.md @@ -0,0 +1,713 @@ +## [6.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.2...v6.0.3) (2023-06-06) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 11.0.3 to 12.0.1 ([#241](https://github.com/libp2p/js-libp2p-websockets/issues/241)) ([f956836](https://github.com/libp2p/js-libp2p-websockets/commit/f95683641bda2f9b250768768451e0c121afc2a0)) +* **dev:** bump aegir from 38.1.8 to 39.0.9 ([#245](https://github.com/libp2p/js-libp2p-websockets/issues/245)) ([4a35f6b](https://github.com/libp2p/js-libp2p-websockets/commit/4a35f6b39a918fb7ef779292553cb452a543afb0)) + +## [6.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.1...v6.0.2) (2023-06-06) + + +### Dependencies + +* add ws types to main dependencies ([#246](https://github.com/libp2p/js-libp2p-websockets/issues/246)) ([628e260](https://github.com/libp2p/js-libp2p-websockets/commit/628e26049c2ee3695d0393a9a69f3a80ced820e7)), closes [/github.com/alanshaw/it-ws/blob/master/package.json#L50](https://github.com/libp2p//github.com/alanshaw/it-ws/blob/master/package.json/issues/L50) + +## [6.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v6.0.0...v6.0.1) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-transport from 3.0.0 to 4.0.0 ([#233](https://github.com/libp2p/js-libp2p-websockets/issues/233)) ([da0367f](https://github.com/libp2p/js-libp2p-websockets/commit/da0367f7a02a23a819d9a768ab209a53e791ae6b)) + +## [6.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.10...v6.0.0) (2023-04-18) + + +### ⚠ BREAKING CHANGES + +* the type of the source/sink properties have changed + +### Dependencies + +* update stream types ([#232](https://github.com/libp2p/js-libp2p-websockets/issues/232)) ([5a69d38](https://github.com/libp2p/js-libp2p-websockets/commit/5a69d38f68f0796f869d8becb5d02be1772cda94)) + +## [5.0.10](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.9...v5.0.10) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#231](https://github.com/libp2p/js-libp2p-websockets/issues/231)) ([e2f7204](https://github.com/libp2p/js-libp2p-websockets/commit/e2f72040a214efd35215a5919aa92fec5038ca11)) + +## [5.0.9](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.8...v5.0.9) (2023-04-10) + + +### Bug Fixes + +* correction package.json exports types path ([#230](https://github.com/libp2p/js-libp2p-websockets/issues/230)) ([0a6ed35](https://github.com/libp2p/js-libp2p-websockets/commit/0a6ed354a131ff00836143611afc18978b08013b)) + +## [5.0.8](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.7...v5.0.8) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#226](https://github.com/libp2p/js-libp2p-websockets/issues/226)) ([8e9affd](https://github.com/libp2p/js-libp2p-websockets/commit/8e9affdd6ddda9a2678a143fb9390cf6917a8aff)) + +## [5.0.7](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.6...v5.0.7) (2023-03-20) + + +### Dependencies + +* bump @multiformats/mafmt from 11.1.2 to 12.0.0 ([#224](https://github.com/libp2p/js-libp2p-websockets/issues/224)) ([ee3bb05](https://github.com/libp2p/js-libp2p-websockets/commit/ee3bb054ac265210c436defb42c62fd5de969232)) + +## [5.0.6](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.5...v5.0.6) (2023-03-17) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#223](https://github.com/libp2p/js-libp2p-websockets/issues/223)) ([e69a70c](https://github.com/libp2p/js-libp2p-websockets/commit/e69a70c1004d25a1272877ecba1ba2af1afc5a2f)) + +## [5.0.5](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.4...v5.0.5) (2023-03-09) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([4936a70](https://github.com/libp2p/js-libp2p-websockets/commit/4936a70ef1d148aab69c4c659ba2e3a7724918d6)) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 8.0.5 to 9.1.3 ([#220](https://github.com/libp2p/js-libp2p-websockets/issues/220)) ([1076b05](https://github.com/libp2p/js-libp2p-websockets/commit/1076b05ccd3bc3f5a0115759baa1f438f66416da)) +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#219](https://github.com/libp2p/js-libp2p-websockets/issues/219)) ([4e0f17b](https://github.com/libp2p/js-libp2p-websockets/commit/4e0f17b04ba4f62b72d91a3c343d748d79ca4000)) + +## [5.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.3...v5.0.4) (2023-03-03) + + +### Bug Fixes + +* Only filter by wss not dns ([#218](https://github.com/libp2p/js-libp2p-websockets/issues/218)) ([434d44c](https://github.com/libp2p/js-libp2p-websockets/commit/434d44cdfcbab48008a160cca2da1129cb43860b)) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([fad99cc](https://github.com/libp2p/js-libp2p-websockets/commit/fad99cca84037922b62785829fb67c6110ea4c16)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([b1954aa](https://github.com/libp2p/js-libp2p-websockets/commit/b1954aad07080a24db9021ea2736d34a4e444d00)) + +## [5.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.2...v5.0.3) (2023-01-13) + + +### Dependencies + +* remove err-code ([#202](https://github.com/libp2p/js-libp2p-websockets/issues/202)) ([40ce006](https://github.com/libp2p/js-libp2p-websockets/commit/40ce0060918cb390b343a748036af4aee43b2146)) + +## [5.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.1...v5.0.2) (2022-12-16) + + +### Documentation + +* publish api docs ([#201](https://github.com/libp2p/js-libp2p-websockets/issues/201)) ([722b03a](https://github.com/libp2p/js-libp2p-websockets/commit/722b03a7a57200505aebac018aeb06ba85219721)) + +## [5.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v5.0.0...v5.0.1) (2022-12-08) + + +### Bug Fixes + +* cannot catch EADDRINUSE ([#198](https://github.com/libp2p/js-libp2p-websockets/issues/198)) ([c7312db](https://github.com/libp2p/js-libp2p-websockets/commit/c7312db639b37c767afe0651cab9d33a6f0246b3)), closes [#184](https://github.com/libp2p/js-libp2p-websockets/issues/184) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 7.1.0 to 8.0.2 ([#199](https://github.com/libp2p/js-libp2p-websockets/issues/199)) ([daff533](https://github.com/libp2p/js-libp2p-websockets/commit/daff53335baec84ae97d937ab79779f475c8ab18)), closes [#318](https://github.com/libp2p/js-libp2p-websockets/issues/318) [#315](https://github.com/libp2p/js-libp2p-websockets/issues/315) [#313](https://github.com/libp2p/js-libp2p-websockets/issues/313) [#312](https://github.com/libp2p/js-libp2p-websockets/issues/312) +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#193](https://github.com/libp2p/js-libp2p-websockets/issues/193)) ([6213f8f](https://github.com/libp2p/js-libp2p-websockets/commit/6213f8f6f113846622c53966478ae75c81fa5d14)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#27](https://github.com/libp2p/js-libp2p-websockets/issues/27) [#24](https://github.com/libp2p/js-libp2p-websockets/issues/24) +* **dev:** bump it-drain from 1.0.5 to 2.0.0 ([#191](https://github.com/libp2p/js-libp2p-websockets/issues/191)) ([e549691](https://github.com/libp2p/js-libp2p-websockets/commit/e549691e40577f9146355998cb504f071772e4e3)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) [#27](https://github.com/libp2p/js-libp2p-websockets/issues/27) [#24](https://github.com/libp2p/js-libp2p-websockets/issues/24) +* **dev:** bump it-take from 1.0.2 to 2.0.0 ([#192](https://github.com/libp2p/js-libp2p-websockets/issues/192)) ([4c037fc](https://github.com/libp2p/js-libp2p-websockets/commit/4c037fc3c116a3ed2ec39aec3fed776fcb6c9690)), closes [#28](https://github.com/libp2p/js-libp2p-websockets/issues/28) + +## [5.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v4.0.1...v5.0.0) (2022-10-12) + + +### ⚠ BREAKING CHANGES + +* modules no longer implement `Initializable` instead switching to constructor injection + +### Bug Fixes + +* remove @libp2p/components ([#190](https://github.com/libp2p/js-libp2p-websockets/issues/190)) ([388b30d](https://github.com/libp2p/js-libp2p-websockets/commit/388b30d1c1024e2f7fd9d8bea85701d997f59dbb)) + +## [4.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v4.0.0...v4.0.1) (2022-10-07) + + +### Dependencies + +* **dev:** bump @libp2p/interface-mocks from 4.0.3 to 6.0.1 ([#189](https://github.com/libp2p/js-libp2p-websockets/issues/189)) ([00b33f0](https://github.com/libp2p/js-libp2p-websockets/commit/00b33f07a9af8446dcf94a4a0567994f6deefcbf)) + +## [4.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.4...v4.0.0) (2022-10-07) + + +### ⚠ BREAKING CHANGES + +* bump @libp2p/interface-transport from 1.0.4 to 2.0.0 (#187) + +### Dependencies + +* bump @libp2p/interface-transport from 1.0.4 to 2.0.0 ([#187](https://github.com/libp2p/js-libp2p-websockets/issues/187)) ([bfeaf1b](https://github.com/libp2p/js-libp2p-websockets/commit/bfeaf1bc695c2becff8c47839726f8105269ad9c)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.3...v3.0.4) (2022-09-21) + + +### Bug Fixes + +* remove set timeout ([#182](https://github.com/libp2p/js-libp2p-websockets/issues/182)) ([23518b0](https://github.com/libp2p/js-libp2p-websockets/commit/23518b0dad79d2c38bca8d600bd763703534b7a6)), closes [#121](https://github.com/libp2p/js-libp2p-websockets/issues/121) +* socket close event not working in browser ([#183](https://github.com/libp2p/js-libp2p-websockets/issues/183)) ([9076b5b](https://github.com/libp2p/js-libp2p-websockets/commit/9076b5bade8dd453b98fc73f0dc0bddaba0fe882)), closes [#179](https://github.com/libp2p/js-libp2p-websockets/issues/179) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([64411ee](https://github.com/libp2p/js-libp2p-websockets/commit/64411eef588a57adb65868940e489f0badfb579d)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#185](https://github.com/libp2p/js-libp2p-websockets/issues/185)) ([539db88](https://github.com/libp2p/js-libp2p-websockets/commit/539db8806dc7748f9c5d6c8ba785a3c78bb92c62)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.2...v3.0.3) (2022-09-01) + + +### Trivial Changes + +* update project config ([#180](https://github.com/libp2p/js-libp2p-websockets/issues/180)) ([4f79f9c](https://github.com/libp2p/js-libp2p-websockets/commit/4f79f9ce789a566b99c57597d2d71e2bce40fd6e)) + + +### Dependencies + +* **dev:** bump wherearewe from 1.0.2 to 2.0.1 ([#177](https://github.com/libp2p/js-libp2p-websockets/issues/177)) ([5d7ae6a](https://github.com/libp2p/js-libp2p-websockets/commit/5d7ae6a5c22c57e7f47f32405fd57ece98664e4d)) + +## [3.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.1...v3.0.2) (2022-08-10) + + +### Bug Fixes + +* update all deps ([#176](https://github.com/libp2p/js-libp2p-websockets/issues/176)) ([4825cd7](https://github.com/libp2p/js-libp2p-websockets/commit/4825cd7c5cec0cfc495b8b4286658927779bebdc)) +* update dial function return type to avoid Connection import issue ([#171](https://github.com/libp2p/js-libp2p-websockets/issues/171)) ([7ea9f83](https://github.com/libp2p/js-libp2p-websockets/commit/7ea9f83c0e4c3b42ba6e16e3dee1932ddce340f6)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v3.0.0...v3.0.1) (2022-07-12) + + +### Trivial Changes + +* **deps-dev:** bump @libp2p/interface-mocks from 2.1.0 to 3.0.1 ([#168](https://github.com/libp2p/js-libp2p-websockets/issues/168)) ([8a17ed7](https://github.com/libp2p/js-libp2p-websockets/commit/8a17ed7eb70e7ac90053cd591bb1e6f331915341)) +* **deps:** bump @libp2p/utils from 2.0.1 to 3.0.0 ([#167](https://github.com/libp2p/js-libp2p-websockets/issues/167)) ([53ba721](https://github.com/libp2p/js-libp2p-websockets/commit/53ba7218d19068e2a6b038ecbea65993af7bd745)) +* update websockets import var ([#165](https://github.com/libp2p/js-libp2p-websockets/issues/165)) ([838b69e](https://github.com/libp2p/js-libp2p-websockets/commit/838b69e04d435e55d038e49f2df66322d986a2e3)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v2.0.1...v3.0.0) (2022-06-17) + + +### ⚠ BREAKING CHANGES + +* the connection API has changed + +### Trivial Changes + +* **deps:** bump @libp2p/logger from 1.1.6 to 2.0.0 ([#160](https://github.com/libp2p/js-libp2p-websockets/issues/160)) ([9074c4a](https://github.com/libp2p/js-libp2p-websockets/commit/9074c4a6725b750a3f8c602aa2655c095d83973d)) +* update deps ([#164](https://github.com/libp2p/js-libp2p-websockets/issues/164)) ([d474a81](https://github.com/libp2p/js-libp2p-websockets/commit/d474a8184a0eec4c09c2ced5dd5d6314be536fb3)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v2.0.0...v2.0.1) (2022-06-16) + + +### Trivial Changes + +* **deps:** bump @libp2p/utils from 1.0.10 to 2.0.0 ([#161](https://github.com/libp2p/js-libp2p-websockets/issues/161)) ([39980fc](https://github.com/libp2p/js-libp2p-websockets/commit/39980fc7fe994a341fd7f2e8a63738a58cfd1b02)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.9...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update to latest interfaces ([#159](https://github.com/libp2p/js-libp2p-websockets/issues/159)) ([e140bed](https://github.com/libp2p/js-libp2p-websockets/commit/e140bed0ae98af8ef0f7b3d6ec2388fa6273e590)) + + +### Trivial Changes + +* increase timeout for firefox ([5098e19](https://github.com/libp2p/js-libp2p-websockets/commit/5098e19796975e29ec91b62f28b52797dc1defde)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.8...v1.0.9) (2022-05-23) + + +### Bug Fixes + +* update interfaces and use static string for toStringTag ([#157](https://github.com/libp2p/js-libp2p-websockets/issues/157)) ([0c93585](https://github.com/libp2p/js-libp2p-websockets/commit/0c93585d0d5cb67c15ba0046b68aa3b196290e12)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.7...v1.0.8) (2022-05-06) + + +### Bug Fixes + +* hard code tag ([#154](https://github.com/libp2p/js-libp2p-websockets/issues/154)) ([c36aebb](https://github.com/libp2p/js-libp2p-websockets/commit/c36aebb9a38434c3e2127b9251427aba53bbb09f)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.6...v1.0.7) (2022-05-04) + + +### Bug Fixes + +* update interfaces ([#153](https://github.com/libp2p/js-libp2p-websockets/issues/153)) ([57c5887](https://github.com/libp2p/js-libp2p-websockets/commit/57c588716627270bbc42ee5e5c4249b99b9af5e5)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.5...v1.0.6) (2022-04-11) + + +### Bug Fixes + +* remove entrypoint config ([#152](https://github.com/libp2p/js-libp2p-websockets/issues/152)) ([cf2334e](https://github.com/libp2p/js-libp2p-websockets/commit/cf2334e8f5063dc98d34776b81e3dad13e761e6e)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.4...v1.0.5) (2022-04-09) + + +### Trivial Changes + +* update tsconfig ([#151](https://github.com/libp2p/js-libp2p-websockets/issues/151)) ([c54d349](https://github.com/libp2p/js-libp2p-websockets/commit/c54d3495d8bc53eaa1e1f4c99d9da404652f5a8d)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.3...v1.0.4) (2022-04-08) + + +### Trivial Changes + +* update aegir ([#150](https://github.com/libp2p/js-libp2p-websockets/issues/150)) ([6c08294](https://github.com/libp2p/js-libp2p-websockets/commit/6c08294e98807e789b791286931d120cfef679cd)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.2...v1.0.3) (2022-03-16) + + +### Bug Fixes + +* update interfaces ([#146](https://github.com/libp2p/js-libp2p-websockets/issues/146)) ([26ef08b](https://github.com/libp2p/js-libp2p-websockets/commit/26ef08bd243ddf714a32acdb6e2a7392209af355)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.1...v1.0.2) (2022-02-21) + + +### Bug Fixes + +* update interfaces ([#145](https://github.com/libp2p/js-libp2p-websockets/issues/145)) ([213ebc5](https://github.com/libp2p/js-libp2p-websockets/commit/213ebc5f85c749d712e1441b5fe49dc636e25f64)) + +### [1.0.1](https://github.com/libp2p/js-libp2p-websockets/compare/v1.0.0...v1.0.1) (2022-02-16) + + +### Bug Fixes + +* add toStringTag and export filters ([#142](https://github.com/libp2p/js-libp2p-websockets/issues/142)) ([03fd000](https://github.com/libp2p/js-libp2p-websockets/commit/03fd000088ac78ea25f8cdf123fbbe8923257ca4)) +* update typesversions ([1cfbc28](https://github.com/libp2p/js-libp2p-websockets/commit/1cfbc28f93adecb1a9b60f53ef5815c87d00c93c)) + +## [1.0.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.2...v1.0.0) (2022-02-10) + + +### ⚠ BREAKING CHANGES + +* switch to named exports, ESM only + +### Features + +* convert to typescript ([#76](https://github.com/libp2p/js-libp2p-websockets/issues/76)) ([#140](https://github.com/libp2p/js-libp2p-websockets/issues/140)) ([c4f6508](https://github.com/libp2p/js-libp2p-websockets/commit/c4f65082a97def50524e56231ce6c84eddf99521)), closes [#139](https://github.com/libp2p/js-libp2p-websockets/issues/139) + +## [0.16.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.1...v0.16.2) (2021-09-28) + + + +## [0.16.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.16.0...v0.16.1) (2021-07-08) + + + +# [0.16.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.9...v0.16.0) (2021-07-07) + + +### chore + +* update deps ([#134](https://github.com/libp2p/js-libp2p-websockets/issues/134)) ([27f6c41](https://github.com/libp2p/js-libp2p-websockets/commit/27f6c4175bd6d5ea3e727a9a6e43136c806077cc)) + + +### BREAKING CHANGES + +* uses new major of mafmt, multiaddr, etc + + + +## [0.15.9](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.8...v0.15.9) (2021-06-11) + + + +## [0.15.8](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.7...v0.15.8) (2021-06-08) + + +### Bug Fixes + +* listener get addrs with wss ([#130](https://github.com/libp2p/js-libp2p-websockets/issues/130)) ([ee47570](https://github.com/libp2p/js-libp2p-websockets/commit/ee47570ff79a51b8f3c3414934d5f7ab9d00f74d)) + + + +## [0.15.7](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.6...v0.15.7) (2021-05-04) + + + +## [0.15.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.5...v0.15.6) (2021-04-18) + + + +## [0.15.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.4...v0.15.5) (2021-04-12) + + + +## [0.15.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.3...v0.15.4) (2021-03-31) + + + +## [0.15.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.2...v0.15.3) (2021-02-22) + + + +## [0.15.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.1...v0.15.2) (2021-02-09) + + +### Bug Fixes + +* add error event handler ([#118](https://github.com/libp2p/js-libp2p-websockets/issues/118)) ([577d350](https://github.com/libp2p/js-libp2p-websockets/commit/577d3505f559b153ec9e0bbca7d31d2f164712bc)) + + + +## [0.15.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.15.0...v0.15.1) (2021-02-05) + + +### Bug Fixes + +* incompatibility with @evanw/esbuild[#740](https://github.com/libp2p/js-libp2p-websockets/issues/740) ([#120](https://github.com/libp2p/js-libp2p-websockets/issues/120)) ([96244f0](https://github.com/libp2p/js-libp2p-websockets/commit/96244f048929c5225905327ae27a88961fe535f8)) + + + +# [0.15.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.1...v0.15.0) (2020-11-24) + + +### Bug Fixes + +* add buffer ([#112](https://github.com/libp2p/js-libp2p-websockets/issues/112)) ([8065e07](https://github.com/libp2p/js-libp2p-websockets/commit/8065e07bad57b5732cdcec5ce3829ac2361604cf)) +* catch thrown maConn errors in listener ([8bfb19a](https://github.com/libp2p/js-libp2p-websockets/commit/8bfb19a78f296c10d8e1a3c0ac608daa9ffcfefc)) +* remove use of assert module ([#101](https://github.com/libp2p/js-libp2p-websockets/issues/101)) ([89d3723](https://github.com/libp2p/js-libp2p-websockets/commit/89d37232b8f603804b6ce5cd8230cc75d2dd8e28)) +* replace node buffers with uint8arrays ([#115](https://github.com/libp2p/js-libp2p-websockets/issues/115)) ([a277bf6](https://github.com/libp2p/js-libp2p-websockets/commit/a277bf6bfbc7ad796e51f7646d7449c203384c06)) + + +### Features + +* custom address filter ([#116](https://github.com/libp2p/js-libp2p-websockets/issues/116)) ([711c721](https://github.com/libp2p/js-libp2p-websockets/commit/711c721b033d28b3c57c37bf9ca98d0f5d2a58b6)) + + +### BREAKING CHANGES + +* Only DNS+WSS addresses are now returned on filter by default in the browser. This can be overritten by the filter option and filters are provided in the module. + + + + +# [0.14.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.6...v0.14.0) (2020-08-11) + + +### Bug Fixes + +* replace node buffers with uint8arrays ([#115](https://github.com/libp2p/js-libp2p-websockets/issues/115)) ([a277bf6](https://github.com/libp2p/js-libp2p-websockets/commit/a277bf6)) + + +### BREAKING CHANGES + +* - All deps used by this module now use Uint8Arrays in place of Buffers + +* chore: remove gh dep + + + + +## [0.13.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.5...v0.13.6) (2020-03-23) + + +### Bug Fixes + +* add buffer ([#112](https://github.com/libp2p/js-libp2p-websockets/issues/112)) ([8065e07](https://github.com/libp2p/js-libp2p-websockets/commit/8065e07)) + + + + +## [0.13.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.4...v0.13.5) (2020-02-26) + + +### Bug Fixes + +* catch thrown maConn errors in listener ([8bfb19a](https://github.com/libp2p/js-libp2p-websockets/commit/8bfb19a)) + + + + +## [0.13.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.3...v0.13.4) (2020-02-14) + + +### Bug Fixes + +* remove use of assert module ([#101](https://github.com/libp2p/js-libp2p-websockets/issues/101)) ([89d3723](https://github.com/libp2p/js-libp2p-websockets/commit/89d3723)) + + + + +## [0.13.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.2...v0.13.3) (2020-02-07) + + + + +## [0.13.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.1...v0.13.2) (2019-12-20) + + + + +## [0.13.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.13.0...v0.13.1) (2019-10-30) + + +### Bug Fixes + +* catch inbound upgrade errors ([#96](https://github.com/libp2p/js-libp2p-websockets/issues/96)) ([5b59fc3](https://github.com/libp2p/js-libp2p-websockets/commit/5b59fc3)) +* support bufferlist usage ([#97](https://github.com/libp2p/js-libp2p-websockets/issues/97)) ([3bf66d0](https://github.com/libp2p/js-libp2p-websockets/commit/3bf66d0)) + + + + +# [0.13.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.3...v0.13.0) (2019-09-30) + + +### Code Refactoring + +* async with multiaddr conn ([#92](https://github.com/libp2p/js-libp2p-websockets/issues/92)) ([ce7bf4f](https://github.com/libp2p/js-libp2p-websockets/commit/ce7bf4f)) + + +### BREAKING CHANGES + +* Switch to using async/await and async iterators. The transport and connection interfaces have changed. See the README for new usage. + + + + +## [0.12.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.2...v0.12.3) (2019-08-21) + + + + +## [0.12.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.1...v0.12.2) (2019-01-24) + + +### Bug Fixes + +* ipv6 naming with multiaddr-to-uri package ([#81](https://github.com/libp2p/js-libp2p-websockets/issues/81)) ([93ef7c3](https://github.com/libp2p/js-libp2p-websockets/commit/93ef7c3)) + + + + +## [0.12.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.12.0...v0.12.1) (2019-01-10) + + +### Bug Fixes + +* reduce bundle size ([68ae2c3](https://github.com/libp2p/js-libp2p-websockets/commit/68ae2c3)) + + + + +# [0.12.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.11.0...v0.12.0) (2018-04-30) + + + + +# [0.11.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.5...v0.11.0) (2018-04-05) + + +### Features + +* add class-is module ([#72](https://github.com/libp2p/js-libp2p-websockets/issues/72)) ([f59cf88](https://github.com/libp2p/js-libp2p-websockets/commit/f59cf88)) +* Pass options to websocket server ([#66](https://github.com/libp2p/js-libp2p-websockets/issues/66)) ([709989a](https://github.com/libp2p/js-libp2p-websockets/commit/709989a)) + + + + +## [0.10.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.4...v0.10.5) (2018-02-20) + + + + +## [0.10.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.2...v0.10.4) (2017-10-22) + + + + +## [0.10.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.2...v0.10.3) (2017-10-22) + + + + +## [0.10.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.1...v0.10.2) (2017-10-20) + + +### Features + +* filter IPFS addrs correctly ([#62](https://github.com/libp2p/js-libp2p-websockets/issues/62)) ([9ddff85](https://github.com/libp2p/js-libp2p-websockets/commit/9ddff85)), closes [#64](https://github.com/libp2p/js-libp2p-websockets/issues/64) +* new aegir ([3d3cdf1](https://github.com/libp2p/js-libp2p-websockets/commit/3d3cdf1)) + + + + +## [0.10.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.10.0...v0.10.1) (2017-07-22) + + + + +# [0.10.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.6...v0.10.0) (2017-03-27) + + +### Bug Fixes + +* **dial:** pass through errors from pull-ws onConnect ([8df8084](https://github.com/libp2p/js-libp2p-websockets/commit/8df8084)) + + + + +## [0.9.6](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.5...v0.9.6) (2017-03-23) + + +### Bug Fixes + +* address parsing ([#57](https://github.com/libp2p/js-libp2p-websockets/issues/57)) ([9fbbe3f](https://github.com/libp2p/js-libp2p-websockets/commit/9fbbe3f)) + + + + +## [0.9.5](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.4...v0.9.5) (2017-03-23) + + + + +## [0.9.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.2...v0.9.4) (2017-03-21) + + + + +## [0.9.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.1...v0.9.2) (2017-02-09) + + + + +## [0.9.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.9.0...v0.9.1) (2016-11-08) + + +### Bug Fixes + +* onConnect does not follow callback pattern ([#36](https://github.com/libp2p/js-libp2p-websockets/issues/36)) ([a821c33](https://github.com/libp2p/js-libp2p-websockets/commit/a821c33)) +* the fix ([0429beb](https://github.com/libp2p/js-libp2p-websockets/commit/0429beb)) + + + + +# [0.9.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.8.1...v0.9.0) (2016-11-03) + + +### Features + +* upgrade to aegir@9 ([#33](https://github.com/libp2p/js-libp2p-websockets/issues/33)) ([e73c99e](https://github.com/libp2p/js-libp2p-websockets/commit/e73c99e)) + + + + +## [0.8.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.8.0...v0.8.1) (2016-09-06) + + +### Features + +* **readme:** update pull-streams section ([64c57f5](https://github.com/libp2p/js-libp2p-websockets/commit/64c57f5)) + + + + +# [0.8.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.2...v0.8.0) (2016-09-06) + + +### Features + +* **pull:** migrate to pull streams ([3f58dca](https://github.com/libp2p/js-libp2p-websockets/commit/3f58dca)) +* **readme:** complete the readme, adding reference about pull-streams ([b62560e](https://github.com/libp2p/js-libp2p-websockets/commit/b62560e)) + + + + +## [0.7.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.1...v0.7.2) (2016-08-29) + + +### Bug Fixes + +* **style:** reduce nested callbacks ([33f5fb3](https://github.com/libp2p/js-libp2p-websockets/commit/33f5fb3)) + + + + +## [0.7.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.7.0...v0.7.1) (2016-08-03) + + + + +# [0.7.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.6.1...v0.7.0) (2016-06-22) + + + + +## [0.6.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.6.0...v0.6.1) (2016-05-29) + + + + +# [0.6.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.5.0...v0.6.0) (2016-05-22) + + + + +# [0.5.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.4...v0.5.0) (2016-05-17) + + + + +## [0.4.4](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.3...v0.4.4) (2016-05-08) + + +### Bug Fixes + +* improve close handling ([cd89354](https://github.com/libp2p/js-libp2p-websockets/commit/cd89354)) + + + + +## [0.4.3](https://github.com/libp2p/js-libp2p-websockets/compare/v0.4.1...v0.4.3) (2016-05-08) + + + + +## [0.4.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.3.2...v0.4.1) (2016-04-25) + + + + +## [0.3.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.2...v0.3.2) (2016-04-14) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.1...v0.2.2) (2016-04-14) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-websockets/compare/v0.2.0...v0.2.1) (2016-03-20) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-websockets/compare/v0.1.0...v0.2.0) (2016-03-14) + + + + +# 0.1.0 (2016-02-26) diff --git a/packages/transport-websockets/LICENSE b/packages/transport-websockets/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-websockets/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-websockets/LICENSE-APACHE b/packages/transport-websockets/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-websockets/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-websockets/LICENSE-MIT b/packages/transport-websockets/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-websockets/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/transport-websockets/README.md b/packages/transport-websockets/README.md new file mode 100644 index 0000000000..1a6a894b79 --- /dev/null +++ b/packages/transport-websockets/README.md @@ -0,0 +1,130 @@ +# @libp2p/websockets + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## Usage + +```sh +> npm i @libp2p/websockets +``` + +### Constructor properties + +```js +import { createLibp2pNode } from 'libp2p' +import { webSockets } from '@libp2p/webrtc-direct' + +const node = await createLibp2p({ + transports: [ + webSockets() + ] + //... other config +}) +await node.start() +await node.dial('/ip4/127.0.0.1/tcp/9090/ws') +``` + +| Name | Type | Description | Default | +| -------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| upgrader | [`Upgrader`](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** | +| filter | `(multiaddrs: Array) => Array` | override transport addresses filter | **Browser:** DNS+WSS multiaddrs / **Node.js:** DNS+\[WS, WSS] multiaddrs | + +You can create your own address filters for this transports, or rely in the filters [provided](./src/filters.js). + +The available filters are: + +- `filters.all` + - Returns all TCP and DNS based addresses, both with `ws` or `wss`. +- `filters.dnsWss` + - Returns all DNS based addresses with `wss`. +- `filters.dnsWsOrWss` + - Returns all DNS based addresses, both with `ws` or `wss`. + +## Libp2p Usage Example + +```js +import { createLibp2pNode } from 'libp2p' +import { websockets } from '@libp2p/websockets' +import filters from '@libp2p/websockets/filters' +import { mplex } from '@libp2p/mplex' +import { noise } from '@libp2p/noise' + +const transportKey = Websockets.prototype[Symbol.toStringTag] +const node = await Libp2p.create({ + transport: [ + websockets({ + // connect to all sockets, even insecure ones + filters: filters.all + }) + ], + streamMuxers: [ + mplex() + ], + connectionEncryption: [ + noise() + ] +}) +``` + +For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-transports](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#customizing-transports). + +## API + +### Transport + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) + +### Connection + +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-websockets/package.json b/packages/transport-websockets/package.json new file mode 100644 index 0000000000..f6a6b8a18a --- /dev/null +++ b/packages/transport-websockets/package.json @@ -0,0 +1,107 @@ +{ + "name": "@libp2p/websockets", + "version": "6.0.3", + "description": "JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-websockets#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./filters": { + "types": "./dist/src/filters.d.ts", + "import": "./dist/src/filters.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser -f ./dist/test/browser.js --cov", + "test:chrome-webworker": "aegir test -t webworker -f ./dist/test/browser.js", + "test:firefox": "aegir test -t browser -f ./dist/test/browser.js -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -f ./dist/test/browser.js -- --browser firefox", + "test:node": "aegir test -t node -f ./dist/test/node.js --cov", + "test:electron-main": "aegir test -t electron-main -f ./dist/test/node.js --cov" + }, + "dependencies": { + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/utils": "^3.0.0", + "@multiformats/mafmt": "^12.1.2", + "@multiformats/multiaddr": "^12.1.3", + "@multiformats/multiaddr-to-uri": "^9.0.2", + "@types/ws": "^8.5.4", + "abortable-iterator": "^5.0.1", + "it-ws": "^6.0.0", + "p-defer": "^4.0.0", + "p-timeout": "^6.0.0", + "wherearewe": "^2.0.1", + "ws": "^8.12.1" + }, + "devDependencies": { + "@libp2p/interface-mocks": "^12.0.0", + "@libp2p/interface-transport-compliance-tests": "^4.0.0", + "aegir": "^39.0.10", + "is-loopback-addr": "^2.0.1", + "it-all": "^3.0.1", + "it-drain": "^3.0.2", + "it-goodbye": "^4.0.1", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "p-wait-for": "^5.0.0", + "uint8arraylist": "^2.3.2", + "uint8arrays": "^4.0.3" + }, + "browser": { + "./dist/src/listener.js": "./dist/src/listener.browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-websockets/src/constants.ts b/packages/transport-websockets/src/constants.ts new file mode 100644 index 0000000000..e8c3939e1d --- /dev/null +++ b/packages/transport-websockets/src/constants.ts @@ -0,0 +1,10 @@ +// p2p multi-address code +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 + +export const CODE_TCP = 6 +export const CODE_WS = 477 +export const CODE_WSS = 478 + +// Time to wait for a connection to close gracefully before destroying it manually +export const CLOSE_TIMEOUT = 2000 diff --git a/packages/transport-websockets/src/filters.ts b/packages/transport-websockets/src/filters.ts new file mode 100644 index 0000000000..8536e7c97a --- /dev/null +++ b/packages/transport-websockets/src/filters.ts @@ -0,0 +1,66 @@ +import * as mafmt from '@multiformats/mafmt' +import { + CODE_CIRCUIT, + CODE_P2P, + CODE_TCP, + CODE_WS, + CODE_WSS +} from './constants.js' +import type { Multiaddr } from '@multiformats/multiaddr' + +export function all (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSockets.matches(testMa) || + mafmt.WebSocketsSecure.matches(testMa) + }) +} + +export function wss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSocketsSecure.matches(testMa) + }) +} + +export function dnsWss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }) +} + +export function dnsWsOrWss (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + const testMa = ma.decapsulateCode(CODE_P2P) + + // WS + if (mafmt.WebSockets.matches(testMa)) { + return mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WS)) + } + + // WSS + return mafmt.WebSocketsSecure.matches(testMa) && + mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) + }) +} diff --git a/packages/transport-websockets/src/index.ts b/packages/transport-websockets/src/index.ts new file mode 100644 index 0000000000..0ce23bd7e8 --- /dev/null +++ b/packages/transport-websockets/src/index.ts @@ -0,0 +1,143 @@ +import { type Transport, type MultiaddrFilter, symbol, type CreateListenerOptions, type DialOptions, type Listener } from '@libp2p/interface-transport' +import { AbortError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri' +import { connect, type WebSocketOptions } from 'it-ws/client' +import pDefer from 'p-defer' +import { isBrowser, isWebWorker } from 'wherearewe' +import * as filters from './filters.js' +import { createListener } from './listener.js' +import { socketToMaConn } from './socket-to-conn.js' +import type { Connection } from '@libp2p/interface-connection' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Server } from 'http' +import type { DuplexWebSocket } from 'it-ws/duplex' +import type { ClientOptions } from 'ws' + +const log = logger('libp2p:websockets') + +export interface WebSocketsInit extends AbortOptions, WebSocketOptions { + filter?: MultiaddrFilter + websocket?: ClientOptions + server?: Server +} + +class WebSockets implements Transport { + private readonly init?: WebSocketsInit + + constructor (init?: WebSocketsInit) { + this.init = init + } + + readonly [Symbol.toStringTag] = '@libp2p/websockets' + + readonly [symbol] = true + + async dial (ma: Multiaddr, options: DialOptions): Promise { + log('dialing %s', ma) + options = options ?? {} + + const socket = await this._connect(ma, options) + const maConn = socketToMaConn(socket, ma) + log('new outbound connection %s', maConn.remoteAddr) + + const conn = await options.upgrader.upgradeOutbound(maConn) + log('outbound connection %s upgraded', maConn.remoteAddr) + return conn + } + + async _connect (ma: Multiaddr, options: AbortOptions): Promise { + if (options?.signal?.aborted === true) { + throw new AbortError() + } + const cOpts = ma.toOptions() + log('dialing %s:%s', cOpts.host, cOpts.port) + + const errorPromise = pDefer() + const errfn = (err: any): void => { + log.error('connection error:', err) + + errorPromise.reject(err) + } + + const rawSocket = connect(toUri(ma), this.init) + + if (rawSocket.socket.on != null) { + rawSocket.socket.on('error', errfn) + } else { + rawSocket.socket.onerror = errfn + } + + if (options.signal == null) { + await Promise.race([rawSocket.connected(), errorPromise.promise]) + + log('connected %s', ma) + return rawSocket + } + + // Allow abort via signal during connect + let onAbort + const abort = new Promise((resolve, reject) => { + onAbort = () => { + reject(new AbortError()) + rawSocket.close().catch(err => { + log.error('error closing raw socket', err) + }) + } + + // Already aborted? + if (options?.signal?.aborted === true) { + onAbort(); return + } + + options?.signal?.addEventListener('abort', onAbort) + }) + + try { + await Promise.race([abort, errorPromise.promise, rawSocket.connected()]) + } finally { + if (onAbort != null) { + options?.signal?.removeEventListener('abort', onAbort) + } + } + + log('connected %s', ma) + return rawSocket + } + + /** + * Creates a Websockets listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound` + */ + createListener (options: CreateListenerOptions): Listener { + return createListener({ ...this.init, ...options }) + } + + /** + * Takes a list of `Multiaddr`s and returns only valid Websockets addresses. + * By default, in a browser environment only DNS+WSS multiaddr is accepted, + * while in a Node.js environment DNS+{WS, WSS} multiaddrs are accepted. + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + if (this.init?.filter != null) { + return this.init?.filter(multiaddrs) + } + + // Browser + if (isBrowser || isWebWorker) { + return filters.wss(multiaddrs) + } + + return filters.all(multiaddrs) + } +} + +export function webSockets (init: WebSocketsInit = {}): (components?: any) => Transport { + return () => { + return new WebSockets(init) + } +} diff --git a/packages/transport-websockets/src/listener.browser.ts b/packages/transport-websockets/src/listener.browser.ts new file mode 100644 index 0000000000..d5568a9d8a --- /dev/null +++ b/packages/transport-websockets/src/listener.browser.ts @@ -0,0 +1,5 @@ +import type { Listener } from '@libp2p/interface-transport' + +export function createListener (): Listener { + throw new Error('WebSocket Servers can not be created in the browser!') +} diff --git a/packages/transport-websockets/src/listener.ts b/packages/transport-websockets/src/listener.ts new file mode 100644 index 0000000000..abb63aa894 --- /dev/null +++ b/packages/transport-websockets/src/listener.ts @@ -0,0 +1,160 @@ +import os from 'os' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' +import { logger } from '@libp2p/logger' +import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' +import { multiaddr, protocols } from '@multiformats/multiaddr' +import { createServer } from 'it-ws/server' +import { socketToMaConn } from './socket-to-conn.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Listener, ListenerEvents, CreateListenerOptions } from '@libp2p/interface-transport' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Server } from 'http' +import type { DuplexWebSocket } from 'it-ws/duplex' +import type { WebSocketServer } from 'it-ws/server' + +const log = logger('libp2p:websockets:listener') + +class WebSocketListener extends EventEmitter implements Listener { + private readonly connections: Set + private listeningMultiaddr?: Multiaddr + private readonly server: WebSocketServer + + constructor (init: WebSocketListenerInit) { + super() + + // Keep track of open connections to destroy when the listener is closed + this.connections = new Set() + + const self = this // eslint-disable-line @typescript-eslint/no-this-alias + + this.server = createServer({ + ...init, + onConnection: (stream: DuplexWebSocket) => { + const maConn = socketToMaConn(stream, toMultiaddr(stream.remoteAddress ?? '', stream.remotePort ?? 0)) + log('new inbound connection %s', maConn.remoteAddr) + + this.connections.add(stream) + + stream.socket.on('close', function () { + self.connections.delete(stream) + }) + + try { + void init.upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection %s upgraded', maConn.remoteAddr) + + if (init?.handler != null) { + init?.handler(conn) + } + + self.dispatchEvent(new CustomEvent('connection', { + detail: conn + })) + }) + .catch(async err => { + log.error('inbound connection failed to upgrade', err) + + await maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + }) + } catch (err) { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + } + } + }) + + this.server.on('listening', () => { + this.dispatchEvent(new CustomEvent('listening')) + }) + this.server.on('error', (err: Error) => { + this.dispatchEvent(new CustomEvent('error', { + detail: err + })) + }) + this.server.on('close', () => { + this.dispatchEvent(new CustomEvent('close')) + }) + } + + async close (): Promise { + await Promise.all( + Array.from(this.connections).map(async maConn => { await maConn.close() }) + ) + + if (this.server.address() == null) { + // not listening, close will throw an error + return + } + + await this.server.close() + } + + async listen (ma: Multiaddr): Promise { + this.listeningMultiaddr = ma + + await this.server.listen(ma.toOptions()) + } + + getAddrs (): Multiaddr[] { + const multiaddrs = [] + const address = this.server.address() + + if (address == null) { + throw new Error('Listener is not ready yet') + } + + if (typeof address === 'string') { + throw new Error('Wrong address type received - expected AddressInfo, got string - are you trying to listen on a unix socket?') + } + + if (this.listeningMultiaddr == null) { + throw new Error('Listener is not ready yet') + } + + const ipfsId = this.listeningMultiaddr.getPeerId() + const protos = this.listeningMultiaddr.protos() + + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (protos.some(proto => proto.code === protocols('ip4').code)) { + const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' + let m = this.listeningMultiaddr.decapsulate('tcp') + m = m.encapsulate(`/tcp/${address.port}${wsProto}`) + if (ipfsId != null) { + m = m.encapsulate(`/p2p/${ipfsId}`) + } + + if (m.toString().includes('0.0.0.0')) { + const netInterfaces = os.networkInterfaces() + Object.values(netInterfaces).forEach(niInfos => { + if (niInfos == null) { + return + } + + niInfos.forEach(ni => { + if (ni.family === 'IPv4') { + multiaddrs.push(multiaddr(m.toString().replace('0.0.0.0', ni.address))) + } + }) + }) + } else { + multiaddrs.push(m) + } + } + + return multiaddrs + } +} + +export interface WebSocketListenerInit extends CreateListenerOptions { + server?: Server +} + +export function createListener (init: WebSocketListenerInit): Listener { + return new WebSocketListener(init) +} diff --git a/packages/transport-websockets/src/socket-to-conn.ts b/packages/transport-websockets/src/socket-to-conn.ts new file mode 100644 index 0000000000..a3bf2b8699 --- /dev/null +++ b/packages/transport-websockets/src/socket-to-conn.ts @@ -0,0 +1,71 @@ +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import pTimeout from 'p-timeout' +import { CLOSE_TIMEOUT } from './constants.js' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { DuplexWebSocket } from 'it-ws/duplex' + +const log = logger('libp2p:websockets:socket') + +export interface SocketToConnOptions extends AbortOptions { + localAddr?: Multiaddr +} + +// Convert a stream into a MultiaddrConnection +// https://github.com/libp2p/interface-transport#multiaddrconnection +export function socketToMaConn (stream: DuplexWebSocket, remoteAddr: Multiaddr, options?: SocketToConnOptions): MultiaddrConnection { + options = options ?? {} + + const maConn: MultiaddrConnection = { + async sink (source) { + if ((options?.signal) != null) { + source = abortableSource(source, options.signal) + } + + try { + await stream.sink(source) + } catch (err: any) { + if (err.type !== 'aborted') { + log.error(err) + } + } + }, + + source: (options.signal != null) ? abortableSource(stream.source, options.signal) : stream.source, + + remoteAddr, + + timeline: { open: Date.now() }, + + async close () { + const start = Date.now() + + try { + await pTimeout(stream.close(), { + milliseconds: CLOSE_TIMEOUT + }) + } catch (err) { + const { host, port } = maConn.remoteAddr.toOptions() + log('timeout closing stream to %s:%s after %dms, destroying it manually', + host, port, Date.now() - start) + + stream.destroy() + } finally { + maConn.timeline.close = Date.now() + } + } + } + + stream.socket.addEventListener('close', () => { + // In instances where `close` was not explicitly called, + // such as an iterable stream ending, ensure we have set the close + // timeline + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + }, { once: true }) + + return maConn +} diff --git a/packages/transport-websockets/test/browser.ts b/packages/transport-websockets/test/browser.ts new file mode 100644 index 0000000000..5fc89b6f89 --- /dev/null +++ b/packages/transport-websockets/test/browser.ts @@ -0,0 +1,98 @@ +/* eslint-env mocha */ + +import { mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { isBrowser, isWebWorker } from 'wherearewe' +import { webSockets } from '../src/index.js' +import type { Connection } from '@libp2p/interface-connection' +import type { Transport } from '@libp2p/interface-transport' + +const protocol = '/echo/1.0.0' + +describe('libp2p-websockets', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + let ws: Transport + let conn: Connection + + beforeEach(async () => { + ws = webSockets()() + conn = await ws.dial(ma, { + upgrader: mockUpgrader({ + events: new EventEmitter() + }) + }) + }) + + afterEach(async () => { + await conn.close() + }) + + it('echo', async () => { + const data = uint8ArrayFromString('hey') + const stream = await conn.newStream([protocol]) + + const res = await pipe( + [data], + stream, + async (source) => all(source) + ) + + expect(res[0].subarray()).to.equalBytes(data) + }) + + it('should filter out no wss websocket addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/443/wss') + const ma3 = multiaddr('/ip6/::1/tcp/80/ws') + const ma4 = multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + + if (isBrowser || isWebWorker) { + expect(valid.length).to.equal(2) + expect(valid).to.deep.equal([ma2, ma4]) + } else { + expect(valid.length).to.equal(4) + } + }) + + describe('stress', () => { + it('one big write', async () => { + const data = new Uint8Array(1000000).fill(5) + const stream = await conn.newStream([protocol]) + + const res = await pipe( + [data], + stream, + async (source) => all(source) + ) + + expect(res[0].subarray()).to.deep.equal(data) + }) + + it('many writes', async function () { + this.timeout(60000) + + const count = 20000 + const data = Array(count).fill(0).map(() => uint8ArrayFromString(Math.random().toString())) + const stream = await conn.newStream([protocol]) + + const res = await pipe( + data, + stream, + async (source) => all(source) + ) + + expect(res.map(list => list.subarray())).to.deep.equal(data) + }) + }) + + it('.createServer throws in browser', () => { + expect(webSockets()().createListener).to.throw() + }) +}) diff --git a/packages/transport-websockets/test/compliance.node.ts b/packages/transport-websockets/test/compliance.node.ts new file mode 100644 index 0000000000..eef82737de --- /dev/null +++ b/packages/transport-websockets/test/compliance.node.ts @@ -0,0 +1,60 @@ +/* eslint-env mocha */ + +import http from 'http' +import tests from '@libp2p/interface-transport-compliance-tests' +import { multiaddr } from '@multiformats/multiaddr' +import * as filters from '../src/filters.js' +import { webSockets } from '../src/index.js' +import type { WebSocketListenerInit } from '../src/listener.js' +import type { Listener } from '@libp2p/interface-transport' + +describe('interface-transport compliance', () => { + tests({ + async setup () { + const ws = webSockets({ filter: filters.all })() + const addrs = [ + multiaddr('/ip4/127.0.0.1/tcp/9091/ws'), + multiaddr('/ip4/127.0.0.1/tcp/9092/ws'), + multiaddr('/dns4/ipfs.io/tcp/9092/ws'), + multiaddr('/dns4/ipfs.io/tcp/9092/wss') + ] + + let delayMs = 0 + const delayedCreateListener = (options: WebSocketListenerInit): Listener => { + // A server that will delay the upgrade event by delayMs + options.server = new Proxy(http.createServer(), { + get (server, prop) { + if (prop === 'on') { + return (event: string, handler: (...args: any[]) => void) => { + server.on(event, (...args) => { + if (event !== 'upgrade' || delayMs === 0) { + handler(...args); return + } + setTimeout(() => { handler(...args) }, delayMs) + }) + } + } + // @ts-expect-error cannot access props with a string + return server[prop] + } + }) + + return ws.createListener(options) + } + + const wsProxy = new Proxy(ws, { + // @ts-expect-error cannot access props with a string + get: (_, prop) => prop === 'createListener' ? delayedCreateListener : ws[prop] + }) + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay (ms: number) { delayMs = ms }, + restore () { delayMs = 0 } + } + + return { transport: wsProxy, addrs, connector } + }, + async teardown () {} + }) +}) diff --git a/packages/transport-websockets/test/fixtures/certificate.pem b/packages/transport-websockets/test/fixtures/certificate.pem new file mode 100644 index 0000000000..840776c01c --- /dev/null +++ b/packages/transport-websockets/test/fixtures/certificate.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu +bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG +A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 ++r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB +JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ +sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF +AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E +QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu +rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/packages/transport-websockets/test/fixtures/key.pem b/packages/transport-websockets/test/fixtures/key.pem new file mode 100644 index 0000000000..3649a93301 --- /dev/null +++ b/packages/transport-websockets/test/fixtures/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ +3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg +f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB +AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l +3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk +Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs +ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq +DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL +Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX +OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav +AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ +yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 +JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/packages/transport-websockets/test/node.ts b/packages/transport-websockets/test/node.ts new file mode 100644 index 0000000000..43d88d5d7a --- /dev/null +++ b/packages/transport-websockets/test/node.ts @@ -0,0 +1,639 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ + +import fs from 'fs' +import http from 'http' +import https from 'https' +import { mockRegistrar, mockUpgrader } from '@libp2p/interface-mocks' +import { EventEmitter } from '@libp2p/interfaces/events' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isLoopbackAddr } from 'is-loopback-addr' +import all from 'it-all' +import drain from 'it-drain' +import { goodbye } from 'it-goodbye' +import { pipe } from 'it-pipe' +import defer from 'p-defer' +import waitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import * as filters from '../src/filters.js' +import { webSockets } from '../src/index.js' +import type { Listener, Transport } from '@libp2p/interface-transport' +import type { Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import './compliance.node.js' + +async function * toBuffers (source: Source): AsyncGenerator { + for await (const list of source) { + yield * list + } +} + +const protocol = '/say-hello/1.0.0' +const registrar = mockRegistrar() +void registrar.handle(protocol, (evt) => { + void pipe([ + uint8ArrayFromString('hey') + ], + evt.stream, + drain + ) +}) +const upgrader = mockUpgrader({ + registrar, + events: new EventEmitter() +}) + +describe('instantiate the transport', () => { + it('create', () => { + const ws = webSockets()() + expect(ws).to.exist() + }) +}) + +describe('listen', () => { + it('should close connections when stopping the listener', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + + const ws = webSockets()() + const listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + + const conn = await ws.dial(ma, { + upgrader + }) + const stream = await conn.newStream([protocol]) + void pipe(stream, stream) + + await listener.close() + + await waitFor(() => conn.stat.timeline.close != null) + }) + + describe('ip4', () => { + let ws: Transport + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + let listener: Listener + + beforeEach(() => { + ws = webSockets()() + }) + + afterEach(async () => { + await listener.close() + }) + + it('listen, check for promise', async () => { + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + }) + + it('listen, check for listening event', (done) => { + listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + done() + }) + + void listener.listen(ma) + }) + + it('should error on starting two listeners on same address', async () => { + listener = ws.createListener({ upgrader }) + const dumbServer = http.createServer() + await new Promise(resolve => dumbServer.listen(ma.toOptions().port, resolve)) + await expect(listener.listen(ma)).to.eventually.rejectedWith('listen EADDRINUSE') + await new Promise(resolve => dumbServer.close(() => { resolve() })) + }) + + it('listen, check for the close event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => { done() }) + void listener.close() + }) + + void listener.listen(ma) + }) + + it('listen on addr with /ipfs/QmHASH', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + }) + + it('listen on port 0', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().port)).to.not.include(0) + }) + + it('listen on any Interface', async () => { + const ma = multiaddr('/ip4/0.0.0.0/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + }) + + it('getAddrs', async () => { + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs[0]).to.deep.equal(ma) + }) + + it('getAddrs on port 0 listen', async () => { + const addr = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') + }) + + it('getAddrs from listening on 0.0.0.0', async () => { + const addr = multiaddr('/ip4/0.0.0.0/tcp/47382/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + }) + + it('getAddrs from listening on 0.0.0.0 and port 0', async () => { + const addr = multiaddr('/ip4/0.0.0.0/tcp/0/ws') + listener = ws.createListener({ upgrader }) + await listener.listen(addr) + const addrs = listener.getAddrs() + expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') + expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') + }) + + it('getAddrs preserves p2p Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/47382/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener({ upgrader }) + + await listener.listen(ma) + const addrs = listener.getAddrs() + expect(addrs.length).to.equal(1) + expect(addrs[0]).to.deep.equal(ma) + }) + }) + + describe('ip6', () => { + let ws: Transport + const ma = multiaddr('/ip6/::1/tcp/9091/ws') + + beforeEach(() => { + ws = webSockets()() + }) + + it('listen, check for promise', async () => { + const listener = ws.createListener({ upgrader }) + await listener.listen(ma) + await listener.close() + }) + + it('listen, check for listening event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + void listener.close().then(done, done) + }) + + void listener.listen(ma) + }) + + it('listen, check for the close event', (done) => { + const listener = ws.createListener({ upgrader }) + + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => { done() }) + void listener.close() + }) + + void listener.listen(ma) + }) + + it('listen on addr with /ipfs/QmHASH', async () => { + const ma = multiaddr('/ip6/::1/tcp/9091/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const listener = ws.createListener({ upgrader }) + await listener.listen(ma) + await listener.close() + }) + }) +}) + +describe('dial', () => { + describe('ip4', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/127.0.0.1/tcp/9091/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ upgrader }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial', async () => { + const conn = await ws.dial(ma, { upgrader }) + const stream = await conn.newStream([protocol]) + + expect((await all(stream.source)).map(list => list.subarray())).to.deep.equal([uint8ArrayFromString('hey')]) + await conn.close() + }) + + it('dial with p2p Id', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9091/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const conn = await ws.dial(ma, { upgrader }) + const stream = await conn.newStream([protocol]) + + expect((await all(stream.source)).map(list => list.subarray())).to.deep.equal([uint8ArrayFromString('hey')]) + await conn.close() + }) + + it('dial should throw on immediate abort', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + const controller = new AbortController() + + const conn = ws.dial(ma, { signal: controller.signal, upgrader }) + controller.abort() + + await expect(conn).to.eventually.be.rejected() + }) + + it('should resolve port 0', async () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + const ws = webSockets()() + + // Create a Promise that resolves when a connection is handled + const deferred = defer() + + const listener = ws.createListener({ handler: deferred.resolve, upgrader }) + + // Listen on the multiaddr + await listener.listen(ma) + + const localAddrs = listener.getAddrs() + expect(localAddrs.length).to.equal(1) + + // Dial to that address + await ws.dial(localAddrs[0], { upgrader }) + + // Wait for the incoming dial to be handled + await deferred.promise + + // close the listener + await listener.close() + }) + }) + + describe('ip4 no loopback', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/0.0.0.0/tcp/0/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial', async () => { + const addrs = listener.getAddrs().filter((ma) => { + const { address } = ma.nodeAddress() + + return !isLoopbackAddr(address) + }) + + // Dial first no loopback address + const conn = await ws.dial(addrs[0], { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + await expect(pipe( + s, + stream, + toBuffers, + s + )).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + }) + + describe('ip4 with wss', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip4/127.0.0.1/tcp/37284/wss') + let server: https.Server + + beforeEach(async () => { + server = https.createServer({ + cert: fs.readFileSync('./test/fixtures/certificate.pem'), + key: fs.readFileSync('./test/fixtures/key.pem') + }) + ws = webSockets({ websocket: { rejectUnauthorized: false }, server })() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { + await listener.close() + server.close() + }) + + it('should listen on wss address', () => { + const addrs = listener.getAddrs() + + expect(addrs).to.have.lengthOf(1) + expect(ma.equals(addrs[0])).to.eql(true) + }) + + it('dial ip4', async () => { + const conn = await ws.dial(ma, { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + const res = await pipe(s, stream, toBuffers, s) + + expect(res[0]).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() + }) + }) + + describe('ip6', () => { + let ws: Transport + let listener: Listener + const ma = multiaddr('/ip6/::1/tcp/9091/ws') + + beforeEach(async () => { + ws = webSockets()() + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream([protocol]).then(async (stream) => { + await pipe(stream, stream) + }) + }, + upgrader + }) + await listener.listen(ma) + }) + + afterEach(async () => { await listener.close() }) + + it('dial ip6', async () => { + const conn = await ws.dial(ma, { upgrader }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const stream = await conn.newStream([protocol]) + + await expect(pipe(s, stream, toBuffers, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + + it('dial with p2p Id', async () => { + const ma = multiaddr('/ip6/::1/tcp/9091/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const conn = await ws.dial(ma, { upgrader }) + + const s = goodbye({ + source: [uint8ArrayFromString('hey')], + sink: all + }) + const stream = await conn.newStream([protocol]) + + await expect(pipe(s, stream, toBuffers, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) + }) + }) +}) + +describe('filter addrs', () => { + let ws: Transport + + describe('default filter addrs with only dns', () => { + before(() => { + ws = webSockets()() + }) + + it('should filter out invalid WS addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma3 = multiaddr('/ip6/::1/tcp/80') + const ma4 = multiaddr('/dnsaddr/ipfs.io/tcp/80') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + + it('should filter correct dns address', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/ws') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws') + const ma3 = multiaddr('/dnsaddr/ipfs.io/tcp/80/wss') + + const valid = ws.filter([ma1, ma2, ma3]) + expect(valid.length).to.equal(3) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + expect(valid[2]).to.deep.equal(ma3) + }) + + it('should filter correct dns address with ipfs id', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns4 address', function () { + const ma1 = multiaddr('/dns4/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns4/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address with ipfs id', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + }) + + describe('custom filter addrs', () => { + before(() => { + ws = webSockets()({ filter: filters.all }) + }) + + it('should fail invalid WS addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma2 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma3 = multiaddr('/ip6/::1/tcp/80') + const ma4 = multiaddr('/dnsaddr/ipfs.io/tcp/80') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + expect(valid.length).to.equal(0) + }) + + it('should filter correct ipv4 addresses', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv4 addresses with ipfs id', function () { + const ma1 = multiaddr('/ip4/127.0.0.1/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/80/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv6 address', function () { + const ma1 = multiaddr('/ip6/::1/tcp/80/ws') + const ma2 = multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct ipv6 addresses with ipfs id', function () { + const ma1 = multiaddr('/ip6/::1/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip6/::1/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns address', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/ws') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws') + const ma3 = multiaddr('/dnsaddr/ipfs.io/tcp/80/wss') + + const valid = ws.filter([ma1, ma2, ma3]) + expect(valid.length).to.equal(3) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + expect(valid[2]).to.deep.equal(ma3) + }) + + it('should filter correct dns address with ipfs id', function () { + const ma1 = multiaddr('/dnsaddr/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dnsaddr/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns4 address', function () { + const ma1 = multiaddr('/dns4/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns4/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter correct dns6 address with ipfs id', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/dns6/ipfs.io/tcp/443/wss/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma2) + }) + + it('should filter mixed addresses', function () { + const ma1 = multiaddr('/dns6/ipfs.io/tcp/80/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + const ma2 = multiaddr('/ip4/127.0.0.1/tcp/9090') + const ma3 = multiaddr('/ip4/127.0.0.1/udp/9090') + const ma4 = multiaddr('/dns6/ipfs.io/ws') + const mh5 = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw' + + '/p2p-circuit/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma1, ma2, ma3, ma4, mh5]) + expect(valid.length).to.equal(2) + expect(valid[0]).to.deep.equal(ma1) + expect(valid[1]).to.deep.equal(ma4) + }) + + it('filter a single addr for this transport', () => { + const ma = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + + const valid = ws.filter([ma]) + expect(valid.length).to.equal(1) + expect(valid[0]).to.deep.equal(ma) + }) + }) +}) diff --git a/packages/transport-websockets/tsconfig.json b/packages/transport-websockets/tsconfig.json new file mode 100644 index 0000000000..9047d208ae --- /dev/null +++ b/packages/transport-websockets/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-mocks" + }, + { + "path": "../interface-transport" + }, + { + "path": "../interface-transport-compliance-tests" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + }, + { + "path": "../utils" + } + ] +} diff --git a/packages/transport-webtransport/.aegir.js b/packages/transport-webtransport/.aegir.js new file mode 100644 index 0000000000..a31c770f9b --- /dev/null +++ b/packages/transport-webtransport/.aegir.js @@ -0,0 +1,61 @@ +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' +import defer from 'p-defer' + +/** @type {import('aegir/types').PartialOptions} */ +export default { + test: { + async before() { + if (!existsSync('./go-libp2p-webtransport-server/main')) { + await new Promise((resolve, reject) => { + exec('go build -o main main.go', + { cwd: './go-libp2p-webtransport-server' }, + (error, stdout, stderr) => { + if (error) { + reject(error) + console.error(`exec error: ${error}`) + return + } + resolve() + }) + }) + } + + const server = spawn('./main', [], { cwd: './go-libp2p-webtransport-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log('stderr:', data.toString()) + }) + const serverAddr = defer() + const serverAddr6 = defer() + + server.stdout.on('data', (buf) => { + const data = buf.toString() + + console.log('stdout:', data); + if (data.includes('addr=/ip4')) { + // Parse the addr out + serverAddr.resolve(`/ip4${data.match(/addr=\/ip4(.*)/)[1]}`) + } + + if (data.includes('addr=/ip6')) { + // Parse the addr out + serverAddr6.resolve(`/ip6${data.match(/addr=\/ip6(.*)/)[1]}`) + } + }) + + return { + server, + env: { + serverAddr: await serverAddr.promise, + serverAddr6: await serverAddr6.promise + } + } + }, + async after(_, { server }) { + server.kill('SIGINT') + } + }, + build: { + bundlesizeMax: '18kB' + } +} diff --git a/packages/transport-webtransport/.gitignore b/packages/transport-webtransport/.gitignore new file mode 100644 index 0000000000..431f7bde19 --- /dev/null +++ b/packages/transport-webtransport/.gitignore @@ -0,0 +1 @@ +go-libp2p-webtransport-server/main diff --git a/packages/transport-webtransport/CHANGELOG.md b/packages/transport-webtransport/CHANGELOG.md new file mode 100644 index 0000000000..562095f662 --- /dev/null +++ b/packages/transport-webtransport/CHANGELOG.md @@ -0,0 +1,142 @@ +## [2.0.2](https://github.com/libp2p/js-libp2p-webtransport/compare/v2.0.1...v2.0.2) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([56ef477](https://github.com/libp2p/js-libp2p-webtransport/commit/56ef477cff1214bebb150414ad6db36174ee0fa1)) +* Update .github/workflows/stale.yml [skip ci] ([cdbdfd4](https://github.com/libp2p/js-libp2p-webtransport/commit/cdbdfd4dc50a5e2bd3729938786a427ae7802f75)) + + +### Dependencies + +* bump @chainsafe/libp2p-noise from 11.0.4 to 12.0.1 ([#80](https://github.com/libp2p/js-libp2p-webtransport/issues/80)) ([599dab1](https://github.com/libp2p/js-libp2p-webtransport/commit/599dab1b4f6ae816b0c0feefc926c1b38d24b676)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-webtransport/compare/v2.0.0...v2.0.1) (2023-04-28) + + +### Dependencies + +* bump @libp2p/interface-transport from 2.1.3 to 4.0.1 ([#72](https://github.com/libp2p/js-libp2p-webtransport/issues/72)) ([04b977d](https://github.com/libp2p/js-libp2p-webtransport/commit/04b977db00bf0d71574f4618eaf0f070a5ce9441)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.11...v2.0.0) (2023-04-28) + + +### ⚠ BREAKING CHANGES + +* the type of the source/sink properties have changed + +### Dependencies + +* update stream types ([#66](https://github.com/libp2p/js-libp2p-webtransport/issues/66)) ([3772060](https://github.com/libp2p/js-libp2p-webtransport/commit/3772060df436f72976d9aaaa9d619ef5e7d93408)) + +## [1.0.11](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.10...v1.0.11) (2023-03-28) + + +### Bug Fixes + +* allow dialling ip6 webtransport addresses ([#60](https://github.com/libp2p/js-libp2p-webtransport/issues/60)) ([fe4612a](https://github.com/libp2p/js-libp2p-webtransport/commit/fe4612a37620203a04b70ec96acce7c890f2ec7d)) + +## [1.0.10](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.9...v1.0.10) (2023-03-24) + + +### Bug Fixes + +* window is not defined in worker contexts ([#59](https://github.com/libp2p/js-libp2p-webtransport/issues/59)) ([94c646b](https://github.com/libp2p/js-libp2p-webtransport/commit/94c646bdcdb9c1e5fa13d00a3ae03bc6c5727404)) + +## [1.0.9](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.8...v1.0.9) (2023-03-22) + + +### Dependencies + +* update @multiformats/multiaddr to 12.x.x ([#58](https://github.com/libp2p/js-libp2p-webtransport/issues/58)) ([1b3d005](https://github.com/libp2p/js-libp2p-webtransport/commit/1b3d005e4d82a2fec3e9ab32bb813cae9e073af2)) + + +### Documentation + +* **example:** add helper instructions for running example ([#56](https://github.com/libp2p/js-libp2p-webtransport/issues/56)) ([0f0d54b](https://github.com/libp2p/js-libp2p-webtransport/commit/0f0d54b56a60ea80a91f96b05b74f2d42f443a15)) + +## [1.0.8](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.7...v1.0.8) (2023-03-22) + + +### Trivial Changes + +* constrain go version examples run with ([#57](https://github.com/libp2p/js-libp2p-webtransport/issues/57)) ([aa177a8](https://github.com/libp2p/js-libp2p-webtransport/commit/aa177a8afcdb4fcccbd22cd97e8676fa00a44187)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([9435f0d](https://github.com/libp2p/js-libp2p-webtransport/commit/9435f0d1f9a93169e26789c6a3cb0706f06149cd)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([63f0c33](https://github.com/libp2p/js-libp2p-webtransport/commit/63f0c33bc2b876c11ec756e132bb679f32f46245)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([5e3a711](https://github.com/libp2p/js-libp2p-webtransport/commit/5e3a71197f1c8e89a9a5c05bdeee544bc3e460f1)) + + +### Dependencies + +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#54](https://github.com/libp2p/js-libp2p-webtransport/issues/54)) ([23bbd82](https://github.com/libp2p/js-libp2p-webtransport/commit/23bbd82bf2c3caa25d5964c98f7362736134862d)) + +## [1.0.7](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.6...v1.0.7) (2023-01-12) + + +### Dependencies + +* Update deps and add quic-v1 support ([#44](https://github.com/libp2p/js-libp2p-webtransport/issues/44)) ([d1613b1](https://github.com/libp2p/js-libp2p-webtransport/commit/d1613b10c1c8164fadcbe9a28175b7f0099d1645)), closes [#35](https://github.com/libp2p/js-libp2p-webtransport/issues/35) + + +### Documentation + +* update project config to publish api docs ([#45](https://github.com/libp2p/js-libp2p-webtransport/issues/45)) ([bdbf402](https://github.com/libp2p/js-libp2p-webtransport/commit/bdbf4025b5f80d6cc8af49596da09386538dc791)) + +## [1.0.6](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.5...v1.0.6) (2022-12-06) + + +### Bug Fixes + +* Make a fix release with [#32](https://github.com/libp2p/js-libp2p-webtransport/issues/32) ([#34](https://github.com/libp2p/js-libp2p-webtransport/issues/34)) ([66a38f6](https://github.com/libp2p/js-libp2p-webtransport/commit/66a38f6e452e72042ad10ef8521544d8b5afecff)) + +## [1.0.5](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.4...v1.0.5) (2022-11-16) + + +### Bug Fixes + +* Close stream after sink ([#23](https://github.com/libp2p/js-libp2p-webtransport/issues/23)) ([a95720c](https://github.com/libp2p/js-libp2p-webtransport/commit/a95720c367c8061ae45b4ae4bc4180e3ceea61cc)) + +## [1.0.4](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.3...v1.0.4) (2022-11-01) + + +### Documentation + +* Use textarea for multiaddr input in example ([#24](https://github.com/libp2p/js-libp2p-webtransport/issues/24)) ([14ce351](https://github.com/libp2p/js-libp2p-webtransport/commit/14ce351375dabb31df948005b20acff56acc483a)) + +## [1.0.3](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.2...v1.0.3) (2022-10-18) + + +### Documentation + +* add fetch-file-from-kubo example ([#12](https://github.com/libp2p/js-libp2p-webtransport/issues/12)) ([4a8f2f3](https://github.com/libp2p/js-libp2p-webtransport/commit/4a8f2f3eb4fdede1510aa2808d4c9a30d7ae86bf)) + +## [1.0.2](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.1...v1.0.2) (2022-10-17) + + +### Bug Fixes + +* update project, remove @libp2p/components and unused deps ([#20](https://github.com/libp2p/js-libp2p-webtransport/issues/20)) ([568638e](https://github.com/libp2p/js-libp2p-webtransport/commit/568638e9fddc57726547e9147647af468a28bf51)) + +## [1.0.1](https://github.com/libp2p/js-libp2p-webtransport/compare/v1.0.0...v1.0.1) (2022-10-12) + + +### Dependencies + +* bump multiformats from 9.9.0 to 10.0.0 ([c3f7d22](https://github.com/libp2p/js-libp2p-webtransport/commit/c3f7d220969de6ec8a632738f760ab11388ef3e7)) +* **dev:** bump @libp2p/interface-transport-compliance-tests ([62c8e6b](https://github.com/libp2p/js-libp2p-webtransport/commit/62c8e6b3c18959d7416767d307a5ebaac8c19ae8)) +* **dev:** bump protons from 5.1.0 to 6.0.0 ([03f7f33](https://github.com/libp2p/js-libp2p-webtransport/commit/03f7f33ba5561771746f1f1cfff7421da36c5889)) + +## 1.0.0 (2022-10-12) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([cc208ac](https://github.com/libp2p/js-libp2p-webtransport/commit/cc208acc1c3459e5ec5f230927bfe5dc6e175f39)) +* use rc version of libp2p ([bdc9dfb](https://github.com/libp2p/js-libp2p-webtransport/commit/bdc9dfb63f9853a38bc3b6999a1f986bff116dda)) + + +### Dependencies + +* bump protons-runtime from 3.1.0 to 4.0.1 ([a7ef395](https://github.com/libp2p/js-libp2p-webtransport/commit/a7ef3959d024813caa327afdd502d5bcb91a15e3)) +* **dev:** bump @libp2p/interface-mocks from 4.0.3 to 7.0.1 ([85a492d](https://github.com/libp2p/js-libp2p-webtransport/commit/85a492da5b8df76d710dd21dd4b8bf59df4e1184)) +* **dev:** bump uint8arrays from 3.1.1 to 4.0.2 ([cb554e8](https://github.com/libp2p/js-libp2p-webtransport/commit/cb554e8dbb19a6ec5b085307f4c04c04ae313d2d)) diff --git a/packages/transport-webtransport/LICENSE b/packages/transport-webtransport/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-webtransport/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-webtransport/LICENSE-APACHE b/packages/transport-webtransport/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-webtransport/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-webtransport/LICENSE-MIT b/packages/transport-webtransport/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-webtransport/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/transport-webtransport/README.md b/packages/transport-webtransport/README.md new file mode 100644 index 0000000000..a7d41e4870 --- /dev/null +++ b/packages/transport-webtransport/README.md @@ -0,0 +1,93 @@ +# @libp2p/webtransport + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> JavaScript implementation of the WebTransport module that libp2p uses and that implements the interface-transport spec + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## Description + +`libp2p-webtransport` is the WebTransport transport implementation compatible with libp2p. + +## Usage + +```sh +> npm i @libp2p/webtransport +``` + +## Libp2p Usage Example + +```js +import { createLibp2pNode } from 'libp2p' +import { webTransport } from '@libp2p/webtransport' +import { noise } from 'libp2p-noise' + +const node = await createLibp2pNode({ + transports: [ + webTransport() + ], + connectionEncryption: [ + noise() + ] +}) +``` + +For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-transports](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#customizing-transports). + +## API + +### Transport + +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) + +### Connection + +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore b/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md b/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md new file mode 100644 index 0000000000..c02db34408 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/README.md @@ -0,0 +1,121 @@ +

      + + IPFS in JavaScript logo + +

      + +

      js-libp2p with WebTransport

      + +

      + js-libp2p using WebTransport! +
      +
      + +
      + Explore the docs + · + Report Bug + · + Request Feature/Example +

      + +## Table of Contents + +- [About The Project](#about-the-project) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation and Running example](#installation-and-running-example) +- [Usage](#usage) +- [References](#references) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Want to hack on IPFS?](#want-to-hack-on-ipfs) + +## About The Project + +- Read the [docs](https://github.com/ipfs/js-ipfs/tree/master/docs) +- Look into other [examples](https://github.com/ipfs-examples/js-ipfs-examples) to learn how to spawn an IPFS node in Node.js and in the Browser +- Consult the [Core API docs](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) to see what you can do with an IPFS node +- Visit https://dweb-primer.ipfs.io to learn about IPFS and the concepts that underpin it +- Head over to https://proto.school to take interactive tutorials that cover core IPFS APIs +- Check out https://docs.ipfs.io for tips, how-tos and more +- See https://blog.ipfs.io for news and more +- Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io + +## Getting Started + +### Prerequisites + +Make sure you have installed all of the following prerequisites on your development machine: + +- Git - [Download & Install Git](https://git-scm.com/downloads). OSX and Linux machines typically have this already installed. +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. + +### Installation and Running example + +**Pre-requisite**: Because this example is in a subfolder of @libp2p/webtransport, if you are running the example inside https://github.com/libp2p/js-libp2p-webtransport, you must build at the root first. If you are running the code outside of https://github.com/libp2p/js-libp2p-webtransport, you must run `npm install --save @libp2p/webtransport` first. + +```console +> npm install +> npm start +``` + +Now open your browser at `http://localhost:8888` + +## Usage + +In this example, you will find a boilerplate you can use to guide yourself into bundling js-ipfs with [browserify](http://browserify.org/), so that you can use it in your own web app! + +You should see the following: + +![](./img/img1.png) +![](./img/img2.png) + +This example demonstrates the `Regular API`, top-level API for add, cat, get and ls Files on IPFS + +_For more examples, please refer to the [Documentation](#documentation)_ + +## References + +- Documentation: + - [IPFS CONFIG](https://github.com/ipfs/js-ipfs/blob/master/docs/CONFIG.md) + - [MISCELLANEOUS](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/MISCELLANEOUS.md) + - [FILES](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md) +- Tutorials: + - [MFS API](https://proto.school/mutable-file-system) + - [Regular File API](https://proto.school/regular-files-api) + +## Documentation + +- [Config](https://docs.ipfs.io/) +- [Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) +- [Examples](https://github.com/ipfs-examples/js-ipfs-examples) +- [Development](https://github.com/ipfs/js-ipfs/blob/master/docs/DEVELOPMENT.md) +- [Tutorials](https://proto.school) + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the IPFS Project +2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing feature'`) +4. Push to the Branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Want to hack on IPFS? + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) + +The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out: + +Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md). + +- **Check out existing issues** The [issue list](https://github.com/ipfs/js-ipfs/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge +- **Look at the [IPFS Roadmap](https://github.com/ipfs/roadmap)** This are the high priority items being worked on right now +- **Perform code reviews** More eyes will help + a. speed the project along + b. ensure quality, and + c. reduce possible future bugs. +- **Add tests**. There can never be enough tests. +- **Join the [Weekly Core Implementations Call](https://github.com/ipfs/team-mgmt/issues/992)** it's where everyone discusses what's going on with IPFS and what's next diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png b/packages/transport-webtransport/examples/fetch-file-from-kubo/img/img1.png new file mode 100644 index 0000000000000000000000000000000000000000..6460e274466a53645168269943943ee195491457 GIT binary patch literal 571580 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

      3>-))F@-M~F(+loY{x$X2aZ(m_ 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

      ouA>`ftUm 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%0DHb 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

      w*!UMi7Dj1&6eu0Cx2lm8iA3}N; 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

      %_t@b8(B{5bVSZQ9$<#-e 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

      $%uG$i3`8K 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%0An_ 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


      FJU3l}6 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 pg0 zgQ&mrud)E2-#WRwpnN_)+$rS_B>Xy;7gkrqviTjX+*Z$(@;}ENP3j*S!!m3`k~fd3 ze;S0pWD`9j&g(WlFSr=bt6ieur6X$N*U8u{NL#nRlw`W~<9Bkj<8ChREv|+-={##D z+3>~1xxhSfC9ZbnTU7&0HB>I-sn?$H(&;v0g+cIHJs_W;3+ou39Qw0D7Oe?(emtrV z3QUH<7o|y0RLLmgE>Kn3pnH;PuaB3Jpgq2Gg*Wgpjj!5OQI})~c{^QR1&R-LGpc58 z_sdx>1$xigD~3vLC$zl1Eun+6j;NW?9hpTfYd>piq_B>J1SwD3ssOB%ef-K`3B-l8 zV8?|TADe4V_Pe`WuVpbYbB^O;=9j#$rJutj*ah{|?^vazaHQo|2)l4-l$H2;!d!f4 zrY44arrQe+q2O=6**?_Qp7Q$r_5Dk`xw^tB*jds4T5fwx-euF+zWAME zvreX17^O&NdV!~=uM>T@YFyq0IV>eSR_ne|f4ab~vI}=4$I>i4TW1Mx&Ea?6NFbDi z+X0X$YU)U^$INSs10dl>VoEs5d?{WHENfSXDcrQpH01Q6`+|=3-f9EYeo3y}Ts$Z( z_SFDilv7M538F>bLfKxrfqL|-{PsMWY~8&mT06XHT$Kp}`v17I*Y-aFz*ljg-(TWeZ0r&Z&#-Qz|L2ZlB%D+P7^`O02~uy)1$=+Uhi0xIqb_Zko( zqx3 zcNG}&C03t1fe{l_rbHNrKbrpejh7B-dUR|P12xi`q=Uc~V*p<=bb`OMga||MTApFL zutNFZU==9UWjXe??zUqQ(w7R!s#yjX7pvSanNR253UVQ_J0LAV*rQR>@#W?5SE^Kd z_;LXYF*T`)7Dc;r)(C} zYSWysOhCCj{z>dG%vVQ2Y05gNWKkLa$6ieQYQwY5^TMD5EJy<0PRonkB85QT$ETK6 z!EG5NwYw0TP%{#8mRd3HY2%yTON*)*pjO%COL6AsguX7^6{~a+ENkD*z$r($iMHj! zFxL6D9hAmFHll;K1oL&VWPtV?tJt0}1eKB6xCeQ}&bCvP$7Qfz*H#FczC>llF&lp= zwn9$H&s&P|f&zADV1|Z$QsWT`JKS#MqG>K-k;EPyo?*c6O-rR3ss8Cq8#*3=$|jDq zODomG6jsr*ATtFIf)@DWF-A7E)60?Ee6y_^>`fx4^eguXKh}&SpqJ%0+-d3CDWhw2 z!XvSiJ{H<5P5g&n!=*=06zdpnnt1qAXXRY&moGl-c40n2(dIr7zWF?E{fETcEOLcP zrkfa|Q~&wgJ4J)Hbj!WZn91gxcSPtWr~pLJ(84yPb|2qyjM)>*S2=jeDz+m2Zj5VB zVZLf`qja5bguR)Vxt(_2el?EGJmAD{ANc(Q=HrB?fI2yXbE+3>nF5aNR5-_Pd9R2s z$~A`n$pScf32ck}Fe3>Sm7`{o33Xy*?L5)AIQ*5@{?}ZuV4e`0_sd(K6y%|kD0fMDc zzui)65aIT}p=5nqBdYw0ZVmg}uSEvZHHsS5ytMLR5byHPm>_?o1a2k{RnqPg{Eqrh zWGF{3x9+5O%z&cz(nzE`w<%Z8+{~=Bly@?Vr$4ghAljUS<6xHb{Vck

      GtV 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 + + + + + + js-libp2p WebTransport + + + +

      + + + + \ No newline at end of file diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json b/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json new file mode 100644 index 0000000000..143eeb2ea6 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/package.json @@ -0,0 +1,26 @@ +{ + "name": "fetch-file-from-kubo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "npm run build && test-browser-example tests" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/webtransport": "../..", + "@multiformats/multiaddr": "^12.1.2", + "blockstore-core": "^4.1.0", + "ipfs-bitswap": "^18.0.1", + "libp2p": "^0.45.9", + "multiformats": "^11.0.2" + }, + "devDependencies": { + "test-ipfs-example": "^1.0.0", + "typescript": "^4.6.4", + "vite": "^3.1.0" + } +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts new file mode 100644 index 0000000000..3d97d38243 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/libp2p.ts @@ -0,0 +1,27 @@ +import { webTransport } from '@libp2p/webtransport' +import { noise } from '@chainsafe/libp2p-noise' +import { createLibp2p, Libp2p } from 'libp2p' +import { createBitswap } from 'ipfs-bitswap' +import { MemoryBlockstore } from 'blockstore-core/memory' + +type Bitswap = ReturnType + +export async function setup (): Promise<{ libp2p: Libp2p, bitswap: Bitswap }> { + const store = new MemoryBlockstore() + + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + // this is only necessary when dialing local addresses + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + const bitswap = createBitswap(node, store) + await bitswap.start() + + return { libp2p: node, bitswap } +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts new file mode 100644 index 0000000000..e2279bdf4c --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/main.ts @@ -0,0 +1,64 @@ +import './style.css' +import { multiaddr } from '@multiformats/multiaddr' +import { setup as libp2pSetup } from './libp2p' +import { CID } from 'multiformats/cid' + +localStorage.debug = '*' + +declare global { + interface Window { + fetchBtn: HTMLButtonElement + connectBtn: HTMLButtonElement + peerInput: HTMLInputElement + cidInput: HTMLInputElement + statusEl: HTMLParagraphElement + downloadEl: HTMLAnchorElement + downloadCidWrapperEl: HTMLDivElement + connlistWrapperEl: HTMLDivElement + connlistEl: HTMLUListElement + } +} + +(async function () { + const { libp2p, bitswap } = await libp2pSetup() + window.connectBtn.onclick = async () => { + const ma = multiaddr(window.peerInput.value) + await libp2p.dial(ma) + } + + libp2p.addEventListener('peer:connect', (_connection) => { + updateConnList() + }) + libp2p.addEventListener('peer:disconnect', (_connection) => { + updateConnList() + }) + + function updateConnList () { + const addrs = libp2p.getConnections().map(c => c.remoteAddr.toString()) + if (addrs.length > 0) { + window.downloadCidWrapperEl.hidden = false + window.connlistWrapperEl.hidden = false + window.connlistEl.innerHTML = '' + addrs.forEach(a => { + const li = document.createElement('li') + li.innerText = a + window.connlistEl.appendChild(li) + }) + } else { + window.downloadCidWrapperEl.hidden = true + window.connlistWrapperEl.hidden = true + window.connlistEl.innerHTML = '' + } + } + + window.fetchBtn.onclick = async () => { + const c = CID.parse(window.cidInput.value) + window.statusEl.hidden = false + const val = await bitswap.want(c) + window.statusEl.hidden = true + + window.downloadEl.href = window.URL.createObjectURL(new Blob([val], { type: 'bytes' })) + window.downloadEl.hidden = false + } +// eslint-disable-next-line no-console +})().catch(err => console.error(err)) diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css new file mode 100644 index 0000000000..072f654118 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/style.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #3178c6aa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + +#connlistWrapperEl ul { + max-width: 400px; + overflow-x: auto; +} \ No newline at end of file diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js b/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js new file mode 100644 index 0000000000..edebee8011 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/tests/test.spec.js @@ -0,0 +1,76 @@ +/* eslint-disable no-console */ +import { setup, expect } from 'test-ipfs-example/browser' +import { spawn, exec } from 'child_process' +import { existsSync } from 'fs' + +// Setup +const test = setup() + +async function spinUpGoLibp2p() { + if (!existsSync('../../go-libp2p-webtransport-server/main')) { + await new Promise((resolve, reject) => { + exec('go build -o main main.go', + { cwd: '../../go-libp2p-webtransport-server' }, + (error, stdout, stderr) => { + if (error) { + reject(error) + console.error(`exec error: ${error}`) + return + } + resolve() + }) + }) + } + + const server = spawn('./main', [], { cwd: '../../go-libp2p-webtransport-server', killSignal: 'SIGINT' }) + server.stderr.on('data', (data) => { + console.log(`stderr: ${data}`, typeof data) + }) + const serverAddr = await (new Promise(resolve => { + server.stdout.on('data', (data) => { + console.log(`stdout: ${data}`, typeof data) + if (data.includes('addr=')) { + // Parse the addr out + resolve((String(data)).match(/addr=([^\s]*)/)[1]) + } + }) + })) + return { server, serverAddr } +} + +test.describe('bundle ipfs with parceljs:', () => { + // DOM + const connectBtn = '#connectBtn' + const connectAddr = '#peerInput' + const connList = '#connlistEl' + + let server + let serverAddr + + // eslint-disable-next-line no-empty-pattern + test.beforeAll(async ({ }, testInfo) => { + testInfo.setTimeout(5 * 60_000) + const s = await spinUpGoLibp2p() + server = s.server + serverAddr = s.serverAddr + console.log('Server addr:', serverAddr) + }, {}) + + test.afterAll(() => { + server.kill('SIGINT') + }) + + test.beforeEach(async ({ servers, page }) => { + await page.goto(servers[0].url) + }) + + test('should connect to a go-libp2p node over webtransport', async ({ page }) => { + await page.fill(connectAddr, serverAddr) + await page.click(connectBtn) + + await page.waitForSelector('#connlistEl:has(li)') + + const connections = await page.textContent(connList) + expect(connections).toContain(serverAddr) + }) +}) diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json b/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json new file mode 100644 index 0000000000..fbd022532d --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js b/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js new file mode 100644 index 0000000000..bcbaff69d7 --- /dev/null +++ b/packages/transport-webtransport/examples/fetch-file-from-kubo/vite.config.js @@ -0,0 +1,8 @@ +export default { + build: { + target: 'es2020' + }, + optimizeDeps: { + esbuildOptions: { target: 'es2020', supported: { bigint: true } } + } +} diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod b/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod new file mode 100644 index 0000000000..e74c5bfb4a --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/go.mod @@ -0,0 +1,95 @@ +module github.com/libp2p/js-libp2p-webtransport/go-libp2p-webtransport-server/m/v2 + +go 1.19 + +require ( + github.com/libp2p/go-libp2p v0.27.1 + github.com/multiformats/go-multiaddr v0.9.0 +) + +require ( + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.2 // indirect + github.com/flynn/noise v1.0.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/huin/goupnp v1.1.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.16.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.1.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-reuseport v0.2.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.53 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.0 // 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-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.8.1 // indirect + github.com/multiformats/go-multihash v0.2.1 // indirect + github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/onsi/ginkgo/v2 v2.9.2 // indirect + github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.2 // indirect + github.com/quic-go/qtls-go1-20 v0.2.2 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + github.com/quic-go/webtransport-go v0.5.2 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/dig v1.16.1 // indirect + go.uber.org/fx v1.19.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + lukechampine.com/blake3 v1.1.7 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum b/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum new file mode 100644 index 0000000000..8b98f347e4 --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/go.sum @@ -0,0 +1,501 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= +github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +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.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +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-20230405160723-4a4c7d95572b h1:Qcx5LM0fSiks9uCyFZwDBUasd3lxd1RM0GYpL+Li5o4= +github.com/google/pprof v0.0.0-20230405160723-4a4c7d95572b/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +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= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= +github.com/huin/goupnp v1.1.0 h1:gEe0Dp/lZmPZiDFzJJaOfUpOvv2MKUkoBX8lDrn9vKU= +github.com/huin/goupnp v1.1.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= +github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.27.1 h1:k1u6RHsX3hqKnslDjsSgLNURxJ3O1atIZCY4gpMbbus= +github.com/libp2p/go-libp2p v0.27.1/go.mod h1:FAvvfQa/YOShUYdiSS03IR9OXzkcJXwcNA2FUCh9ImE= +github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +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-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= +github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k= +github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ= +github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= +github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= +github.com/multiformats/go-multiaddr v0.9.0 h1:3h4V1LHIk5w4hJHekMKWALPXErDfz/sggzwC/NcqbDQ= +github.com/multiformats/go-multiaddr v0.9.0/go.mod h1:mI67Lb1EeTOYb8GQfL/7wpIZwc46ElrvzhYnoJOmTT0= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +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.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8= +github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= +github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +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/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.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk= +github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= +go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= +go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= +go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +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.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +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= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +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= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +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.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= +lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +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= diff --git a/packages/transport-webtransport/go-libp2p-webtransport-server/main.go b/packages/transport-webtransport/go-libp2p-webtransport-server/main.go new file mode 100644 index 0000000000..6a388e2ab0 --- /dev/null +++ b/packages/transport-webtransport/go-libp2p-webtransport-server/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "os" + "os/signal" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/network" + webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" + "github.com/multiformats/go-multiaddr" +) + +func main() { + h, err := libp2p.New(libp2p.Transport(webtransport.New)) + if err != nil { + panic(err) + } + + err = h.Network().Listen(multiaddr.StringCast("/ip4/127.0.0.1/udp/0/quic-v1/webtransport")) + if err != nil { + panic(err) + } + + err = h.Network().Listen(multiaddr.StringCast("/ip6/::1/udp/0/quic-v1/webtransport")) + if err != nil { + panic(err) + } + + h.SetStreamHandler("echo", func(s network.Stream) { + io.Copy(s, s) + s.Close() + }) + + for _, a := range h.Addrs() { + withP2p := a.Encapsulate(multiaddr.StringCast("/p2p/" + h.ID().String())) + fmt.Printf("addr=%s\n", withP2p.String()) + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + select { + case <-c: + case <-time.After(time.Minute): + } +} diff --git a/packages/transport-webtransport/package.json b/packages/transport-webtransport/package.json new file mode 100644 index 0000000000..9c8c6300ac --- /dev/null +++ b/packages/transport-webtransport/package.json @@ -0,0 +1,90 @@ +{ + "name": "@libp2p/webtransport", + "version": "2.0.2", + "description": "JavaScript implementation of the WebTransport module that libp2p uses and that implements the interface-transport spec", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webtransport#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "keywords": [ + "IPFS" + ], + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./filters": { + "types": "./dist/src/filters.d.ts", + "import": "./dist/src/filters.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test -t browser -t webworker", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker" + }, + "dependencies": { + "@chainsafe/libp2p-noise": "^12.0.1", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-stream-muxer": "^4.0.0", + "@libp2p/interface-transport": "^4.0.0", + "@libp2p/logger": "^2.0.0", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "it-stream-types": "^2.0.1", + "multiformats": "^11.0.2", + "uint8arraylist": "^2.4.3" + }, + "devDependencies": { + "aegir": "^39.0.10", + "libp2p": "^0.45.0", + "p-defer": "^4.0.0" + }, + "browser": { + "./dist/src/listener.js": "./dist/src/listener.browser.js" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/transport-webtransport/src/index.ts b/packages/transport-webtransport/src/index.ts new file mode 100644 index 0000000000..d10b5b559d --- /dev/null +++ b/packages/transport-webtransport/src/index.ts @@ -0,0 +1,503 @@ +import { noise } from '@chainsafe/libp2p-noise' +import { type Transport, symbol, type CreateListenerOptions, type DialOptions, type Listener } from '@libp2p/interface-transport' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { type Multiaddr, protocols } from '@multiformats/multiaddr' +import { bases, digest } from 'multiformats/basics' +import { Uint8ArrayList } from 'uint8arraylist' +import type { Connection, Direction, MultiaddrConnection, Stream } from '@libp2p/interface-connection' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface-stream-muxer' +import type { Duplex, Source } from 'it-stream-types' +import type { MultihashDigest } from 'multiformats/hashes/interface' + +declare global { + var WebTransport: any +} + +const log = logger('libp2p:webtransport') + +// @ts-expect-error - Not easy to combine these types. +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b)) + +function decodeCerthashStr (s: string): MultihashDigest { + return digest.decode(multibaseDecoder.decode(s)) +} + +// Duplex that does nothing. Needed to fulfill the interface +function inertDuplex (): Duplex { + return { + source: { + [Symbol.asyncIterator] () { + return { + async next () { + // This will never resolve + return new Promise(() => { }) + } + } + } + }, + sink: async (source: Source) => { + // This will never resolve + return new Promise(() => { }) + } + } +} + +async function webtransportBiDiStreamToStream (bidiStream: any, streamId: string, direction: Direction, activeStreams: Stream[], onStreamEnd: undefined | ((s: Stream) => void)): Promise { + const writer = bidiStream.writable.getWriter() + const reader = bidiStream.readable.getReader() + await writer.ready + + function cleanupStreamFromActiveStreams (): void { + const index = activeStreams.findIndex(s => s === stream) + if (index !== -1) { + activeStreams.splice(index, 1) + stream.stat.timeline.close = Date.now() + onStreamEnd?.(stream) + } + } + + let writerClosed = false + let readerClosed = false; + (async function () { + const err: Error | undefined = await writer.closed.catch((err: Error) => err) + if (err != null) { + const msg = err.message + if (!(msg.includes('aborted by the remote server') || msg.includes('STOP_SENDING'))) { + log.error(`WebTransport writer closed unexpectedly: streamId=${streamId} err=${err.message}`) + } + } + writerClosed = true + if (writerClosed && readerClosed) { + cleanupStreamFromActiveStreams() + } + })().catch(() => { + log.error('WebTransport failed to cleanup closed stream') + }); + + (async function () { + const err: Error | undefined = await reader.closed.catch((err: Error) => err) + if (err != null) { + log.error(`WebTransport reader closed unexpectedly: streamId=${streamId} err=${err.message}`) + } + readerClosed = true + if (writerClosed && readerClosed) { + cleanupStreamFromActiveStreams() + } + })().catch(() => { + log.error('WebTransport failed to cleanup closed stream') + }) + + let sinkSunk = false + const stream: Stream = { + id: streamId, + abort (_err: Error) { + if (!writerClosed) { + writer.abort() + writerClosed = true + } + stream.closeRead() + readerClosed = true + cleanupStreamFromActiveStreams() + }, + close () { + stream.closeRead() + stream.closeWrite() + cleanupStreamFromActiveStreams() + }, + + closeRead () { + if (!readerClosed) { + reader.cancel().catch((err: any) => { + if (err.toString().includes('RESET_STREAM') === true) { + writerClosed = true + } + }) + readerClosed = true + } + if (writerClosed) { + cleanupStreamFromActiveStreams() + } + }, + closeWrite () { + if (!writerClosed) { + writerClosed = true + writer.close().catch((err: any) => { + if (err.toString().includes('RESET_STREAM') === true) { + readerClosed = true + } + }) + } + if (readerClosed) { + cleanupStreamFromActiveStreams() + } + }, + reset () { + stream.close() + }, + stat: { + direction, + timeline: { open: Date.now() } + }, + metadata: {}, + source: (async function * () { + while (true) { + const val = await reader.read() + if (val.done === true) { + readerClosed = true + if (writerClosed) { + cleanupStreamFromActiveStreams() + } + return + } + + yield new Uint8ArrayList(val.value) + } + })(), + sink: async function (source: Source) { + if (sinkSunk) { + throw new Error('sink already called on stream') + } + sinkSunk = true + try { + for await (const chunks of source) { + if (chunks instanceof Uint8Array) { + await writer.write(chunks) + } else { + for (const buf of chunks) { + await writer.write(buf) + } + } + } + } finally { + stream.closeWrite() + } + } + } + + return stream +} + +function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } { + const parts = ma.stringTuples() + + // This is simpler to have inline than extract into a separate function + // eslint-disable-next-line complexity + const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => { + switch (proto) { + case protocols('ip6').code: + // @ts-expect-error - ts error on switch fallthrough + case protocols('dns6').code: + if (value?.includes(':') === true) { + /** + * This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed. + * + * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL` + * `new URL('https://[::1]:4001/blah')` is valid and will not. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + */ + value = `[${value}]` + } + // eslint-disable-next-line no-fallthrough + case protocols('ip4').code: + case protocols('dns4').code: + if (state.seenHost || state.seenPort) { + throw new Error('Invalid multiaddr, saw host and already saw the host or port') + } + return { + ...state, + url: `${state.url}${value ?? ''}`, + seenHost: true + } + case protocols('quic').code: + case protocols('quic-v1').code: + case protocols('webtransport').code: + if (!state.seenHost || !state.seenPort) { + throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport") + } + return state + case protocols('udp').code: + if (state.seenPort) { + throw new Error('Invalid multiaddr, saw port but already saw the port') + } + return { + ...state, + url: `${state.url}:${value ?? ''}`, + seenPort: true + } + case protocols('certhash').code: + if (!state.seenHost || !state.seenPort) { + throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port') + } + return { + ...state, + certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')]) + } + case protocols('p2p').code: + return { + ...state, + remotePeer: peerIdFromString(value ?? '') + } + default: + throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `) + } + }, + // All webtransport urls are https + { url: 'https://', seenHost: false, seenPort: false, certhashes: [] }) + + return { url, certhashes, remotePeer } +} + +// Determines if `maybeSubset` is a subset of `set`. This means that all byte arrays in `maybeSubset` are present in `set`. +export function isSubset (set: Uint8Array[], maybeSubset: Uint8Array[]): boolean { + const intersection = maybeSubset.filter(byteArray => { + return Boolean(set.find((otherByteArray: Uint8Array) => { + if (byteArray.length !== otherByteArray.length) { + return false + } + + for (let index = 0; index < byteArray.length; index++) { + if (otherByteArray[index] !== byteArray[index]) { + return false + } + } + return true + })) + }) + return (intersection.length === maybeSubset.length) +} + +export interface WebTransportInit { + maxInboundStreams?: number +} + +export interface WebTransportComponents { + peerId: PeerId +} + +class WebTransportTransport implements Transport { + private readonly components: WebTransportComponents + private readonly config: Required + + constructor (components: WebTransportComponents, init: WebTransportInit = {}) { + this.components = components + this.config = { + maxInboundStreams: init.maxInboundStreams ?? 1000 + } + } + + readonly [Symbol.toStringTag] = '@libp2p/webtransport' + + readonly [symbol] = true + + async dial (ma: Multiaddr, options: DialOptions): Promise { + log('dialing %s', ma) + const localPeer = this.components.peerId + if (localPeer === undefined) { + throw new Error('Need a local peerid') + } + + options = options ?? {} + + const { url, certhashes, remotePeer } = parseMultiaddr(ma) + + if (certhashes.length === 0) { + throw new Error('Expected multiaddr to contain certhashes') + } + + const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, { + serverCertificateHashes: certhashes.map(certhash => ({ + algorithm: 'sha-256', + value: certhash.digest + })) + }) + wt.closed.catch((error: Error) => { + log.error('WebTransport transport closed due to:', error) + }) + await wt.ready + + if (remotePeer == null) { + throw new Error('Need a target peerid') + } + + if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) { + throw new Error('Failed to authenticate webtransport') + } + + const maConn: MultiaddrConnection = { + close: async (err?: Error) => { + if (err != null) { + log('Closing webtransport with err:', err) + } + wt.close() + }, + remoteAddr: ma, + timeline: { + open: Date.now() + }, + // This connection is never used directly since webtransport supports native streams. + ...inertDuplex() + } + + wt.closed.catch((err: Error) => { + log.error('WebTransport connection closed:', err) + // This is how we specify the connection is closed and shouldn't be used. + maConn.timeline.close = Date.now() + }) + + try { + options?.signal?.throwIfAborted() + } catch (e) { + wt.close() + throw e + } + + return options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt), skipProtection: true }) + } + + async authenticateWebTransport (wt: InstanceType, localPeer: PeerId, remotePeer: PeerId, certhashes: Array>): Promise { + const stream = await wt.createBidirectionalStream() + const writer = stream.writable.getWriter() + const reader = stream.readable.getReader() + await writer.ready + + const duplex = { + source: (async function * () { + while (true) { + const val = await reader.read() + + if (val.value != null) { + yield val.value + } + + if (val.done === true) { + break + } + } + })(), + sink: async function (source: Source) { + for await (const chunk of source) { + await writer.write(chunk) + } + } + } + + const n = noise()() + + const { remoteExtensions } = await n.secureOutbound(localPeer, duplex, remotePeer) + + // We're done with this authentication stream + writer.close().catch((err: Error) => { + log.error(`Failed to close authentication stream writer: ${err.message}`) + }) + + reader.cancel().catch((err: Error) => { + log.error(`Failed to close authentication stream reader: ${err.message}`) + }) + + // Verify the certhashes we used when dialing are a subset of the certhashes relayed by the remote peer + if (!isSubset(remoteExtensions?.webtransportCerthashes ?? [], certhashes.map(ch => ch.bytes))) { + throw new Error("Our certhashes are not a subset of the remote's reported certhashes") + } + + return true + } + + webtransportMuxer (wt: InstanceType): StreamMuxerFactory { + let streamIDCounter = 0 + const config = this.config + return { + protocol: 'webtransport', + createStreamMuxer: (init?: StreamMuxerInit): StreamMuxer => { + // !TODO handle abort signal when WebTransport supports this. + + if (typeof init === 'function') { + // The api docs say that init may be a function + init = { onIncomingStream: init } + } + + const activeStreams: Stream[] = []; + + (async function () { + //! TODO unclear how to add backpressure here? + + const reader = wt.incomingBidirectionalStreams.getReader() + while (true) { + const { done, value: wtStream } = await reader.read() + + if (done === true) { + break + } + + if (activeStreams.length >= config.maxInboundStreams) { + // We've reached our limit, close this stream. + wtStream.writable.close().catch((err: Error) => { + log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`) + }) + wtStream.readable.cancel().catch((err: Error) => { + log.error(`Failed to close inbound stream that crossed our maxInboundStream limit: ${err.message}`) + }) + } else { + const stream = await webtransportBiDiStreamToStream(wtStream, String(streamIDCounter++), 'inbound', activeStreams, init?.onStreamEnd) + activeStreams.push(stream) + init?.onIncomingStream?.(stream) + } + } + })().catch(() => { + log.error('WebTransport failed to receive incoming stream') + }) + + const muxer: StreamMuxer = { + protocol: 'webtransport', + streams: activeStreams, + newStream: async (name?: string): Promise => { + const wtStream = await wt.createBidirectionalStream() + + const stream = await webtransportBiDiStreamToStream(wtStream, String(streamIDCounter++), init?.direction ?? 'outbound', activeStreams, init?.onStreamEnd) + activeStreams.push(stream) + + return stream + }, + + /** + * Close or abort all tracked streams and stop the muxer + */ + close: (err?: Error) => { + if (err != null) { + log('Closing webtransport muxer with err:', err) + } + wt.close() + }, + // This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex. + ...inertDuplex() + } + + try { + init?.signal?.throwIfAborted() + } catch (e) { + wt.close() + throw e + } + + return muxer + } + } + } + + createListener (options: CreateListenerOptions): Listener { + throw new Error('Webtransport servers are not supported in Node or the browser') + } + + /** + * Takes a list of `Multiaddr`s and returns only valid webtransport addresses. + */ + filter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(ma => ma.protoNames().includes('webtransport')) + } +} + +export function webTransport (init: WebTransportInit = {}): (components: WebTransportComponents) => Transport { + return (components: WebTransportComponents) => new WebTransportTransport(components, init) +} diff --git a/packages/transport-webtransport/test/browser.ts b/packages/transport-webtransport/test/browser.ts new file mode 100644 index 0000000000..153743e62b --- /dev/null +++ b/packages/transport-webtransport/test/browser.ts @@ -0,0 +1,189 @@ +/* eslint-disable no-console */ +/* eslint-env mocha */ + +import { noise } from '@chainsafe/libp2p-noise' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import { webTransport, isSubset } from '../src/index' + +declare global { + interface Window { + WebTransport: any + } +} + +describe('libp2p-webtransport', () => { + it('webtransport connects to go-libp2p', async () => { + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const ma = multiaddr(maStr) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + // Ping many times + for (let index = 0; index < 100; index++) { + const now = Date.now() + + // Note we're re-implementing the ping protocol here because as of this + // writing, go-libp2p will reset the stream instead of close it. The next + // version of go-libp2p v0.24.0 will have this fix. When that's released + // we can use the builtin ping system + const stream = await node.dialProtocol(ma, '/ipfs/ping/1.0.0') + + const data = new Uint8Array(32) + globalThis.crypto.getRandomValues(data) + + const pong = new Promise((resolve, reject) => { + (async () => { + for await (const chunk of stream.source) { + const v = chunk.subarray() + const byteMatches: boolean = v.every((byte: number, i: number) => byte === data[i]) + if (byteMatches) { + resolve() + } else { + reject(new Error('Wrong pong')) + } + } + })().catch(reject) + }) + + let res = -1 + await stream.sink((async function * () { + yield data + // Wait for the pong before we close the write side + await pong + res = Date.now() - now + })()) + + stream.close() + + expect(res).to.be.greaterThan(-1) + } + + await node.stop() + const conns = node.getConnections() + expect(conns.length).to.equal(0) + }) + + it('fails to connect without certhashes', async () => { + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const maStrNoCerthash: string = maStr.split('/certhash')[0] + const maStrP2p = maStr.split('/p2p/')[1] + const ma = multiaddr(maStrNoCerthash + '/p2p/' + maStrP2p) + + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + await node.start() + + const err = await expect(node.dial(ma)).to.eventually.be.rejected() + expect(err.toString()).to.contain('Expected multiaddr to contain certhashes') + + await node.stop() + }) + + it('connects to ipv6 addresses', async () => { + if (process.env.serverAddr6 == null) { + throw new Error('serverAddr6 not found') + } + + const ma = multiaddr(process.env.serverAddr6) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + await node.start() + + // the address is unreachable but we can parse it correctly + const stream = await node.dialProtocol(ma, '/ipfs/ping/1.0.0') + stream.close() + + await node.stop() + }) + + it('Closes writes of streams after they have sunk a source', async () => { + // This is the behavior of stream muxers: (see mplex, yamux and compliance tests: https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-stream-muxer-compliance-tests/src/close-test.ts) + if (process.env.serverAddr == null) { + throw new Error('serverAddr not found') + } + + const maStr: string = process.env.serverAddr + const ma = multiaddr(maStr) + const node = await createLibp2p({ + transports: [webTransport()], + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: async () => false + } + }) + + async function * gen (): AsyncGenerator { + yield new Uint8Array([0]) + yield new Uint8Array([1, 2, 3, 4]) + yield new Uint8Array([5, 6, 7]) + yield new Uint8Array([8, 9, 10, 11]) + yield new Uint8Array([12, 13, 14, 15]) + } + + await node.start() + const stream = await node.dialProtocol(ma, 'echo') + + await stream.sink(gen()) + + let expectedNextNumber = 0 + for await (const chunk of stream.source) { + for (const byte of chunk.subarray()) { + expect(byte).to.equal(expectedNextNumber++) + } + } + expect(expectedNextNumber).to.equal(16) + + // Close read, we've should have closed the write side during sink + stream.closeRead() + + expect(stream.stat.timeline.close).to.be.greaterThan(0) + + await node.stop() + }) +}) + +describe('test helpers', () => { + it('correctly checks subsets', () => { + const testCases = [ + { a: [[1, 2, 3]], b: [[4, 5, 6]], isSubset: false }, + { a: [[1, 2, 3], [4, 5, 6]], b: [[1, 2, 3]], isSubset: true }, + { a: [[1, 2, 3], [4, 5, 6]], b: [], isSubset: true }, + { a: [], b: [[1, 2, 3]], isSubset: false }, + { a: [], b: [], isSubset: true }, + { a: [[1, 2, 3]], b: [[1, 2, 3], [4, 5, 6]], isSubset: false }, + { a: [[1, 2, 3]], b: [[1, 2]], isSubset: false } + ] + + for (const tc of testCases) { + expect(isSubset(tc.a.map(b => new Uint8Array(b)), tc.b.map(b => new Uint8Array(b)))).to.equal(tc.isSubset) + } + }) +}) diff --git a/packages/transport-webtransport/tsconfig.json b/packages/transport-webtransport/tsconfig.json new file mode 100644 index 0000000000..4f3f92ea0b --- /dev/null +++ b/packages/transport-webtransport/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-peer-id" + }, + { + "path": "../interface-stream-muxer" + }, + { + "path": "../interface-transport" + }, + { + "path": "../libp2p" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/utils/API.md b/packages/utils/API.md new file mode 100644 index 0000000000..3dbf66d3e6 --- /dev/null +++ b/packages/utils/API.md @@ -0,0 +1,209 @@ +# API + +* [addressSort.publicAddressesFirst(addresses)](#addresssortpublicaddressesfirstaddresses) + * [Parameters](#parameters) + * [Returns](#returns) + * [Example](#example) +* [arrayEquals(a, b)](#arrayequalsa-b) + * [Parameters](#parameters) + * [Returns](#returns) + * [Example](#example) +* [multiaddr .isLoopback(ma)](#multiaddr-isloopbackma) + * [Parameters](#parameters-1) + * [Returns](#returns-1) + * [Example](#example-1) +* [multiaddr .isPrivate(ma)](#multiaddr-isprivatema) + * [Parameters](#parameters-2) + * [Returns](#returns-2) + * [Example](#example-2) +* [ipPortToMultiaddr(ip, port)](#ipporttomultiaddrip-port) + * [Parameters](#parameters-3) + * [Returns](#returns-3) + * [Example](#example-3) +* [streamToMaConnection(streamProperties, options)](#streamtomaconnectionstreamproperties-options) + * [Parameters](#parameters-4) + * [Returns](#returns-4) + * [Example](#example-4) + +## addressSort.publicAddressesFirst(addresses) + +Sort given addresses by putting public addresses first. In case of equality, a certified address will come first. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| addresses | `Array
      ` | Array of AddressBook addresses | + +### Returns + +| Type | Description | +|------|-------------| +| `Array
      ` | returns array of sorted addresses | + +### Example + +```js +const multiaddr = require('multiaddr') +const { publicAddressesFirst } = require('libp2p-utils/src/address-sort') + +const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + } +] + +const sortedAddresses = publicAddressesFirst(addresses) +``` + +## arrayEquals(a, b) + +Verify if two arrays of non primitive types with the "equals" function are equal. +Compatible with multiaddr, peer-id and others. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| a | `Array<*>` | First array to verify | +| b | `Array<*>` | Second array to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if arrays are equal, false otherwise | + +### Example + +```js +const PeerId = require('peer-id') +const arrayEquals = require('libp2p-utils/src/array-equals') + +const peerId1 = await PeerId.create() +const peerId2 = await PeerId.create() + +const equals = arrayEquals([peerId1], [peerId2]) +``` + +## multiaddr `.isLoopback(ma)` + +Check if a given multiaddr is a loopback address. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ma | `Multiaddr` | multiaddr to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if multiaddr is a loopback address, false otherwise | + +### Example + +```js +const multiaddr = require('multiaddr') +const isLoopback = require('libp2p-utils/src/multiaddr/is-loopback') + +const ma = multiaddr('/ip4/127.0.0.1/tcp/1000') +isMultiaddrLoopbackAddrs = isLoopback(ma) +``` + +## multiaddr `.isPrivate(ma)` + +Check if a given multiaddr has a private address. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ma | `Multiaddr` | multiaddr to verify | + +### Returns + +| Type | Description | +|------|-------------| +| `boolean` | returns true if multiaddr is a private address, false otherwise | + +### Example + +```js +const multiaddr = require('multiaddr') +const isPrivate = require('libp2p-utils/src/multiaddr/is-private') + +const ma = multiaddr('/ip4/10.0.0.1/tcp/1000') +isMultiaddrPrivateAddrs = isPrivate(ma) +``` + +## ipPortToMultiaddr(ip, port) + +Transform an IP, Port pair into a multiaddr with tcp transport. + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| ip | `string` | ip for multiaddr | +| port | `number|string` | port for multiaddr | + +### Returns + +| Type | Description | +|------|-------------| +| `Multiaddr` | returns created multiaddr | + +### Example + +```js +const ipPortPairToMultiaddr = require('libp2p-utils/src/multiaddr/ip-port-to-multiaddr') +const ip = '127.0.0.1' +const port = '9090' + +const ma = ipPortPairToMultiaddr(ma) +``` + +## streamToMaConnection(streamProperties, options) + +Convert a duplex stream into a [MultiaddrConnection](https://github.com/libp2p/interface-transport#multiaddrconnection). + +### Parameters + +| Name | Type | Description | +|------|------|-------------| +| streamProperties | `object` | duplex stream properties | +| streamProperties.stream | [`DuplexStream`](https://github.com/libp2p/js-libp2p/blob/master/doc/STREAMING_ITERABLES.md#duplex) | duplex stream | +| streamProperties.remoteAddr | `Multiaddr` | stream remote address | +| streamProperties.localAddr | `Multiaddr` | stream local address | +| [options] | `object` | options | +| [options.signal] | `AbortSignal` | abort signal | + +### Returns + +| Type | Description | +|------|-------------| +| `Connection` | returns a multiaddr [Connection](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/connection) | + +### Example + +```js +const streamToMaConnection = require('libp2p-utils/src/stream-to-ma-conn') + +const stream = { + sink: async source => {/* ... */}, + source: { [Symbol.asyncIterator] () {/* ... */} } +} + +const conn = streamToMaConnection({ + stream, + remoteAddr: /* ... */ + localAddr; /* ... */ +}) +``` diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md new file mode 100644 index 0000000000..8ed268837a --- /dev/null +++ b/packages/utils/CHANGELOG.md @@ -0,0 +1,324 @@ +## [3.0.12](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.11...v3.0.12) (2023-06-15) + + +### Trivial Changes + +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([2c91adc](https://github.com/libp2p/js-libp2p-utils/commit/2c91adc7e17fadd9f96c0fc222fb5557df037459)) +* Update .github/workflows/stale.yml [skip ci] ([e5fbee9](https://github.com/libp2p/js-libp2p-utils/commit/e5fbee99f549ad708dd375516356c5b20915cf87)) + + +### Dependencies + +* **dev:** bump aegir from 38.1.8 to 39.0.10 ([#100](https://github.com/libp2p/js-libp2p-utils/issues/100)) ([da6547c](https://github.com/libp2p/js-libp2p-utils/commit/da6547cdd073ba1a4225be5a419c6776c4ebe6f1)) + +## [3.0.11](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.10...v3.0.11) (2023-04-24) + + +### Dependencies + +* bump @libp2p/interface-peer-store from 1.2.9 to 2.0.0 ([#91](https://github.com/libp2p/js-libp2p-utils/issues/91)) ([c7569d7](https://github.com/libp2p/js-libp2p-utils/commit/c7569d77a56d5fc3a5323c89ba93230206c35d2b)) + +## [3.0.10](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.9...v3.0.10) (2023-04-18) + + +### Dependencies + +* bump it-stream-types from 1.0.5 to 2.0.1 ([#89](https://github.com/libp2p/js-libp2p-utils/issues/89)) ([0de4a85](https://github.com/libp2p/js-libp2p-utils/commit/0de4a85bd6caa3dfec673ceb3be9130d4051e407)) + +## [3.0.9](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.8...v3.0.9) (2023-04-18) + + +### Dependencies + +* **dev:** bump it-all from 2.0.1 to 3.0.1 ([#85](https://github.com/libp2p/js-libp2p-utils/issues/85)) ([b029517](https://github.com/libp2p/js-libp2p-utils/commit/b0295176c2c6553209ebb26149497a5f9a73fc9e)) +* **dev:** bump it-map from 2.0.1 to 3.0.2 ([#88](https://github.com/libp2p/js-libp2p-utils/issues/88)) ([6e24d5a](https://github.com/libp2p/js-libp2p-utils/commit/6e24d5a91b4df40c7a395d3d3c9379120de0754d)) + +## [3.0.8](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.7...v3.0.8) (2023-04-12) + + +### Dependencies + +* bump @libp2p/interface-connection from 3.1.1 to 4.0.0 ([#90](https://github.com/libp2p/js-libp2p-utils/issues/90)) ([d140507](https://github.com/libp2p/js-libp2p-utils/commit/d140507f1d4263886c515f4877425a01f28b88e7)) + +## [3.0.7](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.6...v3.0.7) (2023-03-31) + + +### Dependencies + +* **dev:** bump it-pipe from 2.0.5 to 3.0.0 ([#87](https://github.com/libp2p/js-libp2p-utils/issues/87)) ([fc28634](https://github.com/libp2p/js-libp2p-utils/commit/fc286345ff55e23b7619da2bdcedfd848d7c1f85)) + +## [3.0.6](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.5...v3.0.6) (2023-03-27) + + +### Bug Fixes + +* handle non ip4/ip6/dns addresses in isPrivate ([#84](https://github.com/libp2p/js-libp2p-utils/issues/84)) ([af2c222](https://github.com/libp2p/js-libp2p-utils/commit/af2c2221ad175a06f758a45fc71fbb2f870eece4)) + +## [3.0.5](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.4...v3.0.5) (2023-03-17) + + +### Trivial Changes + +* replace err-code with CodeError ([#70](https://github.com/libp2p/js-libp2p-utils/issues/70)) ([beb252d](https://github.com/libp2p/js-libp2p-utils/commit/beb252d79f69d0f49d1fa4fd664a49e33ff80cd3)), closes [js-libp2p#1269](https://github.com/libp2p/js-libp2p/issues/1269) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([acad1fe](https://github.com/libp2p/js-libp2p-utils/commit/acad1fe38a1cfef19f63de7283e721caec059d34)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([1b96837](https://github.com/libp2p/js-libp2p-utils/commit/1b96837cac6c9625ed243d0f62595582a57f7f04)) +* Update .github/workflows/semantic-pull-request.yml [skip ci] ([10d6e7a](https://github.com/libp2p/js-libp2p-utils/commit/10d6e7a7731b746f199ffb2f186e28185cb512f5)) + + +### Dependencies + +* bump @multiformats/multiaddr from 11.6.1 to 12.0.0 ([#83](https://github.com/libp2p/js-libp2p-utils/issues/83)) ([3eeeeba](https://github.com/libp2p/js-libp2p-utils/commit/3eeeeba52b764b96463a1b6bcfcff394492eab2e)) +* **dev:** bump aegir from 37.12.1 to 38.1.7 ([#80](https://github.com/libp2p/js-libp2p-utils/issues/80)) ([2c262ba](https://github.com/libp2p/js-libp2p-utils/commit/2c262ba37d3668bc4f957914c40c5167cd8faf4f)) + +## [3.0.4](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.3...v3.0.4) (2022-12-16) + + +### Documentation + +* publish api docs ([#69](https://github.com/libp2p/js-libp2p-utils/issues/69)) ([044fd72](https://github.com/libp2p/js-libp2p-utils/commit/044fd7232eb0be2d8cd71fab6130c4f30190e22b)) + +## [3.0.3](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.2...v3.0.3) (2022-12-07) + + +### Bug Fixes + +* update project readme ([#66](https://github.com/libp2p/js-libp2p-utils/issues/66)) ([7e977a2](https://github.com/libp2p/js-libp2p-utils/commit/7e977a2739717225a4b1d74304e69500652a3386)) + + +### Dependencies + +* bump private-ip from 2.3.4 to 3.0.0 ([#63](https://github.com/libp2p/js-libp2p-utils/issues/63)) ([956f404](https://github.com/libp2p/js-libp2p-utils/commit/956f404bba12f2c712999046b19825496fe8be41)), closes [ChainSafe/is-ip#1](https://github.com/ChainSafe/is-ip/issues/1) [#19](https://github.com/libp2p/js-libp2p-utils/issues/19) [#21](https://github.com/libp2p/js-libp2p-utils/issues/21) +* **dev:** bump it-all from 1.0.6 to 2.0.0 ([#62](https://github.com/libp2p/js-libp2p-utils/issues/62)) ([99cca25](https://github.com/libp2p/js-libp2p-utils/commit/99cca2505721f282ed557dcfd28d8b46d064d6e2)), closes [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#27](https://github.com/libp2p/js-libp2p-utils/issues/27) [#24](https://github.com/libp2p/js-libp2p-utils/issues/24) +* **dev:** bump it-map from 1.0.6 to 2.0.0 ([#61](https://github.com/libp2p/js-libp2p-utils/issues/61)) ([88b05b4](https://github.com/libp2p/js-libp2p-utils/commit/88b05b4a6223774cd6dbaaa4f97e1da318f89856)) +* **dev:** bump uint8arrays from 3.1.1 to 4.0.2 ([#60](https://github.com/libp2p/js-libp2p-utils/issues/60)) ([ca0b632](https://github.com/libp2p/js-libp2p-utils/commit/ca0b63243b0ae23b6fa9195387466516c9acce80)), closes [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) [#41](https://github.com/libp2p/js-libp2p-utils/issues/41) [#40](https://github.com/libp2p/js-libp2p-utils/issues/40) [#28](https://github.com/libp2p/js-libp2p-utils/issues/28) + +## [3.0.2](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.1...v3.0.2) (2022-09-21) + + +### Trivial Changes + +* Update .github/workflows/stale.yml [skip ci] ([ea425dc](https://github.com/libp2p/js-libp2p-utils/commit/ea425dc19253202009497587681da1a7703ac429)) + + +### Dependencies + +* update @multiformats/multiaddr to 11.0.0 ([#59](https://github.com/libp2p/js-libp2p-utils/issues/59)) ([46bff23](https://github.com/libp2p/js-libp2p-utils/commit/46bff23bc5296359cfca02fbf078ee197a1629cc)) + +## [3.0.1](https://github.com/libp2p/js-libp2p-utils/compare/v3.0.0...v3.0.1) (2022-08-10) + + +### Bug Fixes + +* update deps ([#55](https://github.com/libp2p/js-libp2p-utils/issues/55)) ([134c633](https://github.com/libp2p/js-libp2p-utils/commit/134c633f107247ce309ed7da3a29f872615ee920)) + +## [3.0.0](https://github.com/libp2p/js-libp2p-utils/compare/v2.0.1...v3.0.0) (2022-06-27) + + +### ⚠ BREAKING CHANGES + +* **deps:** the API of the returned MultiaddrConnection has changed + +### Trivial Changes + +* **deps:** bump @libp2p/interface-connection from 1.0.1 to 2.1.0 ([#51](https://github.com/libp2p/js-libp2p-utils/issues/51)) ([0f99bf8](https://github.com/libp2p/js-libp2p-utils/commit/0f99bf833f7732d74eac4a06fd2b607555c7f34b)) + +## [2.0.1](https://github.com/libp2p/js-libp2p-utils/compare/v2.0.0...v2.0.1) (2022-06-27) + + +### Trivial Changes + +* remove unused deps ([#52](https://github.com/libp2p/js-libp2p-utils/issues/52)) ([8f339c9](https://github.com/libp2p/js-libp2p-utils/commit/8f339c9a50b466d65b41492abfd1bd88ffa0a38c)) + +## [2.0.0](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.10...v2.0.0) (2022-06-15) + + +### ⚠ BREAKING CHANGES + +* uses new single-issue libp2p interface modules + +### Features + +* update libp2p interfaces ([#47](https://github.com/libp2p/js-libp2p-utils/issues/47)) ([018fbe4](https://github.com/libp2p/js-libp2p-utils/commit/018fbe48f3506c0b90dc88779a3f12a2714ab09c)) + +### [1.0.10](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.9...v1.0.10) (2022-04-07) + + +### Trivial Changes + +* update aegir ([#39](https://github.com/libp2p/js-libp2p-utils/issues/39)) ([34f1fde](https://github.com/libp2p/js-libp2p-utils/commit/34f1fde4310c6571e90a6d312924146e089b5a9d)) + +### [1.0.9](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.8...v1.0.9) (2022-03-15) + + +### Bug Fixes + +* refactor address sort to be a regular sort function ([#35](https://github.com/libp2p/js-libp2p-utils/issues/35)) ([8d4e3d6](https://github.com/libp2p/js-libp2p-utils/commit/8d4e3d6f1b56d24e4e58df16ded87d3ca4f82a3f)) + +### [1.0.8](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.7...v1.0.8) (2022-03-03) + + +### Bug Fixes + +* correct update path for stream-to-ma-conn ([#34](https://github.com/libp2p/js-libp2p-utils/issues/34)) ([90cc6f5](https://github.com/libp2p/js-libp2p-utils/commit/90cc6f563c8640ba52ebfe2f8794999664ccd7eb)) + +### [1.0.7](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.6...v1.0.7) (2022-03-02) + + +### Bug Fixes + +* pass duplex to stream-to-ma-conn not stream ([#33](https://github.com/libp2p/js-libp2p-utils/issues/33)) ([ebc5c60](https://github.com/libp2p/js-libp2p-utils/commit/ebc5c6074c971e39c6e5c5c51e251aa00c544b50)) + +### [1.0.6](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.5...v1.0.6) (2022-02-10) + + +### Bug Fixes + +* update interfaces ([#32](https://github.com/libp2p/js-libp2p-utils/issues/32)) ([5d960f3](https://github.com/libp2p/js-libp2p-utils/commit/5d960f3d1566ccb9bf043b71eca5a83117955940)) + +### [1.0.5](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.4...v1.0.5) (2022-01-15) + + +### Bug Fixes + +* update it-* deps to typed versions ([#30](https://github.com/libp2p/js-libp2p-utils/issues/30)) ([b65ae5c](https://github.com/libp2p/js-libp2p-utils/commit/b65ae5c813efdc20ac11a13f349dc914ffc64c48)) + +### [1.0.4](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.3...v1.0.4) (2022-01-15) + + +### Trivial Changes + +* engines version ([#29](https://github.com/libp2p/js-libp2p-utils/issues/29)) ([47ce53c](https://github.com/libp2p/js-libp2p-utils/commit/47ce53c34b0106101215ed4b3ffe9f1fffd51cb6)) + +### [1.0.3](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.2...v1.0.3) (2022-01-14) + + +### Trivial Changes + +* project updates ([#23](https://github.com/libp2p/js-libp2p-utils/issues/23)) ([cb7ea61](https://github.com/libp2p/js-libp2p-utils/commit/cb7ea61e6df7a721863ad8fba7c73d376b2c3ab8)) + +### [1.0.2](https://github.com/libp2p/js-libp2p-utils/compare/v1.0.1...v1.0.2) (2022-01-08) + + +### Trivial Changes + +* add semantic release config ([#22](https://github.com/libp2p/js-libp2p-utils/issues/22)) ([5fcf8c7](https://github.com/libp2p/js-libp2p-utils/commit/5fcf8c7f57864e4e92f6606656cf3ea1f214f0c4)) + +## [0.4.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.4.0...v0.4.1) (2021-07-08) + + + +# [0.4.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.3.1...v0.4.0) (2021-07-07) + + +### chore + +* update to new multiformats ([#18](https://github.com/libp2p/js-libp2p-utils/issues/18)) ([24ca72c](https://github.com/libp2p/js-libp2p-utils/commit/24ca72c95bf485af513fba59830796a5fcf71437)) + + +### BREAKING CHANGES + +* updates multiaddr which uses the new CID class + + + +## [0.3.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.3.0...v0.3.1) (2021-04-12) + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.3...v0.3.0) (2021-04-08) + + +### Features + +* add types ([#16](https://github.com/libp2p/js-libp2p-utils/issues/16)) ([e0552b5](https://github.com/libp2p/js-libp2p-utils/commit/e0552b5b6b1d912a8f6f1e39b1a4b70fca91f547)) + + + + +## [0.2.3](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.2...v0.2.3) (2020-11-30) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-utils/compare/v0.2.1...v0.2.2) (2020-11-16) + + +### Features + +* address sorter ([#13](https://github.com/libp2p/js-libp2p-utils/issues/13)) ([cb5e716](https://github.com/libp2p/js-libp2p-utils/commit/cb5e716)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.3...v0.2.1) (2020-10-08) + + +### Chores + +* update deps ([#9](https://github.com/libp2p/js-libp2p-utils/issues/9)) ([a2ea68f](https://github.com/libp2p/js-libp2p-utils/commit/a2ea68f)) + + +### Features + +* is multiaddr private and loopback ([#10](https://github.com/libp2p/js-libp2p-utils/issues/10)) ([d7fa562](https://github.com/libp2p/js-libp2p-utils/commit/d7fa562)) + + +### BREAKING CHANGES + +* - The multiaddr dep of this module uses Uint8Arrays and may not be + compatible with previous versions + +* chore: remove gh url + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.3...v0.2.0) (2020-08-07) + + +### Chores + +* update deps ([#9](https://github.com/libp2p/js-libp2p-utils/issues/9)) ([a2ea68f](https://github.com/libp2p/js-libp2p-utils/commit/a2ea68f)) + + +### BREAKING CHANGES + +* - The multiaddr dep of this module uses Uint8Arrays and may not be + compatible with previous versions + +* chore: remove gh url + + + + +## [0.1.3](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.2...v0.1.3) (2020-07-15) + + +### Features + +* arrayEquals for non primitive types with equals function ([80668ff](https://github.com/libp2p/js-libp2p-utils/commit/80668ff)) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.1...v0.1.2) (2020-02-15) + + +### Features + +* stream to multiaddr connection converter ([#2](https://github.com/libp2p/js-libp2p-utils/issues/2)) ([6220631](https://github.com/libp2p/js-libp2p-utils/commit/6220631)) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-utils/compare/v0.1.0...v0.1.1) (2020-02-13) + + + + +# 0.1.0 (2019-09-23) + + +### Features + +* ip port to multiaddr ([#1](https://github.com/libp2p/js-libp2p-utils/issues/1)) ([426b421](https://github.com/libp2p/js-libp2p-utils/commit/426b421)) diff --git a/packages/utils/LICENSE b/packages/utils/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/utils/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/utils/LICENSE-APACHE b/packages/utils/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/utils/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/utils/LICENSE-MIT b/packages/utils/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/utils/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 0000000000..4fbd0f1a5f --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,65 @@ +# @libp2p/utils + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Package to aggregate shared logic and dependencies for the libp2p ecosystem + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +The libp2p ecosystem has lots of repos with it comes several problems like: + +- Domain logic dedupe - all modules shared a lot of logic like validation, streams handling, etc. +- Dependencies management - it's really easy with so many repos for dependencies to go out of control, they become outdated, different repos use different modules to do the same thing (like merging defaults options), browser bundles ends up with multiple versions of the same package, bumping versions is cumbersome to do because we need to go through several repos, etc. + +These problems are the motivation for this package, having shared logic in this package avoids creating cyclic dependencies, centralizes common use modules/functions (exactly like aegir does for the tooling), semantic versioning for 3rd party dependencies is handled in one single place (a good example is going from streams 2 to 3) and maintainers should only care about having `libp2p-utils` updated. + +## Usage + +Each function should be imported directly. + +```js +import ipAndPortToMultiaddr from '@libp2p/utils/ip-port-to-multiaddr' + +const ma = ipAndPortToMultiaddr('127.0.0.1', 9000) +``` + +You can check the [API docs](https://libp2p.github.io/js-libp2p-utils). + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..604b675c13 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,110 @@ +{ + "name": "@libp2p/utils", + "version": "3.0.12", + "description": "Package to aggregate shared logic and dependencies for the libp2p ecosystem", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/utils#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./address-sort": { + "types": "./dist/src/address-sort.d.ts", + "import": "./dist/src/address-sort.js" + }, + "./array-equals": { + "types": "./dist/src/array-equals.d.ts", + "import": "./dist/src/array-equals.js" + }, + "./ip-port-to-multiaddr": { + "types": "./dist/src/ip-port-to-multiaddr.d.ts", + "import": "./dist/src/ip-port-to-multiaddr.js" + }, + "./multiaddr/is-loopback": { + "types": "./dist/src/multiaddr/is-loopback.d.ts", + "import": "./dist/src/multiaddr/is-loopback.js" + }, + "./multiaddr/is-private": { + "types": "./dist/src/multiaddr/is-private.d.ts", + "import": "./dist/src/multiaddr/is-private.js" + }, + "./stream-to-ma-conn": { + "types": "./dist/src/stream-to-ma-conn.d.ts", + "import": "./dist/src/stream-to-ma-conn.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@achingbrain/ip-address": "^8.1.0", + "@libp2p/interface-connection": "^5.0.0", + "@libp2p/interface-peer-store": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@libp2p/logger": "^2.0.0", + "@multiformats/multiaddr": "^12.1.3", + "abortable-iterator": "^5.0.1", + "is-loopback-addr": "^2.0.1", + "it-stream-types": "^2.0.1", + "private-ip": "^3.0.0", + "uint8arraylist": "^2.4.3" + }, + "devDependencies": { + "aegir": "^39.0.10", + "it-all": "^3.0.1", + "it-pair": "^2.0.6", + "it-pipe": "^3.0.1", + "uint8arrays": "^4.0.3" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/utils/src/address-sort.ts b/packages/utils/src/address-sort.ts new file mode 100644 index 0000000000..3df8ababd4 --- /dev/null +++ b/packages/utils/src/address-sort.ts @@ -0,0 +1,55 @@ +/** + * @packageDocumentation + * + * Provides strategies to sort a list of multiaddrs. + * + * @example + * + * ```typescript + * import { publicAddressesFirst } from '@libp2p/utils/address-sort' + * import { multiaddr } from '@multformats/multiaddr' + * + * + * const addresses = [ + * multiaddr('/ip4/127.0.0.1/tcp/9000'), + * multiaddr('/ip4/82.41.53.1/tcp/9000') + * ].sort(publicAddressesFirst) + * + * console.info(addresses) + * // ['/ip4/82.41.53.1/tcp/9000', '/ip4/127.0.0.1/tcp/9000'] + * ``` + */ + +import { isPrivate } from './multiaddr/is-private.js' +import type { Address } from '@libp2p/interface-peer-store' + +/** + * Compare function for array.sort(). + * This sort aims to move the private addresses to the end of the array. + * In case of equality, a certified address will come first. + */ +export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { + const isAPrivate = isPrivate(a.multiaddr) + const isBPrivate = isPrivate(b.multiaddr) + + if (isAPrivate && !isBPrivate) { + return 1 + } else if (!isAPrivate && isBPrivate) { + return -1 + } + // Check certified? + if (a.isCertified && !b.isCertified) { + return -1 + } else if (!a.isCertified && b.isCertified) { + return 1 + } + + return 0 +} + +/** + * A test thing + */ +export async function something (): Promise { + return Uint8Array.from([0, 1, 2]) +} diff --git a/packages/utils/src/array-equals.ts b/packages/utils/src/array-equals.ts new file mode 100644 index 0000000000..e94499acbb --- /dev/null +++ b/packages/utils/src/array-equals.ts @@ -0,0 +1,34 @@ +/** + * @packageDocumentation + * + * Provides strategies ensure arrays are equivalent. + * + * @example + * + * ```typescript + * import { arrayEquals } from '@libp2p/utils/array-equals' + * import { multiaddr } from '@multformats/multiaddr' + * + * const ma1 = multiaddr('/ip4/127.0.0.1/tcp/9000'), + * const ma2 = multiaddr('/ip4/82.41.53.1/tcp/9000') + * + * console.info(arrayEquals([ma1], [ma1])) // true + * console.info(arrayEquals([ma1], [ma2])) // false + * ``` + */ + +/** + * Verify if two arrays of non primitive types with the "equals" function are equal. + * Compatible with multiaddr, peer-id and others. + */ +export function arrayEquals (a: any[], b: any[]): boolean { + const sort = (a: any, b: any): number => a.toString().localeCompare(b.toString()) + + if (a.length !== b.length) { + return false + } + + b.sort(sort) + + return a.sort(sort).every((item, index) => b[index].equals(item)) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/utils/src/ip-port-to-multiaddr.ts b/packages/utils/src/ip-port-to-multiaddr.ts new file mode 100644 index 0000000000..bff7020e3a --- /dev/null +++ b/packages/utils/src/ip-port-to-multiaddr.ts @@ -0,0 +1,47 @@ +import { Address4, Address6 } from '@achingbrain/ip-address' +import { CodeError } from '@libp2p/interfaces/errors' +import { logger } from '@libp2p/logger' +import { type Multiaddr, multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:ip-port-to-multiaddr') + +export const Errors = { + ERR_INVALID_IP_PARAMETER: 'ERR_INVALID_IP_PARAMETER', + ERR_INVALID_PORT_PARAMETER: 'ERR_INVALID_PORT_PARAMETER', + ERR_INVALID_IP: 'ERR_INVALID_IP' +} + +/** + * Transform an IP, Port pair into a multiaddr + */ +export function ipPortToMultiaddr (ip: string, port: number | string): Multiaddr { + if (typeof ip !== 'string') { + throw new CodeError(`invalid ip provided: ${ip}`, Errors.ERR_INVALID_IP_PARAMETER) // eslint-disable-line @typescript-eslint/restrict-template-expressions + } + + if (typeof port === 'string') { + port = parseInt(port) + } + + if (isNaN(port)) { + throw new CodeError(`invalid port provided: ${port}`, Errors.ERR_INVALID_PORT_PARAMETER) + } + + try { + // Test valid IPv4 + new Address4(ip) // eslint-disable-line no-new + return multiaddr(`/ip4/${ip}/tcp/${port}`) + } catch {} + + try { + // Test valid IPv6 + const ip6 = new Address6(ip) + return ip6.is4() + ? multiaddr(`/ip4/${ip6.to4().correctForm()}/tcp/${port}`) + : multiaddr(`/ip6/${ip}/tcp/${port}`) + } catch (err) { + const errMsg = `invalid ip:port for creating a multiaddr: ${ip}:${port}` + log.error(errMsg) + throw new CodeError(errMsg, Errors.ERR_INVALID_IP) + } +} diff --git a/packages/utils/src/multiaddr/is-loopback.ts b/packages/utils/src/multiaddr/is-loopback.ts new file mode 100644 index 0000000000..d66f545681 --- /dev/null +++ b/packages/utils/src/multiaddr/is-loopback.ts @@ -0,0 +1,11 @@ +import { isLoopbackAddr } from 'is-loopback-addr' +import type { Multiaddr } from '@multiformats/multiaddr' + +/** + * Check if a given multiaddr is a loopback address. + */ +export function isLoopback (ma: Multiaddr): boolean { + const { address } = ma.nodeAddress() + + return isLoopbackAddr(address) +} diff --git a/packages/utils/src/multiaddr/is-private.ts b/packages/utils/src/multiaddr/is-private.ts new file mode 100644 index 0000000000..ab563e032c --- /dev/null +++ b/packages/utils/src/multiaddr/is-private.ts @@ -0,0 +1,15 @@ +import isIpPrivate from 'private-ip' +import type { Multiaddr } from '@multiformats/multiaddr' + +/** + * Check if a given multiaddr has a private address. + */ +export function isPrivate (ma: Multiaddr): boolean { + try { + const { address } = ma.nodeAddress() + + return Boolean(isIpPrivate(address)) + } catch { + return true + } +} diff --git a/packages/utils/src/stream-to-ma-conn.ts b/packages/utils/src/stream-to-ma-conn.ts new file mode 100644 index 0000000000..3fa09dac4a --- /dev/null +++ b/packages/utils/src/stream-to-ma-conn.ts @@ -0,0 +1,94 @@ +import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import type { MultiaddrConnection } from '@libp2p/interface-connection' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +const log = logger('libp2p:stream:converter') + +export interface Timeline { + /** + * Connection opening timestamp + */ + open: number + + /** + * Connection upgraded timestamp + */ + upgraded?: number + + /** + * Connection closed timestamp + */ + close?: number +} + +export interface StreamOptions { + signal?: AbortSignal + +} + +export interface StreamProperties { + stream: Duplex, Source> + remoteAddr: Multiaddr + localAddr: Multiaddr +} + +/** + * Convert a duplex iterable into a MultiaddrConnection. + * https://github.com/libp2p/interface-transport#multiaddrconnection + */ +export function streamToMaConnection (props: StreamProperties, options: StreamOptions = {}): MultiaddrConnection { + const { stream, remoteAddr } = props + const { sink, source } = stream + + const mapSource = (async function * () { + for await (const list of source) { + if (list instanceof Uint8Array) { + yield list + } else { + yield * list + } + } + }()) + + const maConn: MultiaddrConnection = { + async sink (source) { + if (options.signal != null) { + source = abortableSource(source, options.signal) + } + + try { + await sink(source) + await close() + } catch (err: any) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log(err) + } + } + }, + source: (options.signal != null) ? abortableSource(mapSource, options.signal) : mapSource, + remoteAddr, + timeline: { open: Date.now(), close: undefined }, + async close () { + await sink(async function * () { + yield new Uint8Array(0) + }()) + await close() + } + } + + async function close (): Promise { + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + await Promise.resolve() + } + + return maConn +} diff --git a/packages/utils/test/address-sort.spec.ts b/packages/utils/test/address-sort.spec.ts new file mode 100644 index 0000000000..39ede62c9a --- /dev/null +++ b/packages/utils/test/address-sort.spec.ts @@ -0,0 +1,51 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { publicAddressesFirst } from '../src/address-sort.js' + +describe('address-sort', () => { + it('should sort public addresses first', () => { + const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'), + isCertified: false + } + ] + + const sortedAddresses = addresses.sort(publicAddressesFirst) + expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true) + expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true) + expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true) + }) + + it('should sort public certified addresses first', () => { + const addresses = [ + { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + }, + { + multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'), + isCertified: true + } + ] + + const sortedAddresses = addresses.sort(publicAddressesFirst) + expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true) + expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true) + expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true) + }) +}) diff --git a/packages/utils/test/array-equals.spec.ts b/packages/utils/test/array-equals.spec.ts new file mode 100644 index 0000000000..7b50b76071 --- /dev/null +++ b/packages/utils/test/array-equals.spec.ts @@ -0,0 +1,70 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { arrayEquals } from '../src/array-equals.js' + +describe('non primitive array equals', () => { + it('returns true if two arrays of multiaddrs are equal', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(true) + }) + + it('returns true if two arrays of multiaddrs have the same content but different orders', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(true) + }) + + it('returns false if two arrays of multiaddrs are different', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8001'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(false) + }) + + it('returns false if two arrays of multiaddrs are partially equal, but different lengths', () => { + const a = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/ip4/127.0.0.1/tcp/3000/ws'), + multiaddr('/dns4/test.libp2p.io') + ] + + const b = [ + multiaddr('/ip4/127.0.0.1/tcp/8000'), + multiaddr('/dns4/test.libp2p.io') + ] + + expect(arrayEquals(a, b)).to.eql(false) + }) +}) diff --git a/packages/utils/test/ip-port-to-multiaddr.spec.ts b/packages/utils/test/ip-port-to-multiaddr.spec.ts new file mode 100644 index 0000000000..2e1631743f --- /dev/null +++ b/packages/utils/test/ip-port-to-multiaddr.spec.ts @@ -0,0 +1,47 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { ipPortToMultiaddr, Errors } from '../src/ip-port-to-multiaddr.js' + +describe('IP and port to Multiaddr', () => { + it('creates multiaddr from valid IPv4 IP and port', () => { + const ip = '127.0.0.1' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/${ip}/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv4 IP and numeric port', () => { + const ip = '127.0.0.1' + const port = 9090 + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/${ip}/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv4 in IPv6 IP and port', () => { + const ip = '0:0:0:0:0:0:101.45.75.219' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip4/101.45.75.219/tcp/${port}`) + }) + + it('creates multiaddr from valid IPv6 IP and port', () => { + const ip = '::1' + const port = '9090' + expect(ipPortToMultiaddr(ip, port).toString()).to.equal(`/ip6/${ip}/tcp/${port}`) + }) + + it('throws for missing IP address', () => { + // @ts-expect-error invalid args + expect(() => ipPortToMultiaddr()).to.throw('invalid ip provided').with.property('code', Errors.ERR_INVALID_IP_PARAMETER) + }) + + it('throws for invalid IP address', () => { + const ip = 'aewmrn4awoew' + const port = '234' + expect(() => ipPortToMultiaddr(ip, port)).to.throw('invalid ip:port for creating a multiaddr').with.property('code', Errors.ERR_INVALID_IP) + }) + + it('throws for invalid port', () => { + const ip = '127.0.0.1' + const port = 'garbage' + expect(() => ipPortToMultiaddr(ip, port)).to.throw('invalid port provided').with.property('code', Errors.ERR_INVALID_PORT_PARAMETER) + }) +}) diff --git a/packages/utils/test/multiaddr/is-loopback.spec.ts b/packages/utils/test/multiaddr/is-loopback.spec.ts new file mode 100644 index 0000000000..3aa79ca6cb --- /dev/null +++ b/packages/utils/test/multiaddr/is-loopback.spec.ts @@ -0,0 +1,55 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isLoopback } from '../../src/multiaddr/is-loopback.js' + +describe('multiaddr isLoopback', () => { + it('identifies loopback ip4 multiaddrs', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000'), + multiaddr('/ip4/127.0.1.1/tcp/1000'), + multiaddr('/ip4/127.1.1.1/tcp/1000'), + multiaddr('/ip4/127.255.255.255/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(true) + }) + }) + + it('identifies non loopback ip4 multiaddrs', () => { + [ + multiaddr('/ip4/101.0.26.90/tcp/1000'), + multiaddr('/ip4/10.0.0.1/tcp/1000'), + multiaddr('/ip4/192.168.0.1/tcp/1000'), + multiaddr('/ip4/172.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) + + it('identifies loopback ip6 multiaddrs', () => { + [ + multiaddr('/ip6/::1/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(true) + }) + }) + + it('identifies non loopback ip6 multiaddrs', () => { + [ + multiaddr('/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/::/tcp/1000') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) + + it('identifies other multiaddrs as not loopback addresses', () => { + [ + multiaddr('/dns4/wss0.bootstrap.libp2p.io/tcp/443'), + multiaddr('/dns6/wss0.bootstrap.libp2p.io/tcp/443') + ].forEach(ma => { + expect(isLoopback(ma)).to.eql(false) + }) + }) +}) diff --git a/packages/utils/test/multiaddr/is-private.spec.ts b/packages/utils/test/multiaddr/is-private.spec.ts new file mode 100644 index 0000000000..a5a12f6406 --- /dev/null +++ b/packages/utils/test/multiaddr/is-private.spec.ts @@ -0,0 +1,66 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { isPrivate } from '../../src/multiaddr/is-private.js' + +describe('multiaddr isPrivate', () => { + it('identifies private ip4 multiaddrs', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000'), + multiaddr('/ip4/10.0.0.1/tcp/1000'), + multiaddr('/ip4/192.168.0.1/tcp/1000'), + multiaddr('/ip4/172.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) + + it('identifies public ip4 multiaddrs', () => { + [ + multiaddr('/ip4/101.0.26.90/tcp/1000'), + multiaddr('/ip4/40.1.20.9/tcp/1000'), + multiaddr('/ip4/92.168.0.1/tcp/1000'), + multiaddr('/ip4/2.16.0.1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies private ip6 multiaddrs', () => { + [ + multiaddr('/ip6/fd52:8342:fc46:6c91:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/fd52:8342:fc46:6c91:3ac9:86ff:fe31:1/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) + + it('identifies public ip6 multiaddrs', () => { + [ + multiaddr('/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000'), + multiaddr('/ip6/2000:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies other multiaddrs as not private addresses', () => { + [ + multiaddr('/dns4/wss0.bootstrap.libp2p.io/tcp/443'), + multiaddr('/dns6/wss0.bootstrap.libp2p.io/tcp/443') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(false) + }) + }) + + it('identifies non-public addresses', () => { + [ + multiaddr('/ip4/127.0.0.1/tcp/1000/p2p-circuit'), + multiaddr('/unix/foo/bar/baz.sock'), + multiaddr('/ip4/127.0.0.1/sctp/1000') + ].forEach(ma => { + expect(isPrivate(ma)).to.eql(true) + }) + }) +}) diff --git a/packages/utils/test/stream-to-ma-conn.spec.ts b/packages/utils/test/stream-to-ma-conn.spec.ts new file mode 100644 index 0000000000..470cb85090 --- /dev/null +++ b/packages/utils/test/stream-to-ma-conn.spec.ts @@ -0,0 +1,79 @@ +/* eslint-env mocha */ + +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import all from 'it-all' +import { pair } from 'it-pair' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { streamToMaConnection } from '../src/stream-to-ma-conn.js' +import type { Stream } from '@libp2p/interface-connection' +import type { Duplex, Source } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' + +function toMuxedStream (stream: Duplex, Source, Promise>): Stream { + const muxedStream: Stream = { + ...stream, + close: () => {}, + closeRead: () => {}, + closeWrite: () => {}, + abort: () => {}, + reset: () => {}, + stat: { + direction: 'outbound', + timeline: { + open: Date.now() + } + }, + metadata: {}, + id: `muxed-stream-${Math.random()}` + } + + return muxedStream +} + +describe('Convert stream into a multiaddr connection', () => { + const localAddr = multiaddr('/ip4/101.45.75.219/tcp/6000') + const remoteAddr = multiaddr('/ip4/100.46.74.201/tcp/6002') + + it('converts a stream and adds the provided metadata', async () => { + const stream = pair() + + const maConn = streamToMaConnection({ + stream: toMuxedStream(stream), + localAddr, + remoteAddr + }) + + expect(maConn).to.exist() + expect(maConn.sink).to.exist() + expect(maConn.source).to.exist() + expect(maConn.remoteAddr).to.eql(remoteAddr) + expect(maConn.timeline).to.exist() + expect(maConn.timeline.open).to.exist() + expect(maConn.timeline.close).to.not.exist() + + await maConn.close() + expect(maConn.timeline.close).to.exist() + }) + + it('can stream data over the multiaddr connection', async () => { + const stream = pair() + const maConn = streamToMaConnection({ + stream: toMuxedStream(stream), + localAddr, + remoteAddr + }) + + const data = uint8ArrayFromString('hey') + const streamData = await pipe( + [data], + maConn, + async (source) => all(source) + ) + + expect(streamData).to.eql([data]) + // underlying stream end closes the connection + expect(maConn.timeline.close).to.exist() + }) +}) diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..1bd7fbd5b8 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface-connection" + }, + { + "path": "../interface-peer-store" + }, + { + "path": "../interfaces" + }, + { + "path": "../logger" + } + ] +}

      HQ{JjW6n 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