diff --git a/.gitignore b/.gitignore index 1fe1b00e..dd8b4296 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ node_modules/ +package-lock.json diff --git a/.travis.yml b/.travis.yml index c62306b7..c2c88b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,21 +22,8 @@ jobs: - stage: check script: - - npx aegir commitlint --travis - npx aegir dep-check - npm run lint - - stage: test - name: chrome - addons: - chrome: stable - script: npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - notifications: email: false diff --git a/README.md b/README.md index b137d4aa..03b544b4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ # gossipsub-js [![Travis CI](https://flat.badgen.net/travis/ipfs/aegir)](https://travis-ci.com/ipfs/aegir) -Javascript implementation of Gossipsub + +## Lead Maintainer + +[Vasco Santos](https://github.com/vasco-santos) + +## Table of Contents + +* [Specs](#specs) +* [Install](#Install) +* [Usage](#Usage) +* [API](#API) +* [Contribute](#Contribute) +* [License](#License) + +## Specs +Gossipsub is an implementation of pubsub based on meshsub and floodsub. You can read the specification [here](https://github.com/libp2p/specs/tree/master/pubsub/gossipsub). + +## Install + +`npm install libp2p-gossipsub` + +## Usage + +```javascript +const Gossipsub = require('libp2p-gossipsub') + +const gsub = new Gossipsub(node) + +gsub.start((err) => { + if (err) { + console.log('Upsy', err) + } + gsub.on('fruit', (data) => { + console.log(data) + }) + gsub.subscribe('fruit') + + gsub.publish('fruit', new Buffer('banana')) +}) + +``` + +## API + +## Contribute + +This module is actively under development. Please check out the issues and submit PRs! + +## License + +MIT © Protocol Labs diff --git a/index.js b/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 740a350f..00000000 --- a/package-lock.json +++ /dev/null @@ -1,1219 +0,0 @@ -{ - "name": "gossipsub", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "asn1.js": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.0.1.tgz", - "integrity": "sha512-aO8EaEgbgqq77IEw+1jfx5c9zTbzvkfuRBuZsSsPnTHMkmd5AI4J6OtITLZFa381jReeaQL67J0GBTUu0+ZTVw==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "base-x": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.4.tgz", - "integrity": "sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "big.js": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.1.2.tgz", - "integrity": "sha512-qG6ZOc1lY84Bn8p/z9xvJisj9F4PRyo0pOGqGNYc7gS3p1WciS/3XcLuNI3Z/yYZpMNFhHeX3YNENwgrQq0NTA==" - }, - "bindings": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", - "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" - }, - "bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "blakejs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", - "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-is": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", - "integrity": "sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "defaults-deep": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/defaults-deep/-/defaults-deep-0.2.4.tgz", - "integrity": "sha512-V6BtqzcMvn0EPOy7f+SfMhfmTawq+7UQdt9yZH0EBK89+IHo5f+Hse/qzTorAXOBrQpxpwb6cB/8OgtaMrT+Fg==", - "requires": { - "for-own": "^0.1.3", - "is-extendable": "^0.1.1", - "lazy-cache": "^0.2.3" - } - }, - "drbg.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", - "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", - "requires": { - "browserify-aes": "^1.0.6", - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4" - } - }, - "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "requires": { - "for-in": "^1.0.1" - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hashlru": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.2.1.tgz", - "integrity": "sha1-EPIJmg18BaQPK+r1wdOc8vfavzY=" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoek": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", - "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "interface-connection": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/interface-connection/-/interface-connection-0.3.2.tgz", - "integrity": "sha1-5JSYg/bqeft+3QHuP0/KR6Kf0sQ=", - "requires": { - "pull-defer": "~0.2.2", - "timed-tape": "~0.1.1" - } - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "ip-address": { - "version": "5.8.9", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.8.9.tgz", - "integrity": "sha512-7ay355oMN34iXhET1BmCJVsHjOTSItEEIIpOs38qUC23AIhOy+xIPnkrTuEFjeLMrTJ7m8KMXWgWfy/2Vn9sDw==", - "requires": { - "jsbn": "1.1.0", - "lodash.find": "^4.6.0", - "lodash.max": "^4.0.1", - "lodash.merge": "^4.6.0", - "lodash.padstart": "^4.6.1", - "lodash.repeat": "^4.1.0", - "sprintf-js": "1.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-promise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isemail": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.3.tgz", - "integrity": "sha512-5xbsG5wYADIcB+mfLsd+nst1V/D+I7EU7LEZPo2GOIMu4JzfcRs5yQoypP4avA7QtUqgxYLKBYNv4IdzBmbhdw==", - "requires": { - "punycode": "2.x.x" - } - }, - "joi": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-13.6.0.tgz", - "integrity": "sha512-E4QB0yRgEa6ZZKcSHJuBC+QeAwy+akCG0Bsa9edLqljyhlr+GuGDSmXYW1q7sj/FuAPy+ECUI3evVtK52tVfwg==", - "requires": { - "hoek": "5.x.x", - "isemail": "3.x.x", - "topo": "3.x.x" - } - }, - "joi-browser": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/joi-browser/-/joi-browser-13.4.0.tgz", - "integrity": "sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==" - }, - "js-sha3": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.7.0.tgz", - "integrity": "sha512-Wpks3yBDm0UcL5qlVhwW9Jr9n9i4FfeWBFOOXP5puDS/SiudJGhw7DPyBqn3487qD4F0lsC0q3zxink37f7zeA==" - }, - "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" - }, - "keypair": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/keypair/-/keypair-1.0.1.tgz", - "integrity": "sha1-dgNxknCvtlZO04oiCHoG/Jqk6hs=" - }, - "latency-monitor": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/latency-monitor/-/latency-monitor-0.2.1.tgz", - "integrity": "sha1-QEPV8j3obiv872ztSjtbki4d1+0=", - "requires": { - "debug": "^2.6.0", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "lazy-cache": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", - "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=" - }, - "length-prefixed-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/length-prefixed-stream/-/length-prefixed-stream-1.6.0.tgz", - "integrity": "sha512-gsJvrb5giDqil/ScQ7fEoplsI2Ch4DwnvnfTW2EGl9KBW6Ekzn8JSNESObqNAeZD8HkSjEMvc5XjhuB66fsSZQ==", - "requires": { - "buffer-alloc-unsafe": "^1.0.0", - "readable-stream": "^2.0.0", - "varint": "^5.0.0" - } - }, - "libp2p": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-0.23.1.tgz", - "integrity": "sha512-ArCIJ+EHpeT2qdfEI1Q9sN8oIB0R3Lw7YKLbJKfhK7Mq/+kYug+6RJqgomLJDVDeChTWZa0oe7CB7wEt+IvEpA==", - "requires": { - "async": "^2.6.1", - "joi": "^13.4.0", - "joi-browser": "^13.4.0", - "libp2p-connection-manager": "~0.0.2", - "libp2p-floodsub": "~0.15.0", - "libp2p-ping": "~0.8.0", - "libp2p-switch": "~0.40.7", - "libp2p-websockets": "~0.12.0", - "mafmt": "^6.0.0", - "multiaddr": "^5.0.0", - "peer-book": "~0.8.0", - "peer-id": "~0.11.0", - "peer-info": "~0.14.1" - } - }, - "libp2p-circuit": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/libp2p-circuit/-/libp2p-circuit-0.2.1.tgz", - "integrity": "sha512-Nr2MyO3onFk1E3hnEtII6MefU7Ps4oPOQ1dcsiFSkoq0NOf2PDCIJ12ySyMfZilmnJbMsGklSVi2fuPyv9PqvA==", - "requires": { - "async": "^2.6.0", - "debug": "^3.1.0", - "defaults-deep": "^0.2.4", - "interface-connection": "^0.3.2", - "mafmt": "^6.0.0", - "multiaddr": "^5.0.0", - "multistream-select": "^0.14.1", - "peer-id": "^0.11.0", - "peer-info": "~0.14.1", - "protons": "^1.0.1", - "pull-abortable": "^4.1.1", - "pull-handshake": "^1.1.4", - "pull-stream": "^3.6.7" - } - }, - "libp2p-connection-manager": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/libp2p-connection-manager/-/libp2p-connection-manager-0.0.2.tgz", - "integrity": "sha512-G/OzMfxQe0lHx7ujibPqpFLCeMN9I5vNH0+Rs9zat6+uIT51Saupx95lyoyh5J8nh93ui2cNH7PQnwJMZVKa1A==", - "requires": { - "debug": "^3.1.0", - "latency-monitor": "^0.2.1" - } - }, - "libp2p-crypto": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.13.0.tgz", - "integrity": "sha512-i3r1TBec/xYmC5bcpPiIs3OyUAU3iy53OdRdxqawKoWTQPjYB+TyQ4w+otT66Y0sMcw70O0wH3GFAfPmQgFn+g==", - "requires": { - "asn1.js": "^5.0.0", - "async": "^2.6.0", - "browserify-aes": "^1.2.0", - "bs58": "^4.0.1", - "keypair": "^1.0.1", - "libp2p-crypto-secp256k1": "~0.2.2", - "multihashing-async": "~0.4.8", - "node-forge": "^0.7.5", - "pem-jwk": "^1.5.1", - "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", - "tweetnacl": "^1.0.0" - }, - "dependencies": { - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8" - } - } - }, - "libp2p-crypto-secp256k1": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/libp2p-crypto-secp256k1/-/libp2p-crypto-secp256k1-0.2.2.tgz", - "integrity": "sha1-DdUh8Yq8TjahUuJOmzYwewrpzwU=", - "requires": { - "async": "^2.5.0", - "multihashing-async": "~0.4.6", - "nodeify": "^1.0.1", - "safe-buffer": "^5.1.1", - "secp256k1": "^3.3.0" - } - }, - "libp2p-floodsub": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/libp2p-floodsub/-/libp2p-floodsub-0.15.0.tgz", - "integrity": "sha512-sDVNxE6GKOZ7+qWE06jQuJ/CrYgPfOqkRD4qWPFe02AtghswyocWJkDiceKHx++mW2h2KYl7ae68XK0DLEEOiw==", - "requires": { - "async": "^2.6.0", - "bs58": "^4.0.1", - "debug": "^3.1.0", - "length-prefixed-stream": "^1.5.2", - "libp2p-crypto": "~0.13.0", - "lodash.values": "^4.3.0", - "protons": "^1.0.1", - "pull-pushable": "^2.2.0", - "time-cache": "~0.3.0" - } - }, - "libp2p-identify": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/libp2p-identify/-/libp2p-identify-0.7.2.tgz", - "integrity": "sha512-zYdeUdoUfUMz4FC+eEfrE+GR3G/STTvitGB/DDOlyNdYDLDWS/R7UORppsiHFtCdUj2mEi2E6JJsUZEgeYrJhQ==", - "requires": { - "multiaddr": "^5.0.0", - "peer-id": "~0.10.7", - "peer-info": "~0.14.1", - "protons": "^1.0.1", - "pull-length-prefixed": "^1.3.0", - "pull-stream": "^3.6.7" - }, - "dependencies": { - "libp2p-crypto": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.12.1.tgz", - "integrity": "sha512-1/z8rxZ0DcQNreZhEsl7PnLr7DWOioSvYbKBLGkRwNRiNh1JJLgh0PdTySBb44wkrOGT+TxcGRd7iq3/X6Wxwg==", - "requires": { - "asn1.js": "^5.0.0", - "async": "^2.6.0", - "browserify-aes": "^1.1.1", - "bs58": "^4.0.1", - "keypair": "^1.0.1", - "libp2p-crypto-secp256k1": "~0.2.2", - "multihashing-async": "~0.4.7", - "node-forge": "^0.7.1", - "pem-jwk": "^1.5.1", - "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", - "tweetnacl": "^1.0.0" - } - }, - "peer-id": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.10.7.tgz", - "integrity": "sha512-VEpMFcL9q0NQijmR0jsj38OGbY4yzaWMEareVkDahopmlNT+Cpsot8btPgsgBBApP9NiZj2Enwvh8rZN30ocQw==", - "requires": { - "async": "^2.6.0", - "libp2p-crypto": "~0.12.1", - "lodash": "^4.17.5", - "multihashes": "~0.4.13" - } - }, - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8" - } - } - }, - "libp2p-ping": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/libp2p-ping/-/libp2p-ping-0.8.0.tgz", - "integrity": "sha512-7GtCCvbs6sEabnjh2ZIdru8wuKP4Qux6alw7wuaMosqWkPeFnnFmQsGaWEGpwEmD49A1dsT+aIYvAx5jFB02Bw==", - "requires": { - "libp2p-crypto": "~0.13.0", - "pull-handshake": "^1.1.4", - "pull-stream": "^3.6.7" - } - }, - "libp2p-switch": { - "version": "0.40.7", - "resolved": "https://registry.npmjs.org/libp2p-switch/-/libp2p-switch-0.40.7.tgz", - "integrity": "sha512-0qEDbp7tjnVfzwesfOT5oKGgZzBYKPUt8C0fZ3xtskuVkklumtVlq9bXJ5lAqdmAwDYJaFOcAleOlfZqN/Dhzg==", - "requires": { - "async": "^2.6.0", - "big.js": "^5.1.2", - "debug": "^3.1.0", - "hashlru": "^2.2.1", - "interface-connection": "~0.3.2", - "ip-address": "^5.8.9", - "libp2p-circuit": "~0.2.0", - "libp2p-identify": "~0.7.2", - "lodash.includes": "^4.3.0", - "moving-average": "^1.0.0", - "multiaddr": "^5.0.0", - "multistream-select": "~0.14.2", - "once": "^1.4.0", - "peer-id": "~0.10.7", - "peer-info": "~0.14.1", - "pull-stream": "^3.6.7" - }, - "dependencies": { - "libp2p-crypto": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.12.1.tgz", - "integrity": "sha512-1/z8rxZ0DcQNreZhEsl7PnLr7DWOioSvYbKBLGkRwNRiNh1JJLgh0PdTySBb44wkrOGT+TxcGRd7iq3/X6Wxwg==", - "requires": { - "asn1.js": "^5.0.0", - "async": "^2.6.0", - "browserify-aes": "^1.1.1", - "bs58": "^4.0.1", - "keypair": "^1.0.1", - "libp2p-crypto-secp256k1": "~0.2.2", - "multihashing-async": "~0.4.7", - "node-forge": "^0.7.1", - "pem-jwk": "^1.5.1", - "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", - "tweetnacl": "^1.0.0" - } - }, - "peer-id": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.10.7.tgz", - "integrity": "sha512-VEpMFcL9q0NQijmR0jsj38OGbY4yzaWMEareVkDahopmlNT+Cpsot8btPgsgBBApP9NiZj2Enwvh8rZN30ocQw==", - "requires": { - "async": "^2.6.0", - "libp2p-crypto": "~0.12.1", - "lodash": "^4.17.5", - "multihashes": "~0.4.13" - } - }, - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8" - } - } - }, - "libp2p-websockets": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/libp2p-websockets/-/libp2p-websockets-0.12.0.tgz", - "integrity": "sha512-I4m0MNqzBOwoIneCF/5mXHGaavNf0Hoe/7NFg2WUm74o7240dZEIuNkAoLu1+OJyOPyu4RXeIBhUOS4cjBdCew==", - "requires": { - "class-is": "^1.1.0", - "interface-connection": "~0.3.2", - "lodash.includes": "^4.3.0", - "mafmt": "^6.0.0", - "pull-ws": "^3.3.1" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=" - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isfunction": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" - }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, - "lodash.max": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.max/-/lodash.max-4.0.1.tgz", - "integrity": "sha1-hzVWbGGLNan3YFILSHrnllivE2o=" - }, - "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" - }, - "lodash.padstart": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", - "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=" - }, - "lodash.range": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.range/-/lodash.range-3.2.0.tgz", - "integrity": "sha1-9GHliPZmg/fq3q3lE+OKaaVloV0=" - }, - "lodash.repeat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.1.0.tgz", - "integrity": "sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ=" - }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" - }, - "lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=" - }, - "lodash.values": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", - "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=" - }, - "mafmt": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/mafmt/-/mafmt-6.0.1.tgz", - "integrity": "sha512-towlQHptZPnx6wizscIxMABgILUBCKJBhoI0laX/9SUf6LtkoodsbNp3XY9Er8JgKRc8HgfnmEzDRpFvqtgAbQ==", - "requires": { - "multiaddr": "^5.0.0" - } - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "moving-average": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/moving-average/-/moving-average-1.0.0.tgz", - "integrity": "sha512-97cgMz0U2zciiDp4xRl/n+MYgrm9l7UiYbtsBLPr0rhw6KH3m4LyK2w4d96V6+UwKo+ph7KtQSoL2qgnqZVgvA==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multiaddr": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-5.0.0.tgz", - "integrity": "sha512-IMEo+iCv53MT8c/6SQWbJpJUEENTYr6qp7o635BKJLQG2nkxOIO9LSEFhF5e56Az+DkmI6HGAAjp69AT7Sjulw==", - "requires": { - "bs58": "^4.0.1", - "class-is": "^1.1.0", - "ip": "^1.1.5", - "ip-address": "^5.8.9", - "lodash.filter": "^4.6.0", - "lodash.map": "^4.6.0", - "varint": "^5.0.0", - "xtend": "^4.0.1" - } - }, - "multihashes": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-0.4.14.tgz", - "integrity": "sha512-V/g/EIN6nALXfS/xHUAgtfPP3mn3sPIF/i9beuGKf25QXS2QZYCpeVJbDPEannkz32B2fihzCe2D/KMrbcmefg==", - "requires": { - "bs58": "^4.0.1", - "varint": "^5.0.0" - } - }, - "multihashing-async": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/multihashing-async/-/multihashing-async-0.4.8.tgz", - "integrity": "sha512-LCc4lfxmTJOHKIjZjFNgvmfB6nXS/ErLInT9uwU8udFrRm2PH+aTPk3mfCREKmCiSHOlCWiv2O8rlnBx+OjlMw==", - "requires": { - "async": "^2.6.0", - "blakejs": "^1.1.0", - "js-sha3": "^0.7.0", - "multihashes": "~0.4.13", - "murmurhash3js": "^3.0.1", - "nodeify": "^1.0.1" - } - }, - "multistream-select": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/multistream-select/-/multistream-select-0.14.3.tgz", - "integrity": "sha512-Wu2ulJtUv5DWrilQ3I3rMRd+zdN8K+fZGX09UYfBGr9ZFLeiukCKvftkTiF6j7viCDNDS5VnjwVqwjrLwoS06g==", - "requires": { - "async": "^2.6.0", - "debug": "^3.1.0", - "interface-connection": "~0.3.2", - "lodash.isfunction": "^3.0.9", - "lodash.range": "^3.2.0", - "once": "^1.4.0", - "pull-handshake": "^1.1.4", - "pull-length-prefixed": "^1.3.1", - "pull-stream": "^3.6.7", - "semver": "^5.5.0", - "varint": "^5.0.0" - } - }, - "murmurhash3js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/murmurhash3js/-/murmurhash3js-3.0.1.tgz", - "integrity": "sha1-Ppg+W0fCoG9DpxMXTn5DXKBEuZg=" - }, - "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==" - }, - "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" - }, - "nodeify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", - "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", - "requires": { - "is-promise": "~1.0.0", - "promise": "~1.3.0" - } - }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "requires": { - "wordwrap": "~0.0.2" - } - }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, - "peer-book": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/peer-book/-/peer-book-0.8.0.tgz", - "integrity": "sha512-0An5viX2NnYeaqmwe2Vpzl03K9yxJ08mrktzkCPJyyd6rO4xz6QV2JK2Ku2vTHATP8Ag0ambxvr0QbrkT4UCYA==", - "requires": { - "bs58": "^4.0.1", - "peer-id": "^0.10.7", - "peer-info": "^0.14.1" - }, - "dependencies": { - "libp2p-crypto": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.12.1.tgz", - "integrity": "sha512-1/z8rxZ0DcQNreZhEsl7PnLr7DWOioSvYbKBLGkRwNRiNh1JJLgh0PdTySBb44wkrOGT+TxcGRd7iq3/X6Wxwg==", - "requires": { - "asn1.js": "^5.0.0", - "async": "^2.6.0", - "browserify-aes": "^1.1.1", - "bs58": "^4.0.1", - "keypair": "^1.0.1", - "libp2p-crypto-secp256k1": "~0.2.2", - "multihashing-async": "~0.4.7", - "node-forge": "^0.7.1", - "pem-jwk": "^1.5.1", - "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", - "tweetnacl": "^1.0.0" - } - }, - "peer-id": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.10.7.tgz", - "integrity": "sha512-VEpMFcL9q0NQijmR0jsj38OGbY4yzaWMEareVkDahopmlNT+Cpsot8btPgsgBBApP9NiZj2Enwvh8rZN30ocQw==", - "requires": { - "async": "^2.6.0", - "libp2p-crypto": "~0.12.1", - "lodash": "^4.17.5", - "multihashes": "~0.4.13" - } - }, - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8" - } - } - }, - "peer-id": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.11.0.tgz", - "integrity": "sha512-C/lRJk4CWIgOdKvfO572NvHbPcUwe49I6G0toIhDB5tCohqv/qzy0uBcAK9Ww8TvYI6U4J3C8ACShV9fWjNU4w==", - "requires": { - "async": "^2.6.1", - "libp2p-crypto": "~0.13.0", - "lodash": "^4.17.10", - "multihashes": "~0.4.13" - } - }, - "peer-info": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/peer-info/-/peer-info-0.14.1.tgz", - "integrity": "sha512-I9K+q7sisU0gg5ej6ekbhgolwlcm1tc2wDtLmumptoLYx0DkIT8WVHtgoTnupYwRRqcYADtwddFdiXfb8QFqzg==", - "requires": { - "lodash.uniqby": "^4.7.0", - "mafmt": "^6.0.0", - "multiaddr": "^4.0.0", - "peer-id": "~0.10.7" - }, - "dependencies": { - "libp2p-crypto": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.12.1.tgz", - "integrity": "sha512-1/z8rxZ0DcQNreZhEsl7PnLr7DWOioSvYbKBLGkRwNRiNh1JJLgh0PdTySBb44wkrOGT+TxcGRd7iq3/X6Wxwg==", - "requires": { - "asn1.js": "^5.0.0", - "async": "^2.6.0", - "browserify-aes": "^1.1.1", - "bs58": "^4.0.1", - "keypair": "^1.0.1", - "libp2p-crypto-secp256k1": "~0.2.2", - "multihashing-async": "~0.4.7", - "node-forge": "^0.7.1", - "pem-jwk": "^1.5.1", - "protons": "^1.0.1", - "rsa-pem-to-jwk": "^1.1.3", - "tweetnacl": "^1.0.0" - } - }, - "multiaddr": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-4.0.0.tgz", - "integrity": "sha512-zUatrOCfBd/tJNOSoJ10d2EI2FDXB9PyPZhqUMdXE9mOyR3C+HLuOjga2Ga/eChwvEHIpTYRMoIKF2Nv7af2qQ==", - "requires": { - "bs58": "^4.0.1", - "class-is": "^1.1.0", - "ip": "^1.1.5", - "ip-address": "^5.8.9", - "lodash.filter": "^4.6.0", - "lodash.map": "^4.6.0", - "varint": "^5.0.0", - "xtend": "^4.0.1" - } - }, - "peer-id": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.10.7.tgz", - "integrity": "sha512-VEpMFcL9q0NQijmR0jsj38OGbY4yzaWMEareVkDahopmlNT+Cpsot8btPgsgBBApP9NiZj2Enwvh8rZN30ocQw==", - "requires": { - "async": "^2.6.0", - "libp2p-crypto": "~0.12.1", - "lodash": "^4.17.5", - "multihashes": "~0.4.13" - } - }, - "webcrypto-shim": { - "version": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8", - "from": "github:dignifiedquire/webcrypto-shim#190bc9ec341375df6025b17ae12ddb2428ea49c8" - } - } - }, - "pem-jwk": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pem-jwk/-/pem-jwk-1.5.1.tgz", - "integrity": "sha1-eoY3/S9nqCflfAxC4cI8P9Us+wE=", - "requires": { - "asn1.js": "1.0.3" - }, - "dependencies": { - "asn1.js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-1.0.3.tgz", - "integrity": "sha1-KBuj7B8kSP52X5Kk7s+IP+E2S1Q=", - "requires": { - "bn.js": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "bn.js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-1.3.0.tgz", - "integrity": "sha1-DbTL+W+PI7dC9by50ap6mZSgXoM=", - "optional": true - } - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", - "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", - "requires": { - "is-promise": "~1" - } - }, - "protocol-buffers-schema": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz", - "integrity": "sha512-Xdayp8sB/mU+sUV4G7ws8xtYMGdQnxbeIfLjyO9TZZRJdztBGhlmbI5x1qcY4TG5hBkIKGnc28i7nXxaugu88w==" - }, - "protons": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/protons/-/protons-1.0.1.tgz", - "integrity": "sha512-+0ZKnfVs+4c43tbAQ5j0Mck8wPcLnlxUYzKQoB4iDW4ocdXGnN4P+0dDbgX1FTpoY9+7P2Tn2scJyHHqj+S/lQ==", - "requires": { - "protocol-buffers-schema": "^3.3.1", - "safe-buffer": "^5.1.1", - "signed-varint": "^2.0.1", - "varint": "^5.0.0" - } - }, - "pull-abortable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/pull-abortable/-/pull-abortable-4.1.1.tgz", - "integrity": "sha1-s61a77QRayWRbSbbiTk6yY0NzqE=" - }, - "pull-cat": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/pull-cat/-/pull-cat-1.1.11.tgz", - "integrity": "sha1-tkLdElXaN2pwa220+pYvX9t0wxs=" - }, - "pull-defer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/pull-defer/-/pull-defer-0.2.3.tgz", - "integrity": "sha512-/An3KE7mVjZCqNhZsr22k1Tx8MACnUnHZZNPSJ0S62td8JtYr/AiRG42Vz7Syu31SoTLUzVIe61jtT/pNdjVYA==" - }, - "pull-handshake": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/pull-handshake/-/pull-handshake-1.1.4.tgz", - "integrity": "sha1-YACg/QGIhM39c3JU+Mxgqypjd5E=", - "requires": { - "pull-cat": "^1.1.9", - "pull-pair": "~1.1.0", - "pull-pushable": "^2.0.0", - "pull-reader": "^1.2.3" - } - }, - "pull-length-prefixed": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pull-length-prefixed/-/pull-length-prefixed-1.3.1.tgz", - "integrity": "sha512-Ho0KoVKOILITGPusghadRVcUzflFHAHcv1Hvi/OkUSJLkGK2LNmVjsmIaJbWkizI//okIj2n376JyTFwCWdsYA==", - "requires": { - "pull-pushable": "^2.0.1", - "pull-reader": "^1.3.0", - "safe-buffer": "^5.0.1", - "varint": "^5.0.0" - } - }, - "pull-pair": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pull-pair/-/pull-pair-1.1.0.tgz", - "integrity": "sha1-fuQnJj/fTaglOXrAoF4atLdL120=" - }, - "pull-pushable": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pull-pushable/-/pull-pushable-2.2.0.tgz", - "integrity": "sha1-Xy867UethpGfAbEqLpnW8b13ZYE=" - }, - "pull-reader": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pull-reader/-/pull-reader-1.3.1.tgz", - "integrity": "sha512-CBkejkE5nX50SiSEzu0Qoz4POTJMS/mw8G6aj3h3M/RJoKgggLxyF0IyTZ0mmpXFlXRcLmLmIEW4xeYn7AeDYw==" - }, - "pull-stream": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.9.tgz", - "integrity": "sha512-hJn4POeBrkttshdNl0AoSCVjMVSuBwuHocMerUdoZ2+oIUzrWHFTwJMlbHND7OiKLVgvz6TFj8ZUVywUMXccbw==" - }, - "pull-ws": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/pull-ws/-/pull-ws-3.3.1.tgz", - "integrity": "sha512-kJodbLQT+oKjcRIQO+vQNw6xWBuEo7Kxp51VMOvb6cvPvHYA+aNLzm+NmkB/5dZwbuTRYGMal9QPvH52tzM1ZA==", - "requires": { - "relative-url": "^1.0.2", - "safe-buffer": "^5.1.1", - "ws": "^1.1.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "relative-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/relative-url/-/relative-url-1.0.2.tgz", - "integrity": "sha1-0hxSpy1gYQGLzun5yfwQa/fWUoc=" - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rsa-pem-to-jwk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rsa-pem-to-jwk/-/rsa-pem-to-jwk-1.1.3.tgz", - "integrity": "sha1-JF52vbfnI0z+58oDLTG1TDj6uY4=", - "requires": { - "object-assign": "^2.0.0", - "rsa-unpack": "0.0.6" - } - }, - "rsa-unpack": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/rsa-unpack/-/rsa-unpack-0.0.6.tgz", - "integrity": "sha1-9Q69VqYoN45jHylxYQJs6atO3bo=", - "requires": { - "optimist": "~0.3.5" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "secp256k1": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.5.2.tgz", - "integrity": "sha512-iin3kojdybY6NArd+UFsoTuapOF7bnJNf2UbcWXaY3z+E1sJDipl60vtzB5hbO/uquBu7z0fd4VC4Irp+xoFVQ==", - "requires": { - "bindings": "^1.2.1", - "bip66": "^1.1.3", - "bn.js": "^4.11.3", - "create-hash": "^1.1.2", - "drbg.js": "^1.0.1", - "elliptic": "^6.2.3", - "nan": "^2.2.1", - "safe-buffer": "^5.1.0" - } - }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "signed-varint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz", - "integrity": "sha1-UKmYnafJjCxh2tEZvJdHDvhSgSk=", - "requires": { - "varint": "~5.0.0" - } - }, - "sprintf-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.0.tgz", - "integrity": "sha1-z/yvcC2vZeo5u04PorKZzsGhvkY=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "time-cache": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/time-cache/-/time-cache-0.3.0.tgz", - "integrity": "sha1-7Q388P2kXNyV+9YB/agw6/G9XYs=", - "requires": { - "lodash.throttle": "^4.1.1" - } - }, - "timed-tape": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/timed-tape/-/timed-tape-0.1.1.tgz", - "integrity": "sha1-m25WnxfmbHnx7tLSX/eWL8dBjkk=" - }, - "topo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", - "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", - "requires": { - "hoek": "5.x.x" - } - }, - "tweetnacl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", - "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" - }, - "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "varint": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz", - "integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8=" - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - } - } -} diff --git a/package.json b/package.json index a1afe40c..e23541c9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,31 @@ "lint" ], "dependencies": { - "libp2p": "~0.23.1", - "libp2p-floodsub": "~0.15.0" + "async": "^2.6.2", + "err-code": "^1.1.2", + "libp2p": "~0.25.0-rc.5", + "libp2p-floodsub": "~0.15.8", + "libp2p-pubsub": "~0.1.0", + "libp2p-switch": "~0.42.2", + "peer-id": "~0.12.2", + "peer-info": "~0.15.1", + "protons": "^1.0.1", + "pull-length-prefixed": "^1.3.2", + "pull-stream": "^3.6.9" + }, + "devDependencies": { + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.6", + "aegir": "^18.1.0", + "chai": "^4.2.0", + "chai-spies": "^1.0.0", + "dirty-chai": "^2.0.1", + "libp2p-secio": "~0.11.1", + "libp2p-spdy": "~0.13.1", + "libp2p-tcp": "~0.13.0", + "lodash": "^4.17.11", + "mocha": "^5.2.0", + "promisify-es6": "^1.0.3", + "sinon": "^7.3.2" } } diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..b636e134 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,24 @@ +'use strict' + +const second = exports.second = 1000 +const minute = exports.minute = 60 * second + +// Protocol identifiers +exports.FloodSubID = '/floodsub/1.0.0' +exports.GossipSubID = '/meshsub/1.0.0' + +// Overlay parameters +exports.GossipSubD = 6 +exports.GossipSubDlo = 4 +exports.GossipSubDhi = 12 + +// Gossip parameters +exports.GossipSubHistoryLength = 5 +exports.GossipSubHistoryGossip = 3 + +// Heartbeat interval +exports.GossipSubHeartbeatInitialDelay = 100 / second +exports.GossipSubHeartbeatInterval = second + +// Fanout ttl +exports.GossipSubFanoutTTL = minute diff --git a/src/heartbeat.js b/src/heartbeat.js new file mode 100644 index 00000000..4163e4a6 --- /dev/null +++ b/src/heartbeat.js @@ -0,0 +1,167 @@ +'use strict' + +const constants = require('./constants') +const errcode = require('err-code') + +class Heartbeat { + /** + * @param {Object} gossipsub + * @constructor + */ + constructor (gossipsub) { + this.gossipsub = gossipsub + } + + start (callback) { + if (this._heartbeatTimer) { + const errMsg = 'Heartbeat timer is already running' + this.gossipsub.log(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_HEARTBEAT_ALREADY_RUNNING')) + } + + const heartbeatTimer = { + _onCancel: null, + _timeoutId: null, + runPeriodically: (fn, period) => { + heartbeatTimer._timeoutId = setInterval(fn, period) + }, + cancel: (cb) => { + clearTimeout(heartbeatTimer._timeoutId) + cb() + } + } + + const heartbeat = this._heartbeat.bind(this) + setTimeout(() => { + heartbeat() + heartbeatTimer.runPeriodically(heartbeat, constants.GossipSubHeartbeatInterval) + }, constants.GossipSubHeartbeatInitialDelay) + + this._heartbeatTimer = heartbeatTimer + callback() + } + + /** + * Unmounts the gossipsub protocol and shuts down every connection + * + * @override + * @param {Function} callback + * @returns {void} + */ + stop (callback) { + if (!this._heartbeatTimer) { + const errMsg = 'Heartbeat timer is not running' + this.gossipsub.log(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_HEARTBEAT_NO_RUNNING')) + } + this._heartbeatTimer.cancel(() => { + this._heartbeatTimer = null + callback() + }) + } + + /** + * Maintains the mesh and fanout maps in gossipsub. + * + * @returns {void} + */ + _heartbeat () { + // flush pending control message from retries and gossip + // that hasn't been piggybacked since the last heartbeat + this.gossipsub._flush() + + /** + * @type {Map>} + */ + const tograft = new Map() + const toprune = new Map() + + // maintain the mesh for topics we have joined + this.gossipsub.mesh.forEach((peers, topic) => { + // do we have enough peers? + if (peers.size < constants.GossipSubDlo) { + const ineed = constants.GossipSubD - peers.size + const peersSet = this.gossipsub._getPeers(topic, ineed) + peersSet.forEach((peer) => { + // add topic peers not already in mesh + if (peers.has(peer)) { + return + } + + this.gossipsub.log('HEARTBEAT: Add mesh link to %s in %s', peer.info.id.toB58String(), topic) + peers.add(peer) + const peerGrafts = tograft.get(peer) + if (!peerGrafts) { + tograft.set(peer, [topic]) + } else { + peerGrafts.push(topic) + } + }) + } + + // do we have to many peers? + if (peers.size > constants.GossipSubDhi) { + const idontneed = peers.size - constants.GossipSubD + let peersArray = Array.from(peers) + peersArray = this.gossipsub._shufflePeers(peersArray) + peersArray = peersArray.slice(0, idontneed) + + peersArray.forEach((peer) => { + this.gossipsub.log('HEARTBEAT: Remove mesh link to %s in %s', peer.info.id.toB58String(), topic) + peers.delete(peer) + const peerPrunes = toprune.get(peer) + if (!peerPrunes) { + toprune.set(peer, [topic]) + } else { + peerPrunes.push(topic) + } + }) + } + + this.gossipsub._emitGossip(topic, peers) + }) + + // expire fanout for topics we haven't published to in a while + const now = this.gossipsub._now() + this.gossipsub.lastpub.forEach((topic, lastpb) => { + if ((lastpb + constants.GossipSubFanoutTTL) < now) { + this.gossipsub.fanout.delete(topic) + this.gossipsub.lastpub.delete(topic) + } + }) + + // maintain our fanout for topics we are publishing but we have not joined + this.gossipsub.fanout.forEach((topic, peers) => { + // checks whether our peers are still in the topic + peers.forEach((peer) => { + if (this.gossipsub.topics.has(peer)) { + peers.delete(peer) + } + }) + + // do we need more peers? + if (peers.size < constants.GossipSubD) { + const ineed = constants.GossipSubD - peers.size + const peersSet = this.gossipsub._getPeers(topic, ineed) + peersSet.forEach((peer) => { + if (!peers.has(peer)) { + return + } + + peers.add(peer) + }) + } + + this.gossipsub._emitGossip(topic, peers) + }) + // send coalesced GRAFT/PRUNE messages (will piggyback gossip) + this.gossipsub._sendGraftPrune(tograft, toprune) + + // advance the message history window + this.gossipsub.messageCache.shift() + + this.gossipsub.emit('gossipsub:heartbeat') + } +} + +module.exports = Heartbeat diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..8eff702e --- /dev/null +++ b/src/index.js @@ -0,0 +1,591 @@ +'use strict' + +const assert = require('assert') +const { utils } = require('libp2p-pubsub') + +const BasicPubsub = require('./pubsub') +const { MessageCache } = require('./messageCache') + +const { rpc } = require('./message') +const constants = require('./constants') +const Heartbeat = require('./heartbeat') + +class GossipSub extends BasicPubsub { + /** + * @param {Object} libp2p + * @constructor + */ + constructor (libp2p) { + super('libp2p:gossipsub', constants.GossipSubID, libp2p) + + /** + * Map of topic meshes + * + * @type {Map>} + */ + this.mesh = new Map() + + /** + * Map of topics to set of peers. These mesh peers are the ones to which we are publishing without a topic membership + * + * @type {Map>} + */ + this.fanout = new Map() + + /** + * Map of last publish time for fanout topics + * + * @type {Map} + */ + this.lastpub = new Map() + + /** + * Map of pending messages to gossip + * + * @type {Map> } + */ + this.gossip = new Map() + + /** + * Map of control messages + * + * @type {Map} + */ + this.control = new Map() + + /** + * A message cache that contains the messages for last few hearbeat ticks + * + */ + this.messageCache = new MessageCache(constants.GossipSubHistoryGossip, constants.GossipSubHistoryLength) + + /** + * A heartbeat timer that maintains the mesh + */ + this.heartbeat = new Heartbeat(this) + } + + /** + * Removes a peer from the router + * + * @override + * @param {Peer} peer + * @returns {PeerInfo} + */ + _removePeer (peer) { + super._removePeer(peer) + // Only delete when no one else if referencing this peer. + if (peer._references === 0) { + // Remove this peer from the mesh + // eslint-disable-next-line no-unused-vars + for (const [_, peers] of this.mesh.entries()) { + peers.delete(peer) + } + // Remove this peer from the fanout + // eslint-disable-next-line no-unused-vars + for (const [_, peers] of this.fanout.entries()) { + peers.delete(peer) + } + + // Remove from gossip mapping + this.gossip.delete(peer) + // Remove from control mapping + this.control.delete(peer) + } + return peer + } + + /** + * Handles an rpc control message from a peer + * + * @param {Peer} peer + * @param {rpc.RPC} rpc + * @returns {void} + */ + _handleRpcControl (peer, rpc) { + const controlMsg = rpc.control + + if (!controlMsg) { + return + } + + const iWant = this._handleIHave(peer, controlMsg.ihave) + const iHave = this._handleIWant(peer, controlMsg.iwant) + const prune = this._handleGraft(peer, controlMsg.graft) + this._handlePrune(peer, controlMsg.prune) + + if (!iWant || !iHave || !prune) { + return + } + + const outRpc = this._rpcWithControl(iHave, null, iWant, null, prune) + this._sendRpc(rpc.from, outRpc) + } + + /** + * Process incoming message, + * emitting locally and forwarding on to relevant floodsub and gossipsub peers + * @param {rpc.RPC.Message} msg + */ + _processRpcMessage (msg) { + super._processRpcMessage(msg) + const topics = msg.topicIDs + + // Emit to floodsub peers + this.peers.forEach((peer) => { + if (peer.info.protocols.has(constants.FloodSubID) && + peer.info.id.toB58String() !== msg.from && + utils.anyMatch(peer.topics, topics) && + peer.isWritable + ) { + peer.sendMessages(utils.normalizeOutRpcMessages([msg])) + this.log('publish msg on topics - floodsub', topics, peer.info.id.toB58String()) + } + }) + + // Emit to peers in the mesh + topics.forEach((topic) => { + if (!this.mesh.has(topic)) { + return + } + this.mesh.get(topic).forEach((peer) => { + if (!peer.isWritable || peer.info.id.toB58String() === msg.from) { + return + } + peer.sendMessages(utils.normalizeOutRpcMessages([msg])) + this.log('publish msg on topic - meshsub', topic, peer.info.id.toB58String()) + }) + }) + } + + /** + * Handles IHAVE messages + * + * @param {Peer} peer + * @param {Array} ihave + * + * @returns {rpc.RPC.ControlIWant} + */ + _handleIHave (peer, ihave) { + const iwant = new Set() + + ihave.forEach(({ topicID, messageIDs }) => { + if (!this.mesh.has(topicID)) { + return + } + + messageIDs.forEach((msgID) => { + if (this.seenCache.has(msgID)) { + return + } + iwant.add(msgID) + }) + }) + + if (!iwant.size) { + return + } + + this.log('IHAVE: Asking for %d messages from %s', iwant.size, peer.info.id.toB58String()) + + return { + messageIDs: Array.from(iwant) + } + } + + /** + * Handles IWANT messages + * Returns messages to send back to peer + * + * @param {Peer} peer + * @param {Array} iwant + * + * @returns {Array} + */ + _handleIWant (peer, iwant) { + // @type {Map} + const ihave = new Map() + + iwant.forEach(({ messageIDs }) => { + messageIDs.forEach((msgID) => { + const msg = this.messageCache.get(msgID) + if (msg) { + ihave.set(msgID, msg) + } + }) + }) + + if (!ihave.size) { + return + } + + this.log('IWANT: Sending %d messages to %s', ihave.size, peer.info.id.toB58String()) + + return Array.from(ihave.values()) + } + + /** + * Handles Graft messages + * + * @param {Peer} peer + * @param {Array} graft + * + * @return {Array} + * + */ + _handleGraft (peer, graft) { + const prune = [] + + graft.forEach(({ topicID }) => { + const peers = this.mesh.get(topicID) + if (!peers) { + prune.push(topicID) + } else { + this.log('GRAFT: Add mesh link from %s in %s', peer.info.id.toB58String(), topicID) + peers.add(peer) + peer.topics.add(topicID) + this.mesh.set(topicID, peers) + } + }) + + if (!prune.length) { + return + } + + const buildCtrlPruneMsg = (topic) => { + return { + topicID: topic + } + } + + return prune.map(buildCtrlPruneMsg) + } + + /** + * Handles Prune messages + * + * @param {Peer} peer + * @param {Array} prune + * + * @returns {void} + * + */ + _handlePrune (peer, prune) { + prune.forEach(({ topicID }) => { + const peers = this.mesh.get(topicID) + if (peers) { + this.log('PRUNE: Remove mesh link to %s in %s', peer.info.id.toB58String(), topicID) + peers.delete(peer) + peer.topics.delete(topicID) + } + }) + } + + /** + * Mounts the gossipsub protocol onto the libp2p node and sends our + * our subscriptions to every peer connected + * + * @override + * @param {Function} callback + * @returns {void} + * + */ + start (callback) { + super.start((err) => { + if (err) return callback(err) + this.heartbeat.start(callback) + }) + } + + /** + * Unmounts the gossipsub protocol and shuts down every connection + * + * @override + * @param {Function} callback + * @returns {void} + */ + stop (callback) { + super.stop((err) => { + if (err) return callback(err) + this.mesh = new Map() + this.fanout = new Map() + this.lastpub = new Map() + this.gossip = new Map() + this.control = new Map() + this.heartbeat.stop(callback) + }) + } + + /** + * Join topics + * @param {Array|string} topics + * @returns {void} + */ + join (topics) { + assert(this.started, 'GossipSub has not started') + topics = utils.ensureArray(topics) + + this.log('JOIN %s', topics) + + topics.forEach((topic) => { + // Send GRAFT to mesh peers + const fanoutPeers = this.fanout.get(topic) + if (fanoutPeers) { + this.mesh.set(topic, fanoutPeers) + this.fanout.delete(topic) + this.lastpub.delete(topic) + } else { + const peers = this._getPeers(topic, constants.GossipSubD) + this.mesh.set(topic, peers) + } + this.mesh.get(topic).forEach((peer) => { + this.log('JOIN: Add mesh link to %s in %s', peer.info.id.toB58String(), topic) + this._sendGraft(peer, topic) + }) + }) + } + + /** + * Leave topics + * + * @param {Array|string} topics + * @returns {void} + */ + leave (topics) { + topics = utils.ensureArray(topics) + + this.log('LEAVE %s', topics) + + topics.forEach((topic) => { + // Send PRUNE to mesh peers + const meshPeers = this.mesh.get(topic) + if (meshPeers) { + meshPeers.forEach((peer) => { + this.log('LEAVE: Remove mesh link to %s in %s', peer.info.id.toB58String(), topic) + this._sendPrune(peer, topic) + }) + this.mesh.delete(topic) + } + }) + } + + _publish (messages) { + messages.forEach((msgObj) => { + this.messageCache.put(msgObj) + // @type Set + const tosend = new Set() + msgObj.topicIDs.forEach((topic) => { + const peersInTopic = this.topics.get(topic) + if (!peersInTopic) { + return + } + + // floodsub peers + peersInTopic.forEach((peer) => { + if (peer.info.protocols.has(constants.FloodSubID)) { + tosend.add(peer) + } + }) + + // Gossipsub peers handling + let meshPeers = this.mesh.get(topic) + if (!meshPeers) { + // We are not in the mesh for topic, use fanout peers + meshPeers = this.fanout.get(topic) + if (!meshPeers) { + // If we are not in the fanout, then pick any peers in topic + const peers = this._getPeers(topic, constants.GossipSubD) + + if (peers.size > 0) { + meshPeers = peers + this.fanout.set(topic, peers) + } else { + meshPeers = [] + } + } + // Store the latest publishing time + this.lastpub.set(topic, this._now()) + } + + meshPeers.forEach((peer) => { + tosend.add(peer) + }) + }) + // Publish messages to peers + tosend.forEach((peer) => { + if (peer.info.id.toB58String() === msgObj.from) { + return + } + this._sendRpc(peer, { msgs: [msgObj] }) + }) + }) + } + + /** + * Sends a GRAFT message to a peer + * + * @param {Peer} peer + * @param {String} topic + * @returns {void} + */ + _sendGraft (peer, topic) { + const graft = [{ + topicID: topic + }] + + const out = this._rpcWithControl(null, null, null, graft, null) + this._sendRpc(peer, out) + } + + /** + * Sends a PRUNE message to a peer + * + * @param {Peer} peer + * @param {String} topic + * @returns {void} + */ + _sendPrune (peer, topic) { + const prune = [{ + topicID: topic + }] + + const out = this._rpcWithControl(null, null, null, null, prune) + this._sendRpc(peer, out) + } + + _sendRpc (peer, outRpc) { + if (!peer || !peer.isWritable) { + return + } + + // piggyback control message retries + const ctrl = this.control.get(peer) + if (ctrl) { + this._piggybackControl(peer, outRpc, ctrl) + this.control.delete(peer) + } + + // piggyback gossip + const ihave = this.gossip.get(peer) + if (ihave) { + this._piggybackGossip(peer, outRpc, ihave) + this.gossip.delete(peer) + } + + peer.write(rpc.RPC.encode(outRpc)) + } + + _piggybackControl (peer, outRpc, ctrl) { + const tograft = (ctrl.graft || []) + .filter(({ topicID }) => (this.mesh.get(topicID) || new Set()).has(peer)) + const toprune = (ctrl.prune || []) + .filter(({ topicID }) => !(this.mesh.get(topicID) || new Set()).has(peer)) + + if (!tograft.length && !toprune.length) { + return + } + + outRpc.control = outRpc.control || {} + outRpc.control.graft = (outRpc.control.graft || []).concat(tograft) + outRpc.control.prune = (outRpc.control.prune || []).concat(toprune) + } + + _piggybackGossip (peer, outRpc, ihave) { + outRpc.control = outRpc.control || {} + outRpc.control.ihave = ihave + } + + /** + * Send graft and prune messages + * + * @param {Map>} tograft + * @param {Map>} toprune + */ + _sendGraftPrune (tograft, toprune) { + for (const [p, topics] of tograft) { + const graft = topics.map((topicID) => ({ topicID })) + let prune = null + // If a peer also has prunes, process them now + const pruneMsg = toprune.get(p) + if (pruneMsg) { + prune = pruneMsg.map((topicID) => ({ topicID })) + toprune.delete(p) + } + + const outRpc = this._rpcWithControl(null, null, null, graft, prune) + this._sendRpc(p, outRpc) + } + for (const [p, topics] of toprune) { + const prune = topics.map((topicID) => ({ topicID })) + const outRpc = this._rpcWithControl(null, null, null, null, prune) + this._sendRpc(p, outRpc) + } + } + + /** + * Emits gossip to peers in a particular topic + * + * @param {String} topic + * @param {Set} peers - peers to exclude + * @returns {void} + */ + _emitGossip (topic, peers) { + const messageIDs = this.messageCache.getGossipIDs(topic) + if (!messageIDs.length) { + return + } + + const gossipSubPeers = this._getPeers(topic, constants.GossipSubD) + gossipSubPeers.forEach((peer) => { + // skip mesh peers + if (!peers.has(peer)) { + this._pushGossip(peer, { + topicID: topic, + messageIDs: messageIDs + }) + } + }) + } + + /** + * Flush gossip and control messages + */ + _flush () { + // send gossip first, which will also piggyback control + for (const [peer, ihave] of this.gossip.entries()) { + this.gossip.delete(peer) + const out = this._rpcWithControl(null, ihave, null, null, null) + this._sendRpc(peer, out) + } + // send the remaining control messages + for (const [peer, control] of this.control.entries()) { + this.control.delete(peer) + const out = this._rpcWithControl(null, null, null, control.graft, control.prune) + this._sendRpc(peer, out) + } + } + + /** + * Adds new IHAVE messages to pending gossip + * + * @param {Peer} peer + * @param {Array} controlIHaveMsgs + * @returns {void} + */ + _pushGossip (peer, controlIHaveMsgs) { + this.log('Add gossip to %s', peer.info.id.toB58String()) + const gossip = this.gossip.get(peer) || [] + this.gossip.set(peer, gossip.concat(controlIHaveMsgs)) + } + + /** + * Returns the current time in milliseconds + * + * @returns {number} + */ + _now () { + return Date.now() + } +} + +module.exports = GossipSub diff --git a/src/message/index.js b/src/message/index.js new file mode 100644 index 00000000..ed860a60 --- /dev/null +++ b/src/message/index.js @@ -0,0 +1,10 @@ +'use strict' + +const protons = require('protons') + +const rpcProto = protons(require('./rpc.proto.js')) +const topicDescriptorProto = protons(require('./topic-descriptor.proto.js')) + +exports = module.exports +exports.rpc = rpcProto +exports.td = topicDescriptorProto diff --git a/src/message/rpc.proto.js b/src/message/rpc.proto.js new file mode 100644 index 00000000..636ff1a7 --- /dev/null +++ b/src/message/rpc.proto.js @@ -0,0 +1,45 @@ +'use strict' +module.exports = ` +message RPC { + repeated SubOpts subscriptions = 1; + repeated Message msgs = 2; + optional ControlMessage control = 3; + + message SubOpts { + optional bool subscribe = 1; // subscribe or unsubcribe + optional string topicID = 2; + } + + message Message { + optional bytes from = 1; + optional bytes data = 2; + optional bytes seqno = 3; + repeated string topicIDs = 4; + optional bytes signature = 5; + optional bytes key = 6; + } + + message ControlMessage { + repeated ControlIHave ihave = 1; + repeated ControlIWant iwant = 2; + repeated ControlGraft graft = 3; + repeated ControlPrune prune = 4; + } + + message ControlIHave { + optional string topicID = 1; + repeated string messageIDs = 2; + } + + message ControlIWant { + repeated string messageIDs = 1; + } + + message ControlGraft { + optional string topicID = 1; + } + + message ControlPrune { + optional string topicID = 1; + } +}` diff --git a/src/message/topic-descriptor.proto.js b/src/message/topic-descriptor.proto.js new file mode 100644 index 00000000..482c22f1 --- /dev/null +++ b/src/message/topic-descriptor.proto.js @@ -0,0 +1,29 @@ +'use strict' +module.exports = ` +message TopicDescriptor { + optional string name = 1; + optional AuthOpts auth = 2; + optional EncOpts enc = 3; + + message AuthOpts { + optional AuthMode mode = 1; + repeated bytes keys = 2; // root keys to trust + + enum AuthMode { + NONE = 0; // no authentication, anyone can publish + KEY = 1; // only messages signed by keys in the topic descriptor are accepted + WOT = 2; // web of trust, certificates can allow publisher set to grow + } + } + + message EncOpts { + optional EncMode mode = 1; + repeated bytes keyHashes = 2; // the hashes of the shared keys used (salted) + + enum EncMode { + NONE = 0; // no encryption, anyone can read + SHAREDKEY = 1; // messages are encrypted with shared key + WOT = 2; // web of trust, certificates can allow publisher set to grow + } + } +}` diff --git a/src/messageCache.js b/src/messageCache.js new file mode 100644 index 00000000..2a76712e --- /dev/null +++ b/src/messageCache.js @@ -0,0 +1,109 @@ +'use strict' + +const { utils } = require('libp2p-pubsub') + +class CacheEntry { + /** + * @param {String} msgID + * @param {Array} topics + * + * @constructor + */ + constructor (msgID, topics) { + this.msgID = msgID + this.topics = topics + } +} + +class MessageCache { + /** + * @param {Number} gossip + * @param {Number} history + * + * @constructor + */ + constructor (gossip, history) { + /** + * @type {Map} + */ + this.msgs = new Map() + + /** + * @type {Array>} + */ + this.history = [] + for (let i = 0; i < history; i++) { + this.history[i] = [] + } + + /** + * @type {Number} + */ + this.gossip = gossip + } + + /** + * Adds a message to the current window and the cache + * + * @param {rpc.RPC.Message} msg + * @returns {void} + */ + put (msg) { + const msgID = utils.msgId(msg.from, msg.seqno) + this.msgs.set(msgID, msg) + this.history[0].push(new CacheEntry(msgID, msg.topicIDs)) + } + + /** + * Retrieves a message from the cache by its ID, if it is still present + * + * @param {String} msgID + * @returns {rpc.RPC.Message} + */ + get (msgID) { + return this.msgs.get(msgID) + } + + /** + * Retrieves a list of message IDs for a given topic + * + * @param {String} topic + * + * @returns {Array} + */ + getGossipIDs (topic) { + const msgIDs = [] + for (let i = 0; i < this.gossip; i++) { + this.history[i].forEach((entry) => { + for (const t of entry.topics) { + if (t === topic) { + msgIDs.push(entry.msgID) + break + } + } + }) + } + + return msgIDs + } + + /** + * Shifts the current window, discarding messages older than this.history.length of the cache + * + * @returns {void} + */ + shift () { + const last = this.history[this.history.length - 1] + last.forEach((entry) => { + this.msgs.delete(entry.msgID) + }) + + this.history.pop() + this.history.unshift([]) + } +} + +module.exports = { + CacheEntry, + MessageCache +} diff --git a/src/pubsub.js b/src/pubsub.js new file mode 100644 index 00000000..0e72473e --- /dev/null +++ b/src/pubsub.js @@ -0,0 +1,367 @@ +'use strict' + +const Pubsub = require('libp2p-pubsub') +const pull = require('pull-stream') +const lp = require('pull-length-prefixed') +const nextTick = require('async/nextTick') +const { utils } = require('libp2p-pubsub') +const asyncMap = require('async/map') +const errcode = require('err-code') + +const assert = require('assert') + +const { rpc } = require('./message') + +class BasicPubSub extends Pubsub { + /** + * @param {String} debugName + * @param {String} multicodec + * @param {Object} libp2p libp2p implementation + * @constructor + */ + constructor (debugName, multicodec, libp2p) { + super(debugName, multicodec, libp2p) + /** + * A set of subscriptions + */ + this.subscriptions = new Set() + } + + /** + * When a peer has dialed into another peer, it sends its subscriptions to it. + * @override + * @param {PeerInfo} peerInfo + * @param {Connection} conn + * @param {Function} callback + * + * @returns {void} + * + */ + _onDial (peerInfo, conn, callback) { + super._onDial(peerInfo, conn, (err) => { + if (err) return callback(err) + const idB58Str = peerInfo.id.toB58String() + const peer = this.peers.get(idB58Str) + if (peer && peer.isWritable) { + // Immediately send my own subscription to the newly established conn + peer.sendSubscriptions(this.subscriptions) + } + nextTick(() => callback()) + }) + } + + /** + * Processes a peer's connection to another peer. + * + * @param {String} idB58Str + * @param {Connection} conn + * @param {Peer} peer + * + * @returns {void} + * + */ + _processConnection (idB58Str, conn, peer) { + pull( + conn, + lp.decode(), + pull.map((data) => rpc.RPC.decode(data)), + pull.drain( + (rpc) => this._onRpc(idB58Str, rpc), + (err) => this._onConnectionEnd(idB58Str, peer, err) + ) + ) + } + + /** + * Handles an rpc request from a peer + * + * @param {String} idB58Str + * @param {Object} rpc + * @returns {void} + */ + _onRpc (idB58Str, rpc) { + if (!rpc) { + return + } + + const peer = this.peers.get(idB58Str) + if (!peer) { + return + } + + this.log('rpc from', idB58Str) + const subs = rpc.subscriptions + const msgs = rpc.msgs + + if (subs.length) { + // update peer subscriptions + peer.updateSubscriptions(subs) + subs.forEach((subOptMsg) => { + const t = subOptMsg.topicID + + if (!this.topics.has(t)) { + this.topics.set(t, new Set()) + } + + const topicSet = this.topics.get(t) + if (subOptMsg.subscribe) { + // subscribe peer to new topic + topicSet.add(peer) + } else { + // unsubscribe from existing topic + topicSet.delete(peer) + } + }) + this.emit('pubsub:subscription-change', peer.info, peer.topics, subs) + } + + if (msgs.length) { + utils.normalizeInRpcMessages(msgs).forEach((msg) => { + const seqno = utils.msgId(msg.from, msg.seqno) + if (!this.seenCache.has(seqno)) { + this._processRpcMessage(msg) + this.seenCache.put(seqno) + } + }) + } + this._handleRpcControl(peer, rpc) + } + + /** + * @param {rpc.RPC.Message} msg + */ + _processRpcMessage (msg) { + // Emit to self + this._emitMessage(msg.topicIDs, msg) + } + + _emitMessage (topics, message) { + topics.forEach((topic) => { + if (this.subscriptions.has(topic)) { + this.emit(topic, message) + } + }) + } + + _handleRpcControl (peer, rpc) { + throw errcode('_handleRpcControl must be implemented by the subclass', 'ERR_NOT_IMPLEMENTED') + } + + /** + * Returns a buffer of a RPC message that contains a control message + * + * @param {Array} msgs + * @param {Array} ihave + * @param {Array} iwant + * @param {Array} graft + * @param {Array} prune + * + * @returns {rpc.RPC} + * + */ + _rpcWithControl (msgs, ihave, iwant, graft, prune) { + return { + subscriptions: [], + msgs: msgs || [], + control: { + ihave: ihave || [], + iwant: iwant || [], + graft: graft || [], + prune: prune || [] + } + } + } + + /** + * Mounts the protocol onto the libp2p node and sends our + * our subscriptions to every peer connected + * + * @override + * @param {Function} callback + * @returns {void} + * + */ + start (callback) { + super.start((err) => { + if (err) { + return callback(err) + } + callback() + }) + } + + /** + * Unmounts the protocol and shuts down every connection + * + * @override + * @param {Function} callback + * @returns {void} + */ + stop (callback) { + super.stop((err) => { + if (err) return callback(err) + this.subscriptions = new Set() + callback() + }) + } + + /** + * Subscribes to topics + * @param {Array|string} topics + * @returns {void} + */ + subscribe (topics) { + assert(this.started, 'Pubsub has not started') + + topics = utils.ensureArray(topics) + + const newTopics = topics.filter((topic) => !this.subscriptions.has(topic)) + if (newTopics.length === 0) { + return + } + + // set subscriptions + newTopics.forEach((topic) => { + this.subscriptions.add(topic) + }) + + // Broadcast SUBSCRIBE to all peers + this.peers.forEach((peer) => { + peer.sendSubscriptions(newTopics) + }) + + this.join(newTopics) + } + + join (topics) { + throw errcode('join must be implemented by the subclass', 'ERR_NOT_IMPLEMENTED') + } + + /** + * Leaves a topic + * + * @param {Array|string} topics + * @returns {void} + */ + unsubscribe (topics) { + topics = utils.ensureArray(topics) + + const unTopics = topics.filter((topic) => this.subscriptions.has(topic)) + if (!unTopics.length) { + return + } + + // delete subscriptions + unTopics.forEach((topic) => { + this.subscriptions.delete(topic) + }) + + // Broadcast UNSUBSCRIBE to all peers + this.peers.forEach((peer) => { + peer.sendUnsubscriptions(topics) + }) + + this.leave(unTopics) + } + + leave (topics) { + throw errcode('leave must be implemented by the subclass', 'ERR_NOT_IMPLEMENTED') + } + + /** + * Publishes messages to all subscribed peers + * + * @param {Array|string} topics + * @param {Array|any} messages + * @param {Function|null} callback + * @returns {void} + */ + publish (topics, messages, callback) { + assert(this.started, 'Pubsub has not started') + this.log('publish', topics, messages) + topics = utils.ensureArray(topics) + messages = utils.ensureArray(messages) + callback = callback || (() => {}) + + const from = this.libp2p.peerInfo.id.toB58String() + + const buildMessage = (msg, cb) => { + const seqno = utils.randomSeqno() + const msgObj = { + from: from, + data: msg, + seqno: seqno, + topicIDs: topics + } + this.seenCache.put(msgObj.seqno) + this._buildMessage(msgObj, cb) + } + + asyncMap(messages, buildMessage, (err, msgObjects) => { + if (err) callback(err) + this._publish(utils.normalizeOutRpcMessages(msgObjects)) + }) + } + + _publish (rpcs) { + throw errcode('_publish must be implemented by the subclass', 'ERR_NOT_IMPLEMENTED') + } + + /** + * Given a topic, returns up to count peers subscribed to that topic + * + * @param {String} topic + * @param {Number} count + * @returns {Set} + * + */ + _getPeers (topic, count) { + const peersInTopic = this.topics.get(topic) + if (!peersInTopic) { + return new Set() + } + + // Adds all peers using our protocol + let peers = [] + peersInTopic.forEach((peer) => { + if (peer.info.protocols.has(this.multicodec)) { + peers.push(peer) + } + }) + + // Pseudo-randomly shuffles peers + peers = this._shufflePeers(peers) + if (count > 0 && peers.length > count) { + peers = peers.slice(0, count) + } + + return new Set(peers) + } + + /** + * Pseudo-randomly shuffles peers + * + * @param {Array} peers + * @returns {Array} + */ + _shufflePeers (peers) { + if (peers.length <= 1) { + return peers + } + + for (let i = 0; i < peers.length; i++) { + const randInt = () => { + return Math.floor(Math.random() * Math.floor(peers.length)) + } + + const j = randInt() + const tmp = peers[i] + peers[i] = peers[j] + peers[j] = tmp + + return peers + } + } +} + +module.exports = BasicPubSub diff --git a/test/2-nodes.js b/test/2-nodes.js new file mode 100644 index 00000000..d01f3bcf --- /dev/null +++ b/test/2-nodes.js @@ -0,0 +1,612 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ +'use strict' + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-spies')) +const expect = chai.expect +const times = require('lodash/times') + +const { + createNode, + expectSet, + first, + dialNode, + startNode, + stopNode +} = require('./utils') + +const shouldNotHappen = (msg) => expect.fail() + +describe('1 node', () => { + describe('basics', () => { + let nodeA + + beforeEach(async () => { + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + }) + + afterEach(async function () { + if (nodeA.gs.started) { + await stopNode(nodeA.gs) + } + await stopNode(nodeA) + }) + + it('should mount the pubsub protocol', () => { + expect(nodeA.gs.peers.size).to.be.eql(0) + expect(nodeA.gs.mesh.size).to.eql(0) + expect(nodeA.gs.fanout.size).to.eql(0) + expect(nodeA.gs.lastpub.size).to.eql(0) + expect(nodeA.gs.gossip.size).to.eql(0) + expect(nodeA.gs.control.size).to.eql(0) + expect(nodeA.gs.subscriptions.size).to.eql(0) + expect(nodeA._switch.protocols[nodeA.gs.multicodec]).to.not.be.null() + }) + + it('should start a gossipsub successfully', async () => { + await startNode(nodeA.gs) + expect(nodeA.gs.started).to.equal(true) + }) + }) +}) + +describe('2 nodes', () => { + describe('basics', () => { + let nodeA + let nodeB + + beforeEach(async () => { + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('Dial from nodeA to nodeB', async () => { + await dialNode(nodeA, nodeB.peerInfo) + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(nodeA.gs.peers.size).to.equal(1) + expect(nodeB.gs.peers.size).to.equal(1) + }) + }) + + describe('subscription functionality', () => { + let nodeA + let nodeB + + beforeEach(async function () { + this.timeout(4000) + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + await dialNode(nodeA, nodeB.peerInfo) + await new Promise((resolve) => setTimeout(resolve, 1000)) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('Subscribe to a topic', async () => { + const topic = 'Z' + nodeA.gs.subscribe(topic) + nodeB.gs.subscribe(topic) + + // await subscription change + const [changedPeerInfo, changedTopics, changedSubs] = await new Promise((resolve) => { + nodeA.gs.once('pubsub:subscription-change', (...args) => resolve(args)) + }) + + expectSet(nodeA.gs.subscriptions, [topic]) + expectSet(nodeB.gs.subscriptions, [topic]) + expect(nodeA.gs.peers.size).to.equal(1) + expect(nodeB.gs.peers.size).to.equal(1) + expectSet(first(nodeA.gs.peers).topics, [topic]) + expectSet(first(nodeB.gs.peers).topics, [topic]) + + expect(changedPeerInfo.id.toB58String()).to.equal(first(nodeA.gs.peers).info.id.toB58String()) + expectSet(changedTopics, [topic]) + expect(changedSubs).to.be.eql([{ topicID: topic, subscribe: true }]) + + // await heartbeats + await Promise.all([ + new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)), + new Promise((resolve) => nodeB.gs.once('gossipsub:heartbeat', resolve)) + ]) + + expect(first(nodeA.gs.mesh.get(topic)).info.id.toB58String()).to.equal(first(nodeA.gs.peers).info.id.toB58String()) + expect(first(nodeB.gs.mesh.get(topic)).info.id.toB58String()).to.equal(first(nodeB.gs.peers).info.id.toB58String()) + }) + }) + + describe('publish functionality', () => { + let nodeA + let nodeB + const topic = 'Z' + + beforeEach(async function () { + this.timeout(4000) + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + await dialNode(nodeA, nodeB.peerInfo) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + nodeA.gs.subscribe(topic) + nodeB.gs.subscribe(topic) + + // await subscription change and heartbeat + await new Promise((resolve) => nodeA.gs.once('pubsub:subscription-change', resolve)) + await Promise.all([ + new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)), + new Promise((resolve) => nodeB.gs.once('gossipsub:heartbeat', resolve)) + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('Publish to a topic - nodeA', async () => { + const promise = new Promise((resolve) => nodeB.gs.once(topic, resolve)) + nodeA.gs.once(topic, (m) => shouldNotHappen) + + nodeA.gs.publish(topic, Buffer.from('hey')) + + const msg = await promise + + expect(msg.data.toString()).to.equal('hey') + expect(msg.from).to.be.eql(nodeA.gs.libp2p.peerInfo.id.toB58String()) + + nodeA.gs.removeListener(topic, shouldNotHappen) + }) + + it('Publish to a topic - nodeB', async () => { + const promise = new Promise((resolve) => nodeA.gs.once(topic, resolve)) + nodeB.gs.once(topic, shouldNotHappen) + + nodeB.gs.publish(topic, Buffer.from('banana')) + + const msg = await promise + + expect(msg.data.toString()).to.equal('banana') + expect(msg.from).to.be.eql(nodeB.gs.libp2p.peerInfo.id.toB58String()) + + nodeB.gs.removeListener(topic, shouldNotHappen) + }) + + it('Publish 10 msg to a topic', (done) => { + let counter = 0 + + nodeB.gs.once(topic, shouldNotHappen) + + nodeA.gs.on(topic, receivedMsg) + + function receivedMsg (msg) { + expect(msg.data.toString()).to.equal('banana') + expect(msg.from).to.be.eql(nodeB.gs.libp2p.peerInfo.id.toB58String()) + expect(Buffer.isBuffer(msg.seqno)).to.be.true() + expect(msg.topicIDs).to.be.eql([topic]) + + if (++counter === 10) { + nodeA.gs.removeListener(topic, receivedMsg) + nodeB.gs.removeListener(topic, shouldNotHappen) + done() + } + } + + times(10, () => nodeB.gs.publish(topic, Buffer.from('banana'))) + }) + + it('Publish 10 msg to a topic as array', (done) => { + let counter = 0 + + nodeB.gs.once(topic, shouldNotHappen) + + nodeA.gs.on(topic, receivedMsg) + + function receivedMsg (msg) { + expect(msg.data.toString()).to.equal('banana') + expect(msg.from).to.be.eql(nodeB.gs.libp2p.peerInfo.id.toB58String()) + expect(Buffer.isBuffer(msg.seqno)).to.be.true() + expect(msg.topicIDs).to.be.eql([topic]) + + if (++counter === 10) { + nodeA.gs.removeListener(topic, receivedMsg) + nodeB.gs.removeListener(topic, shouldNotHappen) + done() + } + } + + const msgs = [] + times(10, () => msgs.push(Buffer.from('banana'))) + nodeB.gs.publish(topic, msgs) + }) + }) + + describe('publish after unsubscribe', () => { + let nodeA + let nodeB + const topic = 'Z' + + beforeEach(async function () { + this.timeout(4000) + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + await dialNode(nodeA, nodeB.peerInfo) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + nodeA.gs.subscribe(topic) + nodeB.gs.subscribe(topic) + + // await subscription change and heartbeat + await new Promise((resolve) => nodeA.gs.once('pubsub:subscription-change', resolve)) + await Promise.all([ + new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)), + new Promise((resolve) => nodeB.gs.once('gossipsub:heartbeat', resolve)) + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('Unsubscribe from a topic', async () => { + nodeA.gs.unsubscribe(topic) + expect(nodeA.gs.subscriptions.size).to.equal(0) + + const [changedPeerInfo, changedTopics, changedSubs] = await new Promise((resolve) => { + nodeB.gs.once('pubsub:subscription-change', (...args) => resolve(args)) + }) + await new Promise((resolve) => nodeB.gs.once('gossipsub:heartbeat', resolve)) + + expect(nodeB.gs.peers.size).to.equal(1) + expectSet(first(nodeB.gs.peers).topics, []) + expect(changedPeerInfo.id.toB58String()).to.equal(first(nodeB.gs.peers).info.id.toB58String()) + expectSet(changedTopics, []) + expect(changedSubs).to.be.eql([{ topicID: topic, subscribe: false }]) + }) + + it('Publish to a topic after unsubscribe', async () => { + nodeA.gs.unsubscribe(topic) + await new Promise((resolve) => nodeB.gs.once('pubsub:subscription-change', resolve)) + await new Promise((resolve) => nodeB.gs.once('gossipsub:heartbeat', resolve)) + + const promise = new Promise((resolve, reject) => { + nodeA.gs.once(topic, reject) + setTimeout(() => { + nodeA.gs.removeListener(topic, reject) + resolve() + }, 100) + }) + + nodeB.gs.publish('Z', Buffer.from('banana')) + nodeA.gs.publish('Z', Buffer.from('banana')) + + try { + await promise + } catch (e) { + expect.fail('message should not be received') + } + }) + }) + + describe('nodes send state on connection', () => { + let nodeA + let nodeB + + before(async () => { + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + }) + + after(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('existing subscriptions are sent upon peer connection', async function () { + this.timeout(5000) + nodeA.gs.subscribe('Za') + nodeB.gs.subscribe('Zb') + + expect(nodeA.gs.peers.size).to.equal(0) + expectSet(nodeA.gs.subscriptions, ['Za']) + expect(nodeB.gs.peers.size).to.equal(0) + expectSet(nodeB.gs.subscriptions, ['Zb']) + + await dialNode(nodeA, nodeB.peerInfo) + + await Promise.all([ + new Promise((resolve) => nodeA.gs.once('pubsub:subscription-change', resolve)), + new Promise((resolve) => nodeB.gs.once('pubsub:subscription-change', resolve)) + ]) + expect(nodeA.gs.peers.size).to.equal(1) + expect(nodeB.gs.peers.size).to.equal(1) + + expectSet(nodeA.gs.subscriptions, ['Za']) + expect(nodeB.gs.peers.size).to.equal(1) + expectSet(first(nodeB.gs.peers).topics, ['Za']) + + expectSet(nodeB.gs.subscriptions, ['Zb']) + expect(nodeA.gs.peers.size).to.equal(1) + expectSet(first(nodeA.gs.peers).topics, ['Zb']) + }) + }) + + describe('nodes handle stopping', () => { + let nodeA + let nodeB + + before(async () => { + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + + await dialNode(nodeA, nodeB.peerInfo) + await new Promise((resolve) => setTimeout(resolve, 1000)) + }) + + after(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('nodes don\'t have peers after stopped', async () => { + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + expect(nodeA.gs.peers.size).to.equal(0) + expect(nodeB.gs.peers.size).to.equal(0) + }) + }) + + describe('prevent concurrent dials', () => { + let sandbox + let nodeA + let nodeB + + before(async () => { + sandbox = chai.spy.sandbox() + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + // Put node B in node A's peer book + nodeA.peerBook.put(nodeB.peerInfo) + + await startNode(nodeB.gs) + }) + + after(async function () { + this.timeout(4000) + sandbox.restore() + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('does not dial twice to same peer', async () => { + sandbox.on(nodeA.gs, ['_onDial']) + + // When node A starts, it will dial all peers in its peer book, which + // is just peer B + await startNode(nodeA.gs) + + // Simulate a connection coming in from peer B at the same time. This + // causes gossipsub to dial peer B + nodeA.emit('peer:connect', nodeB.peerInfo) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + // Check that only one dial was made + expect(nodeA.gs._onDial).to.have.been.called.once() + }) + }) + + describe('allow dials even after error', () => { + let sandbox + let nodeA + let nodeB + + before(async () => { + sandbox = chai.spy.sandbox() + + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + // Put node B in node A's peer book + nodeA.peerBook.put(nodeB.peerInfo) + + await startNode(nodeB.gs) + }) + + after(async function () { + this.timeout(4000) + sandbox.restore() + await Promise.all([ + stopNode(nodeA.gs), + stopNode(nodeB.gs) + ]) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('can dial again after error', (done) => { + let firstTime = true + const dialProtocol = nodeA.gs.libp2p.dialProtocol.bind(nodeA.gs.libp2p) + sandbox.on(nodeA.gs.libp2p, 'dialProtocol', (peerInfo, multicodec, cb) => { + // Return an error for the first dial + if (firstTime) { + firstTime = false + return cb(new Error('dial error')) + } + + // Subsequent dials proceed as normal + dialProtocol(peerInfo, multicodec, cb) + }) + + // When node A starts, it will dial all peers in its peer book, which + // is just peer B + nodeA.gs.start(startComplete) + + function startComplete () { + // Simulate a connection coming in from peer B. This causes gossipsub + // to dial peer B + nodeA.emit('peer:connect', nodeB.peerInfo) + + // Check that both dials were made + setTimeout(() => { + expect(nodeA.gs.libp2p.dialProtocol).to.have.been.called.twice() + done() + }, 1000) + } + }) + }) + + describe('prevent processing dial after stop', () => { + let sandbox + let nodeA + let nodeB + + before(async () => { + sandbox = chai.spy.sandbox() + + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + nodeB = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeB) + + // Put node B in node A's peer book + nodeA.peerBook.put(nodeB.peerInfo) + + await Promise.all([ + startNode(nodeA.gs), + startNode(nodeB.gs) + ]) + }) + + after(async function () { + this.timeout(4000) + sandbox.restore() + await stopNode(nodeB.gs) + await Promise.all([ + stopNode(nodeA), + stopNode(nodeB) + ]) + }) + + it('does not process dial after stop', (done) => { + sandbox.on(nodeA.gs, ['_onDial']) + + // Simulate a connection coming in from peer B at the same time. This + // causes gossipsub to dial peer B + nodeA.emit('peer:connect', nodeB.peerInfo) + + // Stop gossipsub before the dial can complete + nodeA.gs.stop(() => { + // Check that the dial was not processed + setTimeout(() => { + expect(nodeA.gs._onDial).to.not.have.been.called() + done() + }, 1000) + }) + }) + }) +}) diff --git a/test/gossip.js b/test/gossip.js new file mode 100644 index 00000000..ffed3f39 --- /dev/null +++ b/test/gossip.js @@ -0,0 +1,102 @@ +'use strict' +/* eslint-env mocha */ + +const { expect } = require('chai') +const sinon = require('sinon') + +const { GossipSubDhi } = require('../src/constants') +const { + first, + createNode, + dialNode, + startNode, + stopNode +} = require('./utils') + +describe('gossip', () => { + let nodes = Array.from({ length: GossipSubDhi + 2 }) // enough nodes to trigger high threshold + + beforeEach(async () => { + for (let i = 0; i < nodes.length; i++) { + nodes[i] = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodes[i]) + await startNode(nodes[i].gs) + } + }) + afterEach(async function () { + this.timeout(10000) + await Promise.all(nodes.map((n) => stopNode(n.gs))) + await Promise.all(nodes.map((n) => stopNode(n))) + }) + + it('should send gossip to non-mesh peers in topic', async function () { + this.timeout(10000) + const nodeA = nodes[0] + const topic = 'Z' + // add subscriptions to each node + nodes.forEach((n) => n.gs.subscribe(topic)) + // every node connected to every other + for (let i = 0; i < nodes.length - 1; i++) { + for (let j = i + 1; j < nodes.length; j++) { + await dialNode(nodes[i], nodes[j].peerInfo) + } + } + await new Promise((resolve) => setTimeout(resolve, 500)) + // await mesh rebalancing + await Promise.all(nodes.map((n) => new Promise((resolve) => n.gs.once('gossipsub:heartbeat', resolve)))) + await new Promise((resolve) => setTimeout(resolve, 500)) + // set spy + sinon.spy(nodeA.gs, 'log') + + nodeA.gs.publish(topic, Buffer.from('hey')) + await new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)) + expect(nodeA.gs.log.callCount).to.be.gt(1) + nodeA.gs.log.getCalls() + .filter((call) => call.args[0] === 'Add gossip to %s') + .map((call) => call.args[1]) + .forEach((peerId) => { + nodeA.gs.mesh.get(topic).forEach((meshPeer) => { + expect(meshPeer.info.id.toB58String()).to.not.equal(peerId) + }) + }) + + // unset spy + nodeA.gs.log.restore() + }) + + it('should send piggyback gossip into other sent messages', async function () { + this.timeout(10000) + const nodeA = nodes[0] + const topic = 'Z' + // add subscriptions to each node + nodes.forEach((n) => n.gs.subscribe(topic)) + // every node connected to every other + for (let i = 0; i < nodes.length - 1; i++) { + for (let j = i + 1; j < nodes.length; j++) { + await dialNode(nodes[i], nodes[j].peerInfo) + } + } + await new Promise((resolve) => setTimeout(resolve, 500)) + // await mesh rebalancing + await Promise.all(nodes.map((n) => new Promise((resolve) => n.gs.once('gossipsub:heartbeat', resolve)))) + await new Promise((resolve) => setTimeout(resolve, 500)) + + const peerB = first(nodeA.gs.mesh.get(topic)) + const nodeB = nodes.find((n) => n.peerInfo.id.toB58String() === peerB.info.id.toB58String()) + // set spy + sinon.spy(nodeB.gs, 'log') + + // manually add control message to be sent to peerB + nodeA.gs.control.set(peerB, { graft: [{ topicID: topic }] }) + nodeA.gs.publish(topic, Buffer.from('hey')) + await new Promise((resolve) => setTimeout(resolve, 500)) + expect(nodeB.gs.log.callCount).to.be.gt(1) + // expect control message to be sent alongside published message + const call = nodeB.gs.log.getCalls().find((call) => call.args[0] === 'GRAFT: Add mesh link from %s in %s') + expect(call).to.not.equal(undefined) + expect(call.args[1]).to.equal(nodeA.peerInfo.id.toB58String()) + + // unset spy + nodeB.gs.log.restore() + }) +}) diff --git a/test/heartbeat.js b/test/heartbeat.js new file mode 100644 index 00000000..f1bf82c8 --- /dev/null +++ b/test/heartbeat.js @@ -0,0 +1,34 @@ +'use strict' +/* eslint-env mocha */ + +const { expect } = require('chai') + +const { GossipSubHeartbeatInterval } = require('../src/constants') +const { + createNode, + startNode, + stopNode +} = require('./utils') + +describe('heartbeat', () => { + let nodeA + before(async () => { + nodeA = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodeA) + await startNode(nodeA.gs) + }) + after(async () => { + await stopNode(nodeA.gs) + await stopNode(nodeA) + }) + + it('should occur with regularity defined by a constant', async function () { + this.timeout(3000) + await new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)) + const t1 = Date.now() + await new Promise((resolve) => nodeA.gs.once('gossipsub:heartbeat', resolve)) + const t2 = Date.now() + const safeDelta = 100 // ms + expect(t2 - t1).to.be.lt(GossipSubHeartbeatInterval + safeDelta) + }) +}) diff --git a/test/mesh.js b/test/mesh.js new file mode 100644 index 00000000..dc073d66 --- /dev/null +++ b/test/mesh.js @@ -0,0 +1,63 @@ +'use strict' +/* eslint-env mocha */ + +const { expect } = require('chai') + +const { GossipSubDhi } = require('../src/constants') +const { + createNode, + dialNode, + startNode, + stopNode +} = require('./utils') + +describe('mesh overlay', () => { + let nodes = Array.from({ length: GossipSubDhi + 2 }) // enough nodes to trigger high threshold + + beforeEach(async () => { + for (let i = 0; i < nodes.length; i++) { + nodes[i] = await createNode('/ip4/127.0.0.1/tcp/0') + await startNode(nodes[i]) + await startNode(nodes[i].gs) + } + }) + afterEach(async function () { + this.timeout(10000) + await Promise.all(nodes.map((n) => stopNode(n.gs))) + await Promise.all(nodes.map((n) => stopNode(n))) + }) + + it('should add mesh peers below threshold', async function () { + this.timeout(3000) + // test against node0 + const node0 = nodes[0] + const topic = 'Z' + // add subscriptions to each node + nodes.forEach((n) => n.gs.subscribe(topic)) + // connect N (< GossipsubD) nodes to node0 + const N = 4 + await Promise.all(nodes.slice(nodes.length - N).map((n) => dialNode(n, node0.peerInfo))) + await new Promise((resolve) => setTimeout(resolve, 500)) + // await mesh rebalancing + await new Promise((resolve) => node0.gs.once('gossipsub:heartbeat', resolve)) + expect(node0.gs.mesh.get(topic).size).to.equal(N) + }) + it('should remove mesh peers once above threshold', async function () { + this.timeout(10000) + // test against node0 + const node0 = nodes[0] + const topic = 'Z' + // add subscriptions to each node + nodes.forEach((n) => n.gs.subscribe(topic)) + // connect all nodes to node0 + for (let i = 0; i < nodes.length - 1; i++) { + for (let j = i + 1; j < nodes.length; j++) { + await dialNode(nodes[i], nodes[j].peerInfo) + } + } + await new Promise((resolve) => setTimeout(resolve, 500)) + // await mesh rebalancing + await new Promise((resolve) => node0.gs.once('gossipsub:heartbeat', resolve)) + expect(node0.gs.mesh.get(topic).size).to.be.lte(GossipSubDhi) + }) +}) diff --git a/test/messageCache.js b/test/messageCache.js new file mode 100644 index 00000000..e16dd80c --- /dev/null +++ b/test/messageCache.js @@ -0,0 +1,139 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ +/* eslint-disable no-unused-expressions */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const chaiSpies = require('chai-spies') +chai.use(chaiSpies) +const expect = chai.expect + +const { MessageCache } = require('../src/messageCache') +const { utils } = require('libp2p-pubsub') +const Buffer = require('buffer').Buffer + +const getMsgID = (msg) => { + return utils.msgId(msg.from, msg.seqno) +} + +describe('Testing Message Cache Operations', () => { + const messageCache = new MessageCache(3, 5) + const testMessages = [] + + before(() => { + const makeTestMessage = (n) => { + return { + from: 'test', + data: Buffer.from(n.toString()), + seqno: utils.randomSeqno(), + topicIDs: ['test'] + } + } + + for (let i = 0; i < 60; i++) { + testMessages.push(makeTestMessage(i)) + } + + for (let i = 0; i < 10; i++) { + messageCache.put(testMessages[i]) + } + }) + + it('Should retrieve correct messages for each test message', () => { + for (let i = 0; i < 10; i++) { + const msgId = getMsgID(testMessages[i]) + const message = messageCache.get(msgId) + expect(message).to.equal(testMessages[i]) + } + }) + + it('Get GossipIDs', () => { + const gossipIDs = messageCache.getGossipIDs('test') + expect(gossipIDs.length).to.equal(10) + + for (let i = 0; i < 10; i++) { + const messageID = getMsgID(testMessages[i]) + expect(messageID).to.equal(gossipIDs[i]) + } + }) + + it('Shift message cache', () => { + messageCache.shift() + for (let i = 10; i < 20; i++) { + messageCache.put(testMessages[i]) + } + + for (let i = 0; i < 20; i++) { + const messageID = getMsgID(testMessages[i]) + const message = messageCache.get(messageID) + expect(message).to.equal(testMessages[i]) + } + + let gossipIDs = messageCache.getGossipIDs('test') + expect(gossipIDs.length).to.equal(20) + + for (let i = 0; i < 10; i++) { + const messageID = getMsgID(testMessages[i]) + expect(messageID).to.equal(gossipIDs[10 + i]) + } + + for (let i = 10; i < 20; i++) { + const messageID = getMsgID(testMessages[i]) + expect(messageID).to.equal(gossipIDs[i - 10]) + } + + messageCache.shift() + for (let i = 20; i < 30; i++) { + messageCache.put(testMessages[i]) + } + + messageCache.shift() + for (let i = 30; i < 40; i++) { + messageCache.put(testMessages[i]) + } + + messageCache.shift() + for (let i = 40; i < 50; i++) { + messageCache.put(testMessages[i]) + } + + messageCache.shift() + for (let i = 50; i < 60; i++) { + messageCache.put(testMessages[i]) + } + + expect(messageCache.msgs.size).to.equal(50) + + for (let i = 0; i < 10; i++) { + const messageID = getMsgID(testMessages[i]) + const message = messageCache.get(messageID) + expect(message).to.be.an('undefined') + } + + for (let i = 10; i < 60; i++) { + const messageID = getMsgID(testMessages[i]) + const message = messageCache.get(messageID) + expect(message).to.equal(testMessages[i]) + } + + gossipIDs = messageCache.getGossipIDs('test') + expect(gossipIDs.length).to.equal(30) + + for (let i = 0; i < 10; i++) { + const messageID = getMsgID(testMessages[50 + i]) + expect(messageID).to.equal(gossipIDs[i]) + } + + for (let i = 10; i < 20; i++) { + const messageID = getMsgID(testMessages[30 + i]) + expect(messageID).to.equal(gossipIDs[i]) + } + + for (let i = 20; i < 30; i++) { + const messageID = getMsgID(testMessages[10 + i]) + expect(messageID).to.equal(gossipIDs[i]) + } + }) +}) diff --git a/test/multiple-nodes.js b/test/multiple-nodes.js new file mode 100644 index 00000000..b003e6e6 --- /dev/null +++ b/test/multiple-nodes.js @@ -0,0 +1,356 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ +'use strict' + +const chai = require('chai') +chai.use(require('dirty-chai')) +const expect = chai.expect +const promisify = require('promisify-es6') + +const { + createNode, + expectSet, + dialNode, + startNode, + stopNode +} = require('./utils') + +describe('multiple nodes (more than 2)', () => { + describe('every peer subscribes to the topic', () => { + describe('line', () => { + // line + // ◉────◉────◉ + // a b c + describe('subscribe', () => { + let a + let b + let c + const topic = 'Z' + + beforeEach(async () => { + a = await createNode('/ip4/127.0.0.1/tcp/0') + b = await createNode('/ip4/127.0.0.1/tcp/0') + c = await createNode('/ip4/127.0.0.1/tcp/0') + await Promise.all([ + startNode(a), + startNode(b), + startNode(c) + ]) + await Promise.all([ + startNode(a.gs), + startNode(b.gs), + startNode(c.gs) + ]) + await dialNode(a, b.peerInfo) + await dialNode(b, c.peerInfo) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(a.gs), + stopNode(b.gs), + stopNode(c.gs) + ]) + await Promise.all([ + stopNode(a), + stopNode(b), + stopNode(c) + ]) + }) + + it('subscribe to the topic on all nodes', async () => { + a.gs.subscribe(topic) + b.gs.subscribe(topic) + c.gs.subscribe(topic) + + expectSet(a.gs.subscriptions, [topic]) + expectSet(b.gs.subscriptions, [topic]) + expectSet(c.gs.subscriptions, [topic]) + + await Promise.all([ + promisify(a.gs.once.bind(a.gs))('gossipsub:heartbeat'), + promisify(b.gs.once.bind(b.gs))('gossipsub:heartbeat'), + promisify(c.gs.once.bind(c.gs))('gossipsub:heartbeat') + ]) + + expect(a.gs.peers.size).to.equal(1) + expect(b.gs.peers.size).to.equal(2) + expect(c.gs.peers.size).to.equal(1) + + const aPeerId = a.peerInfo.id.toB58String() + const bPeerId = b.peerInfo.id.toB58String() + const cPeerId = c.peerInfo.id.toB58String() + + expectSet(a.gs.peers.get(bPeerId).topics, [topic]) + expectSet(b.gs.peers.get(aPeerId).topics, [topic]) + expectSet(b.gs.peers.get(cPeerId).topics, [topic]) + expectSet(c.gs.peers.get(bPeerId).topics, [topic]) + + expect(a.gs.mesh.get(topic).size).to.equal(1) + expect(b.gs.mesh.get(topic).size).to.equal(2) + expect(c.gs.mesh.get(topic).size).to.equal(1) + }) + }) + + describe('publish', () => { + let a + let b + let c + const topic = 'Z' + + beforeEach(async () => { + a = await createNode('/ip4/127.0.0.1/tcp/0') + b = await createNode('/ip4/127.0.0.1/tcp/0') + c = await createNode('/ip4/127.0.0.1/tcp/0') + await Promise.all([ + startNode(a), + startNode(b), + startNode(c) + ]) + await Promise.all([ + startNode(a.gs), + startNode(b.gs), + startNode(c.gs) + ]) + await dialNode(a, b.peerInfo) + await dialNode(b, c.peerInfo) + + a.gs.subscribe(topic) + b.gs.subscribe(topic) + c.gs.subscribe(topic) + + await Promise.all([ + promisify(a.gs.once.bind(a.gs))('gossipsub:heartbeat'), + promisify(b.gs.once.bind(b.gs))('gossipsub:heartbeat'), + promisify(c.gs.once.bind(c.gs))('gossipsub:heartbeat') + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(a.gs), + stopNode(b.gs), + stopNode(c.gs) + ]) + await Promise.all([ + stopNode(a), + stopNode(b), + stopNode(c) + ]) + }) + + it('publish on node a', async () => { + let msgB = new Promise((resolve) => b.gs.once('Z', resolve)) + let msgC = new Promise((resolve) => c.gs.once('Z', resolve)) + + a.gs.publish('Z', Buffer.from('hey')) + msgB = await msgB + msgC = await msgC + + expect(msgB.data.toString()).to.equal('hey') + expect(msgC.data.toString()).to.equal('hey') + }) + + it('publish array on node a', async () => { + let msgB = new Promise((resolve) => { + const output = [] + b.gs.on('Z', (msg) => { + output.push(msg) + if (output.length === 2) { + b.gs.removeAllListeners('Z') + resolve(output) + } + }) + }) + let msgC = new Promise((resolve) => { + const output = [] + c.gs.on('Z', (msg) => { + output.push(msg) + if (output.length === 2) { + c.gs.removeAllListeners('Z') + resolve(output) + } + }) + }) + + a.gs.publish('Z', [Buffer.from('hey'), Buffer.from('hey')]) + msgB = await msgB + msgC = await msgC + + expect(msgB.length).to.equal(2) + expect(msgB[0].data.toString()).to.equal('hey') + expect(msgB[1].data.toString()).to.equal('hey') + expect(msgC.length).to.equal(2) + expect(msgC[0].data.toString()).to.equal('hey') + expect(msgC[1].data.toString()).to.equal('hey') + }) + }) + }) + + describe('1 level tree', () => { + // 1 level tree + // ┌◉┐ + // │b│ + // ◉─┘ └─◉ + // a c + + let a + let b + let c + const topic = 'Z' + + beforeEach(async () => { + a = await createNode('/ip4/127.0.0.1/tcp/0') + b = await createNode('/ip4/127.0.0.1/tcp/0') + c = await createNode('/ip4/127.0.0.1/tcp/0') + await Promise.all([ + startNode(a), + startNode(b), + startNode(c) + ]) + await Promise.all([ + startNode(a.gs), + startNode(b.gs), + startNode(c.gs) + ]) + await dialNode(a, b.peerInfo) + await dialNode(b, c.peerInfo) + + a.gs.subscribe(topic) + b.gs.subscribe(topic) + c.gs.subscribe(topic) + + await Promise.all([ + promisify(a.gs.once.bind(a.gs))('gossipsub:heartbeat'), + promisify(b.gs.once.bind(b.gs))('gossipsub:heartbeat'), + promisify(c.gs.once.bind(c.gs))('gossipsub:heartbeat') + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(a.gs), + stopNode(b.gs), + stopNode(c.gs) + ]) + await Promise.all([ + stopNode(a), + stopNode(b), + stopNode(c) + ]) + }) + + it('publish on node b', async () => { + let msgA = new Promise((resolve) => a.gs.once('Z', resolve)) + let msgC = new Promise((resolve) => c.gs.once('Z', resolve)) + + b.gs.publish('Z', Buffer.from('hey')) + msgA = await msgA + msgC = await msgC + + expect(msgA.data.toString()).to.equal('hey') + expect(msgC.data.toString()).to.equal('hey') + }) + }) + + describe('2 level tree', () => { + // 2 levels tree + // ┌◉┐ + // │c│ + // ┌◉─┘ └─◉┐ + // │b d│ + // ◉─┘ └─◉ + // a e + let a + let b + let c + let d + let e + const topic = 'Z' + + beforeEach(async function () { + this.timeout(5000) + a = await createNode('/ip4/127.0.0.1/tcp/0') + b = await createNode('/ip4/127.0.0.1/tcp/0') + c = await createNode('/ip4/127.0.0.1/tcp/0') + d = await createNode('/ip4/127.0.0.1/tcp/0') + e = await createNode('/ip4/127.0.0.1/tcp/0') + await Promise.all([ + startNode(a), + startNode(b), + startNode(c), + startNode(d), + startNode(e) + ]) + await Promise.all([ + startNode(a.gs), + startNode(b.gs), + startNode(c.gs), + startNode(d.gs), + startNode(e.gs) + ]) + await dialNode(a, b.peerInfo) + await dialNode(b, c.peerInfo) + await dialNode(c, d.peerInfo) + await dialNode(d, e.peerInfo) + + await new Promise((resolve) => setTimeout(resolve, 500)) + + a.gs.subscribe(topic) + b.gs.subscribe(topic) + c.gs.subscribe(topic) + d.gs.subscribe(topic) + e.gs.subscribe(topic) + + await Promise.all([ + promisify(a.gs.once.bind(a.gs))('gossipsub:heartbeat'), + promisify(b.gs.once.bind(b.gs))('gossipsub:heartbeat'), + promisify(c.gs.once.bind(c.gs))('gossipsub:heartbeat'), + promisify(d.gs.once.bind(c.gs))('gossipsub:heartbeat'), + promisify(e.gs.once.bind(c.gs))('gossipsub:heartbeat') + ]) + }) + + afterEach(async function () { + this.timeout(4000) + await Promise.all([ + stopNode(a.gs), + stopNode(b.gs), + stopNode(c.gs), + stopNode(d.gs), + stopNode(e.gs) + ]) + await Promise.all([ + stopNode(a), + stopNode(b), + stopNode(c), + stopNode(d), + stopNode(e) + ]) + }) + + it('publishes from c', async () => { + let msgA = new Promise((resolve) => a.gs.once('Z', resolve)) + let msgB = new Promise((resolve) => b.gs.once('Z', resolve)) + let msgD = new Promise((resolve) => d.gs.once('Z', resolve)) + let msgE = new Promise((resolve) => e.gs.once('Z', resolve)) + + const msg = 'hey from c' + c.gs.publish('Z', Buffer.from(msg)) + + msgA = await msgA + msgB = await msgB + msgD = await msgD + msgE = await msgE + + expect(msgA.data.toString()).to.equal(msg) + expect(msgB.data.toString()).to.equal(msg) + expect(msgD.data.toString()).to.equal(msg) + expect(msgE.data.toString()).to.equal(msg) + }) + }) + }) +}) diff --git a/test/node.js b/test/node.js new file mode 100644 index 00000000..2f163825 --- /dev/null +++ b/test/node.js @@ -0,0 +1,8 @@ +'use strict' + +require('./messageCache') +require('./2-nodes') +require('./multiple-nodes') +require('./heartbeat') +require('./mesh') +require('./gossip') diff --git a/test/nodejs-bundle.js b/test/nodejs-bundle.js new file mode 100644 index 00000000..c5e840a8 --- /dev/null +++ b/test/nodejs-bundle.js @@ -0,0 +1,24 @@ +'use strict' + +const TCP = require('libp2p-tcp') +const spdy = require('libp2p-spdy') +const secio = require('libp2p-secio') +const libp2p = require('libp2p') + +class Node extends libp2p { + constructor ({ peerInfo, peerBook }) { + const modules = { + transport: [TCP], + streamMuxer: [spdy], + connEncryption: [secio] + } + + super({ + modules, + peerInfo, + peerBook + }) + } +} + +module.exports = Node diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 00000000..a118d1a6 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,29 @@ +'use strict' + +const PeerId = require('peer-id') +const PeerInfo = require('peer-info') +const { expect } = require('chai') +const promisify = require('promisify-es6') + +const Node = require('./nodejs-bundle') + +const GossipSub = require('../src') + +exports.first = (map) => map.values().next().value + +exports.expectSet = (set, subs) => { + expect(Array.from(set.values())).to.eql(subs) +} + +exports.createNode = async (maddr) => { + const id = await promisify(PeerId.create)({ bits: 1024 }) + const peerInfo = await promisify(PeerInfo.create)(id) + peerInfo.multiaddrs.add(maddr) + const node = new Node({ peerInfo }) + node.gs = new GossipSub(node) + return node +} + +exports.startNode = async (node) => promisify(node.start.bind(node))() +exports.stopNode = async (node) => promisify(node.stop.bind(node))() +exports.dialNode = async (node, peerInfo) => promisify(node.dial.bind(node))(peerInfo)