From dbe80ccc7afedfbb7471329a384b30324c7b43ea Mon Sep 17 00:00:00 2001 From: David Dias Date: Wed, 7 Dec 2016 10:57:01 -0800 Subject: [PATCH] feat: new bitswap --- API.md | 82 ---- README.md | 116 +++++- benchmarks/index.js | 8 +- benchmarks/put-get.js | 6 +- img/architecture.monopic | Bin 0 -> 4281 bytes img/architecture.png | Bin 0 -> 103613 bytes img/architecture.txt | 51 +++ package.json | 11 +- .../decision-engine/index.js} | 75 ++-- .../decision-engine}/ledger.js | 22 +- src/{ => components}/network/index.js | 94 +++-- .../want-manager}/index.js | 51 +-- .../want-manager}/msg-queue.js | 19 +- src/constants.js | 10 +- src/decision/index.js | 5 - src/decision/pq.js | 31 -- src/index.js | 181 ++++----- src/message/entry.js | 38 -- src/message/index.js | 119 ------ src/message/message.proto.js | 21 - src/types/message/entry.js | 40 ++ src/types/message/index.js | 210 ++++++++++ src/types/message/message.proto.js | 28 ++ src/types/wantlist/entry.js | 42 ++ src/types/wantlist/index.js | 65 ++++ src/wantlist/entry.js | 55 --- src/wantlist/index.js | 58 --- test/browser.js | 2 +- .../decision-engine/index-test.js} | 183 ++++----- .../decision-engine}/ledger.spec.js | 22 +- .../network/gen-bitswap-network.node.js | 24 +- test/components/network/network.node.js | 365 ++++++++++++++++++ test/components/wantmanager/index.spec.js | 97 +++++ test/components/wantmanager/msg-queue.spec.js | 110 ++++++ test/index-test.js | 280 ++++++-------- test/message.spec.js | 222 ----------- test/network/network.node.js | 203 ---------- test/node.js | 6 +- .../bitswap110-message-full-wantlist | Bin 0 -> 301 bytes .../bitswap110-message-one-block | Bin 0 -> 26 bytes test/types/message.spec.js | 281 ++++++++++++++ test/{ => types}/wantlist.spec.js | 113 ++++-- test/utils.js | 4 +- test/wantmanager/index.spec.js | 73 ---- test/wantmanager/msg-queue.spec.js | 80 ---- 45 files changed, 1989 insertions(+), 1514 deletions(-) delete mode 100644 API.md create mode 100644 img/architecture.monopic create mode 100644 img/architecture.png create mode 100644 img/architecture.txt rename src/{decision/engine.js => components/decision-engine/index.js} (78%) rename src/{decision => components/decision-engine}/ledger.js (59%) rename src/{ => components}/network/index.js (61%) rename src/{wantmanager => components/want-manager}/index.js (64%) rename src/{wantmanager => components/want-manager}/msg-queue.js (68%) delete mode 100644 src/decision/index.js delete mode 100644 src/decision/pq.js delete mode 100644 src/message/entry.js delete mode 100644 src/message/index.js delete mode 100644 src/message/message.proto.js create mode 100644 src/types/message/entry.js create mode 100644 src/types/message/index.js create mode 100644 src/types/message/message.proto.js create mode 100644 src/types/wantlist/entry.js create mode 100644 src/types/wantlist/index.js delete mode 100644 src/wantlist/entry.js delete mode 100644 src/wantlist/index.js rename test/{decision/engine-test.js => components/decision-engine/index-test.js} (51%) rename test/{decision => components/decision-engine}/ledger.spec.js (58%) rename test/{ => components}/network/gen-bitswap-network.node.js (86%) create mode 100644 test/components/network/network.node.js create mode 100644 test/components/wantmanager/index.spec.js create mode 100644 test/components/wantmanager/msg-queue.spec.js delete mode 100644 test/message.spec.js delete mode 100644 test/network/network.node.js create mode 100644 test/test-data/serialized-from-go/bitswap110-message-full-wantlist create mode 100644 test/test-data/serialized-from-go/bitswap110-message-one-block create mode 100644 test/types/message.spec.js rename test/{ => types}/wantlist.spec.js (51%) delete mode 100644 test/wantmanager/index.spec.js delete mode 100644 test/wantmanager/msg-queue.spec.js diff --git a/API.md b/API.md deleted file mode 100644 index e37fa651..00000000 --- a/API.md +++ /dev/null @@ -1,82 +0,0 @@ -# API - -## Public Methods - -### `constructor(id, libp2p, datastore)` - -- `id: PeerId`, the id of the local instance. -- `libp2p: Libp2p`, instance of the local network stack. -- `blockstore: Datastore`, instance of the local database (`IpfsRepo.blockstore`) - -Create a new instance. - - -### `getStream(key)` - -- `key: Multihash|Array` - -Returns a source `pull-stream`. Values emitted are the received blocks. - -Example: - -```js -// Single block -pull( - bitswap.getStream(key), - pull.collect((err, blocks) => { - // blocks === [block] - }) -) - -// Many blocks -pull( - bitswap.getStream([key1, key2, key3]), - pull.collect((err, blocks) => { - // blocks === [block1, block2, block3] - }) -) -``` - - -> Note: This is safe guarded so that the network is not asked -> for blocks that are in the local `datastore`. - - -### `unwant(keys)` - -- `keys: Mutlihash|[]Multihash` - -Cancel previously requested keys, forcefully. That means they are removed from the -wantlist independent of how many other resources requested these keys. Callbacks -attached to `getBlock` are errored with `Error('manual unwant: key')`. - -### `cancelWants(keys)` - -- `keys: Multihash|[]Multihash` - -Cancel previously requested keys. - -### `putStream()` - -Returns a duplex `pull-stream` that emits an object `{key: Multihash}` for every written block when it was stored. -Objects passed into here should be of the form `{data: Buffer, key: Multihash}` - -### `put(blockAndKey, cb)` - -- `blockAndKey: {data: Buffer, key: Multihash}` -- `cb: Function` - -Announce that the current node now has the block containing `data`. This will store it -in the local database and attempt to serve it to all peers that are known - to have requested it. The callback is called when we are sure that the block - is stored. - -### `wantlistForPeer(peerId)` - -- `peerId: PeerId` - -Get the wantlist for a given peer. - -### `stat()` - -Get stats about about the current state of the bitswap instance. diff --git a/README.md b/README.md index e997ac45..640af1f4 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ ### npm ```sh -> npm i ipfs-bitswap +> npm install ipfs-bitswap --save ``` ### Use in Node.js ```js -const bitswap = require('ipfs-bitswap') +const Bitswap = require('ipfs-bitswap') ``` ### Use in a browser with browserify, webpack or any other bundler @@ -45,7 +45,7 @@ const bitswap = require('ipfs-bitswap') The code published to npm that gets loaded on require is in fact a ES5 transpiled version with the right shims added. This means that you can require it and use with your favourite bundler without having to adjust asset management process. ```js -const bitswap = require('ipfs-bitswap') +const Bitswap = require('ipfs-bitswap') ``` ### Use in a browser using a script tag @@ -62,6 +62,116 @@ Loading this module through a script tag will make the `IpfsBitswap` object avai For the documentation see [API.md](API.md). +### API + +#### `new Bitswap(libp2p, blockstore)` + +- `libp2p: Libp2p`, instance of the local network stack. +- `blockstore: Blockstore`, instance of the local database (`IpfsRepo.blockstore`) + +Create a new instance. + +#### `getStream(cid)` + +- `cid: CID|Array` + +Returns a source `pull-stream`. Values emitted are the received blocks. + +Example: + +```js +// Single block +pull( + bitswap.getStream(cid), + pull.collect((err, blocks) => { + // blocks === [block] + }) +) + +// Many blocks +pull( + bitswap.getStream([cid1, cid2, cid3]), + pull.collect((err, blocks) => { + // blocks === [block1, block2, block3] + }) +) +``` + +> Note: This is safe guarded so that the network is not asked +> for blocks that are in the local `datastore`. + +#### `unwant(cids)` + +- `cids: CID|[]CID` + +Cancel previously requested keys, forcefully. That means they are removed from the +wantlist independent of how many other resources requested these keys. Callbacks +attached to `getBlock` are errored with `Error('manual unwant: key')`. + +#### `cancelWants(cids)` + +- `cid: CID|[]CID` + +Cancel previously requested keys. + +#### `putStream()` + +Returns a duplex `pull-stream` that emits an object `{key: Multihash}` for every written block when it was stored. +Objects passed into here should be of the form `{data: Buffer, key: Multihash}` + +#### `put(blockAndCid, callback)` + +- `blockAndKey: {data: Buffer, cid: CID}` +- `callback: Function` + +Announce that the current node now has the block containing `data`. This will store it +in the local database and attempt to serve it to all peers that are known + to have requested it. The callback is called when we are sure that the block + is stored. + +#### `wantlistForPeer(peerId)` + +- `peerId: PeerId` + +Get the wantlist for a given peer. + +#### `stat()` + +Get stats about about the current state of the bitswap instance. + +## Development + +### Structure + +![](/img/architecture.png) + +```sh +» tree src +src +├── components +│   ├── decision +│   │   ├── engine.js +│   │   ├── index.js +│   │   ├── ledger.js +│   │   ├── peer-request-queue.js +│   │   └── pq.js +│   ├── network # Handles peerSet and open new conns +│   │   └── index.js +│   └── want-manager # Keeps track of all blocks the peer wants (not the others which it is connected) +│   ├── index.js +│   └── msg-queue.js # Messages to send queue, one per peer +├── constants.js +├── index.js +└── types + ├── message # (Type) message that is put in the wire + │   ├── entry.js + │   ├── index.js + │   └── message.proto.js + └── wantlist # (Type) track wanted blocks + ├── entry.js + └── index.js +``` + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipfs-bitswap/issues)! diff --git a/benchmarks/index.js b/benchmarks/index.js index 73537139..2652449d 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -11,6 +11,7 @@ const Block = require('ipfs-block') const pull = require('pull-stream') const assert = require('assert') const crypto = require('crypto') +const CID = require('cids') const utils = require('../test/utils') @@ -54,6 +55,7 @@ function round (nodeArr, blockFactor, n, cb) { if (err) { return cb(err) } + const cids = keys.map((k) => new CID(k)) let d series([ // put blockFactor amount of blocks per node @@ -63,8 +65,8 @@ function round (nodeArr, blockFactor, n, cb) { const data = _.map(_.range(blockFactor), (j) => { const index = i * blockFactor + j return { - data: blocks[index].data, - key: keys[index] + block: blocks[index], + cid: cids[index] } }) each( @@ -80,7 +82,7 @@ function round (nodeArr, blockFactor, n, cb) { // fetch all blocks on every node (cb) => parallel(_.map(nodeArr, (node, i) => (callback) => { pull( - node.bitswap.getStream(keys), + node.bitswap.getStream(cids), pull.collect((err, res) => { if (err) { return callback(err) diff --git a/benchmarks/put-get.js b/benchmarks/put-get.js index 333f0353..33f9d8a8 100644 --- a/benchmarks/put-get.js +++ b/benchmarks/put-get.js @@ -7,6 +7,8 @@ const assert = require('assert') const pull = require('pull-stream') const series = require('async/series') const crypto = require('crypto') +const CID = require('cids') + const utils = require('../test/utils') const suite = new Benchmark.Suite('put-get') @@ -64,7 +66,7 @@ function put (blocks, bs, callback) { if (err) { return cb(err) } - cb(null, {key: key, data: b.data}) + cb(null, {cid: new CID(key), block: b}) }) }), bs.putStream(), @@ -76,7 +78,7 @@ function get (blocks, bs, callback) { pull( pull.values(blocks), pull.asyncMap((b, cb) => b.key(cb)), - pull.map((k) => bs.getStream(k)), + pull.map((k) => bs.getStream(new CID(k))), pull.flatten(), pull.collect((err, res) => { if (err) { diff --git a/img/architecture.monopic b/img/architecture.monopic new file mode 100644 index 0000000000000000000000000000000000000000..759155641e96ced72abb17f2a368a6cb55aa16ae GIT binary patch literal 4281 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 bklm;7)NY6Tl6)@K{^{xeNDH|(Gr#}<3A8)G literal 0 HcmV?d00001 diff --git a/img/architecture.png b/img/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..32b5967bc2c6294239d9e9f0d67a893fea670d48 GIT binary patch literal 103613 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 KRrJE}-TwnQzwZ$M literal 0 HcmV?d00001 diff --git a/img/architecture.txt b/img/architecture.txt new file mode 100644 index 00000000..736815e2 --- /dev/null +++ b/img/architecture.txt @@ -0,0 +1,51 @@ + + ┌────────────────────────────────────────────────────────────────────────────┐ + │ Bitswap API │ + └────────────────────────────────────────────────────────────────────────────┘ + │ ▲ + │register wants │yields the + │and unwants │received + │ │blocks + │ │ + │ send block to other nodes┌───────────────────────────┐ + │ by adding them to their │ Decision Engine │ + │ buckets │ │ + │ ┌─────────◀├ ─ ─ ─ │ + │ │ │Ledger│ │ + │ │ └───────────────────────────┘ + │ │ ▲ + ▼ │ │ + ┌────────────────────┐ │ │ + │ │ │ │ + │ Want Manager │ │ │ + │ │ │ │ + ├───────────┬──┬─────┘ │ │ + │my wantlist│ │ │ │ + └───────────┘ │update wantlist │ │ + │messages │ receive a block │ + └─────┬───────┬───────┤ │ + │ │ │ │ + ▼ ▼ ▼ │ + ┌───────┬───────┬───────┐ │ + │Message│Message│... │ │ + │Queue/ │Queue/ │ │ │ + │peer │peer │ │ │ + └───────┴───────┴───────┘ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + └───────┴───────┴─┐ │ + │ │ + │ │ + ▼ │ + ┌────────────────────────────────────────┐ + │ Network │ + └────────────────────────────────────────┘ + │ ▲ │ ▲ + ▼ │ │ │ + ┌─────────┐ ▼ │ + │Transform│ /ipfs/bitswap/1.1.0 + └─────────┘ + │ ▲ + ▼ │ + /ipfs/bitswap/1.0.0 \ No newline at end of file diff --git a/package.json b/package.json index e22c6bd0..c68386b8 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "homepage": "https://github.com/ipfs/js-ipfs-bitswap#readme", "devDependencies": { - "aegir": "9.2.2", + "aegir": "9.3.0", "benchmark": "^2.1.2", "buffer-loader": "0.0.1", "chai": "^3.5.0", @@ -56,8 +56,8 @@ }, "dependencies": { "async": "^2.1.4", - "cids": "^0.3.4", - "debug": "^2.3.3", + "cids": "^0.3.5", + "debug": "^2.4.4", "heap": "^0.2.6", "ipfs-block": "^0.5.3", "lodash.debounce": "^4.0.8", @@ -68,13 +68,14 @@ "lodash.pullallwith": "^4.7.0", "lodash.uniqwith": "^4.5.0", "lodash.values": "^4.3.0", - "multihashes": "^0.3.0", + "multihashes": "^0.3.1", "protocol-buffers": "^3.2.1", "pull-defer": "^0.2.2", "pull-length-prefixed": "^1.2.0", "pull-paramap": "^1.2.1", "pull-pushable": "^2.0.1", - "pull-stream": "^3.5.0" + "pull-stream": "^3.5.0", + "varint-decoder": "^0.1.1" }, "contributors": [ "David Dias ", diff --git a/src/decision/engine.js b/src/components/decision-engine/index.js similarity index 78% rename from src/decision/engine.js rename to src/components/decision-engine/index.js index f1462351..5617a6ee 100644 --- a/src/decision/engine.js +++ b/src/components/decision-engine/index.js @@ -3,23 +3,24 @@ const debug = require('debug') const pull = require('pull-stream') const each = require('async/each') -const map = require('async/map') const waterfall = require('async/waterfall') +const map = require('async/map') const debounce = require('lodash.debounce') const uniqWith = require('lodash.uniqwith') const find = require('lodash.find') const values = require('lodash.values') const groupBy = require('lodash.groupby') const pullAllWith = require('lodash.pullallwith') +const CID = require('cids') const log = debug('bitswap:engine') log.error = debug('bitswap:engine:error') -const Message = require('../message') -const Wantlist = require('../wantlist') +const Message = require('../../types/message') +const Wantlist = require('../../types/wantlist') const Ledger = require('./ledger') -module.exports = class Engine { +class DecisionEngine { constructor (blockstore, network) { this.blockstore = blockstore this.network = network @@ -36,8 +37,9 @@ module.exports = class Engine { _sendBlocks (env, cb) { const msg = new Message(false) - env.blocks.forEach((block) => { - msg.addBlockWithKey(block.block, block.key) + + env.blocks.forEach((b) => { + msg.addBlock(b.cid, b.block) }) // console.log('sending %s blocks', msg.blocks.size) @@ -57,20 +59,20 @@ module.exports = class Engine { const tasks = this._tasks this._tasks = [] const entries = tasks.map((t) => t.entry) - const keys = entries.map((e) => e.key) - const uniqKeys = uniqWith(keys, (a, b) => a.equals(b)) + const cids = entries.map((e) => e.cid) + const uniqCids = uniqWith(cids, (a, b) => a.equals(b)) const groupedTasks = groupBy(tasks, (task) => task.target.toB58String()) waterfall([ - (cb) => map(uniqKeys, (k, cb) => { + (cb) => map(uniqCids, (cid, cb) => { pull( - this.blockstore.getStream(k), + this.blockstore.getStream(cid.multihash), pull.collect((err, blocks) => { if (err) { return cb(err) } cb(null, { - key: k, + cid: cid, block: blocks[0] }) }) @@ -79,8 +81,8 @@ module.exports = class Engine { (blocks, cb) => each(values(groupedTasks), (tasks, cb) => { // all tasks have the same target const peer = tasks[0].target - const blockList = keys.map((k) => { - return find(blocks, (b) => b.key.equals(k)) + const blockList = cids.map((cid) => { + return find(blocks, (b) => b.cid.equals(cid)) }) this._sendBlocks({ @@ -91,7 +93,7 @@ module.exports = class Engine { log.error('failed to send', err) } blockList.forEach((block) => { - this.messageSent(peer, block.block, block.key) + this.messageSent(peer, block.block, block.cid) }) cb() }) @@ -105,24 +107,25 @@ module.exports = class Engine { } wantlistForPeer (peerId) { - if (!this.ledgerMap.has(peerId.toB58String())) { + const peerIdStr = peerId.toB58String() + if (!this.ledgerMap.has(peerIdStr)) { return new Map() } - return this.ledgerMap.get(peerId.toB58String()).wantlist.sortedEntries() + return this.ledgerMap.get(peerIdStr).wantlist.sortedEntries() } peers () { return Array.from(this.ledgerMap.values()).map((l) => l.partner) } - receivedBlocks (keys) { - if (!keys.length) { + receivedBlocks (cids) { + if (!cids.length) { return } // Check all connected peers if they want the block we received for (let l of this.ledgerMap.values()) { - keys + cids .map((k) => l.wantlistContains(k)) .filter(Boolean) .forEach((e) => { @@ -162,10 +165,10 @@ module.exports = class Engine { let wants = [] for (let entry of msg.wantlist.values()) { if (entry.cancel) { - ledger.cancelWant(entry.key) + ledger.cancelWant(entry.cid) cancels.push(entry) } else { - ledger.wants(entry.key, entry.priority) + ledger.wants(entry.cid, entry.priority) wants.push(entry) } } @@ -180,15 +183,15 @@ module.exports = class Engine { pullAllWith(this._tasks, entries, (t, e) => { const sameTarget = t.target.toB58String() === id - const sameKey = t.entry.key.equals(e.key) - return sameTarget && sameKey + const sameCid = t.entry.cid.equals(e.cid) + return sameTarget && sameCid }) } _addWants (ledger, peerId, entries, cb) { each(entries, (entry, cb) => { // If we already have the block, serve it - this.blockstore.has(entry.key, (err, exists) => { + this.blockstore.has(entry.cid.multihash, (err, exists) => { if (err) { log.error('failed existence check') } else if (exists) { @@ -214,24 +217,24 @@ module.exports = class Engine { log('got block (%s bytes)', block.data.length) ledger.receivedBytes(block.data.length) - cb(null, key) + cb(null, new CID(key)) }) - }, (err, keys) => { + }, (err, cids) => { if (err) { return callback(err) } - this.receivedBlocks(keys) + this.receivedBlocks(cids) callback() }) } // Clear up all accounting things after message was sent - messageSent (peerId, block, key) { + messageSent (peerId, block, cid) { const ledger = this._findOrCreate(peerId) ledger.sentBytes(block ? block.data.length : 0) - if (key) { - ledger.wantlist.remove(key) + if (cid) { + ledger.wantlist.remove(cid) } } @@ -249,16 +252,18 @@ module.exports = class Engine { // } // // TODO: figure out how to remove all other references - // in the peerrequestqueue + // in the peer request queue } _findOrCreate (peerId) { - if (this.ledgerMap.has(peerId.toB58String())) { - return this.ledgerMap.get(peerId.toB58String()) + const peerIdStr = peerId.toB58String() + if (this.ledgerMap.has(peerIdStr)) { + return this.ledgerMap.get(peerIdStr) } const l = new Ledger(peerId) - this.ledgerMap.set(peerId.toB58String(), l) + + this.ledgerMap.set(peerIdStr, l) return l } @@ -271,3 +276,5 @@ module.exports = class Engine { this._running = false } } + +module.exports = DecisionEngine diff --git a/src/decision/ledger.js b/src/components/decision-engine/ledger.js similarity index 59% rename from src/decision/ledger.js rename to src/components/decision-engine/ledger.js index 3d98069f..a4619078 100644 --- a/src/decision/ledger.js +++ b/src/components/decision-engine/ledger.js @@ -1,8 +1,8 @@ 'use strict' -const Wantlist = require('../wantlist') +const Wantlist = require('../../types/wantlist') -module.exports = class Ledger { +class Ledger { constructor (peerId) { this.partner = peerId this.wantlist = new Wantlist() @@ -17,26 +17,28 @@ module.exports = class Ledger { } sentBytes (n) { - this.exchangeCount ++ + this.exchangeCount++ this.lastExchange = (new Date()).getTime() this.accounting.bytesSent += n } receivedBytes (n) { - this.exchangeCount ++ + this.exchangeCount++ this.lastExchange = (new Date()).getTime() this.accounting.bytesRecv += n } - wants (key, priority) { - this.wantlist.add(key, priority) + wants (cid, priority) { + this.wantlist.add(cid, priority) } - cancelWant (key) { - this.wantlist.remove(key) + cancelWant (cid) { + this.wantlist.remove(cid) } - wantlistContains (key) { - return this.wantlist.contains(key) + wantlistContains (cid) { + return this.wantlist.contains(cid) } } + +module.exports = Ledger diff --git a/src/network/index.js b/src/components/network/index.js similarity index 61% rename from src/network/index.js rename to src/components/network/index.js index 07a2b2b4..34e5589f 100644 --- a/src/network/index.js +++ b/src/components/network/index.js @@ -6,34 +6,38 @@ const pull = require('pull-stream') const pushable = require('pull-pushable') const setImmediate = require('async/setImmediate') -const Message = require('../message') -const cs = require('../constants') +const Message = require('../../types/message') +const CONSTANTS = require('../../constants') const log = debug('bitswap:network') log.error = debug('bitswap:network:error') -const PROTOCOL_IDENTIFIER = '/ipfs/bitswap/1.0.0' +const BITSWAP100 = '/ipfs/bitswap/1.0.0' +const BITSWAP110 = '/ipfs/bitswap/1.1.0' -module.exports = class Network { - constructor (libp2p, peerBook, bitswap) { +class Network { + constructor (libp2p, peerBook, bitswap, b100Only) { this.libp2p = libp2p this.peerBook = peerBook this.bitswap = bitswap this.conns = new Map() + this.b100Only = b100Only || false // increase event listener max - this.libp2p.swarm.setMaxListeners(cs.maxListeners) - this._running = false + this.libp2p.swarm.setMaxListeners(CONSTANTS.maxListeners) } start () { this._running = true // bind event listeners - this._onConnection = this._onConnection.bind(this) this._onPeerMux = this._onPeerMux.bind(this) this._onPeerMuxClosed = this._onPeerMuxClosed.bind(this) - this.libp2p.handle(PROTOCOL_IDENTIFIER, this._onConnection) + this._onConnection = this._onConnection.bind(this) + this.libp2p.handle(BITSWAP100, this._onConnection) + if (!this.b100Only) { + this.libp2p.handle(BITSWAP110, this._onConnection) + } this.libp2p.swarm.on('peer-mux-established', this._onPeerMux) this.libp2p.swarm.on('peer-mux-closed', this._onPeerMuxClosed) @@ -47,12 +51,17 @@ module.exports = class Network { stop () { this._running = false - this.libp2p.unhandle(PROTOCOL_IDENTIFIER) - this.libp2p.swarm.removeListener('peer-mux-established', this._onPeerMux) + this.libp2p.unhandle(BITSWAP100) + if (!this.b100Only) { + this.libp2p.unhandle(BITSWAP110) + } + + this.libp2p.swarm.removeListener('peer-mux-established', this._onPeerMux) this.libp2p.swarm.removeListener('peer-mux-closed', this._onPeerMuxClosed) } + // Handles both types of bitswap messgages _onConnection (protocol, conn) { if (!this._running) { return @@ -61,7 +70,7 @@ module.exports = class Network { pull( conn, lp.decode(), - pull.asyncMap((data, cb) => Message.fromProto(data, cb)), + pull.asyncMap((data, cb) => Message.deserialize(data, cb)), pull.asyncMap((msg, cb) => { conn.getPeerInfo((err, peerInfo) => { if (err) { @@ -96,8 +105,8 @@ module.exports = class Network { } // Connect to the given peer - connectTo (peerId, cb) { - const done = (err) => setImmediate(() => cb(err)) + connectTo (peerId, callback) { + const done = (err) => setImmediate(() => callback(err)) if (!this._running) { return done(new Error('No running network')) @@ -114,9 +123,9 @@ module.exports = class Network { } // Send the given msg (instance of Message) to the given peer - sendMessage (peerId, msg, cb) { + sendMessage (peerId, msg, callback) { if (!this._running) { - return cb(new Error('No running network')) + return callback(new Error('No running network')) } const stringId = peerId.toB58String() @@ -125,27 +134,50 @@ module.exports = class Network { try { peerInfo = this.peerBook.getByB58String(stringId) } catch (err) { - return cb(err) + return callback(err) } if (this.conns.has(stringId)) { - log('connection exists') - this.conns.get(stringId).push(msg.toProto()) - return cb() + this.conns.get(stringId)(msg) + return callback() } - log('dialByPeerInfo') - this.libp2p.dialByPeerInfo(peerInfo, PROTOCOL_IDENTIFIER, (err, conn) => { - // log('dialed %s', peerInfo.id.toB58String(), err) + const msgQueue = pushable() + + // Attempt Bitswap 1.1.0 + this.libp2p.dialByPeerInfo(peerInfo, BITSWAP110, (err, conn) => { if (err) { - return cb(err) + // Attempt Bitswap 1.0.0 + this.libp2p.dialByPeerInfo(peerInfo, BITSWAP100, (err, conn) => { + if (err) { + return callback(err) + } + log('dialed %s on Bitswap 1.0.0', peerInfo.id.toB58String()) + + this.conns.set(stringId, (msg) => { + msgQueue.push(msg.serializeToBitswap100()) + }) + + this.conns.get(stringId)(msg) + + withConn(this.conns, conn) + callback() + }) + return } + log('dialed %s on Bitswap 1.1.0', peerInfo.id.toB58String()) - const msgQueue = pushable() - msgQueue.push(msg.toProto()) + this.conns.set(stringId, (msg) => { + msgQueue.push(msg.serializeToBitswap110()) + }) - this.conns.set(stringId, msgQueue) + this.conns.get(stringId)(msg) + withConn(this.conns, conn) + callback() + }) + + function withConn (conns, conn) { pull( msgQueue, lp.encode(), @@ -155,11 +187,11 @@ module.exports = class Network { log.error(err) } msgQueue.end() - this.conns.delete(stringId) + conns.delete(stringId) }) ) - - cb() - }) + } } } + +module.exports = Network diff --git a/src/wantmanager/index.js b/src/components/want-manager/index.js similarity index 64% rename from src/wantmanager/index.js rename to src/components/want-manager/index.js index a7220478..c963a819 100644 --- a/src/wantmanager/index.js +++ b/src/components/want-manager/index.js @@ -2,38 +2,38 @@ const debug = require('debug') -const Message = require('../message') -const Wantlist = require('../wantlist') -const cs = require('../constants') +const Message = require('../../types/message') +const Wantlist = require('../../types/wantlist') +const CONSTANTS = require('../../constants') const MsgQueue = require('./msg-queue') const log = debug('bitswap:wantmanager') log.error = debug('bitswap:wantmanager:error') -module.exports = class Wantmanager { +module.exports = class WantManager { constructor (network) { this.peers = new Map() - this.wl = new Wantlist() + this.wantlist = new Wantlist() this.network = network } - _addEntries (keys, cancel, force) { - const entries = keys.map((key, i) => { - return new Message.Entry(key, cs.kMaxPriority - i, cancel) + _addEntries (cids, cancel, force) { + const entries = cids.map((cid, i) => { + return new Message.Entry(cid, CONSTANTS.kMaxPriority - i, cancel) }) entries.forEach((e) => { // add changes to our wantlist if (e.cancel) { if (force) { - this.wl.removeForce(e.key) + this.wantlist.removeForce(e.cid) } else { - this.wl.remove(e.key) + this.wantlist.remove(e.cid) } } else { log('adding to wl') - this.wl.add(e.key, e.priority) + this.wantlist.add(e.cid, e.priority) } }) @@ -55,8 +55,9 @@ module.exports = class Wantmanager { // new peer, give them the full wantlist const fullwantlist = new Message(true) - for (let entry of this.wl.entries()) { - fullwantlist.addEntry(entry[1].key, entry[1].priority) + + for (let entry of this.wantlist.entries()) { + fullwantlist.addEntry(entry[1].cid, entry[1].priority) } mq.addMessage(fullwantlist) @@ -80,21 +81,21 @@ module.exports = class Wantmanager { this.peers.delete(peerId.toB58String()) } - // add all the keys to the wantlist - wantBlocks (keys) { - this._addEntries(keys, false) + // add all the cids to the wantlist + wantBlocks (cids) { + this._addEntries(cids, false) } // remove blocks of all the given keys without respecting refcounts - unwantBlocks (keys) { - log('unwant blocks: %s', keys.length) - this._addEntries(keys, true, true) + unwantBlocks (cids) { + log('unwant blocks: %s', cids.length) + this._addEntries(cids, true, true) } // cancel wanting all of the given keys - cancelWants (keys) { - log('cancel wants: %s', keys.length) - this._addEntries(keys, true) + cancelWants (cids) { + log('cancel wants: %s', cids.length) + this._addEntries(cids, true) } // Returns a list of all currently connected peers @@ -114,8 +115,8 @@ module.exports = class Wantmanager { this.timer = setInterval(() => { // resend entirew wantlist every so often const fullwantlist = new Message(true) - for (let entry of this.wl.entries()) { - fullwantlist.addEntry(entry[1].key, entry[1].priority) + for (let entry of this.wantlist.entries()) { + fullwantlist.addEntry(entry[1].cid, entry[1].priority) } this.peers.forEach((p) => { @@ -126,7 +127,7 @@ module.exports = class Wantmanager { stop () { for (let mq of this.peers.values()) { - this.disconnected(mq.id) + this.disconnected(mq.peerId) } clearInterval(this.timer) } diff --git a/src/wantmanager/msg-queue.js b/src/components/want-manager/msg-queue.js similarity index 68% rename from src/wantmanager/msg-queue.js rename to src/components/want-manager/msg-queue.js index 8a629428..3ea18ac0 100644 --- a/src/wantmanager/msg-queue.js +++ b/src/components/want-manager/msg-queue.js @@ -2,14 +2,14 @@ const debug = require('debug') const debounce = require('lodash.debounce') -const Message = require('../message') +const Message = require('../../types/message') const log = debug('bitswap:wantmanager:queue') log.error = debug('bitswap:wantmanager:queue:error') module.exports = class MsgQueue { constructor (peerId, network) { - this.id = peerId + this.peerId = peerId this.network = network this.refcnt = 1 @@ -31,14 +31,16 @@ module.exports = class MsgQueue { } _sendEntries () { - if (!this._entries.length) return + if (!this._entries.length) { + return + } const msg = new Message(false) this._entries.forEach((entry) => { if (entry.cancel) { - msg.cancel(entry.key) + msg.cancel(entry.cid) } else { - msg.addEntry(entry.key, entry.priority) + msg.addEntry(entry.cid, entry.priority) } }) this._entries = [] @@ -46,14 +48,13 @@ module.exports = class MsgQueue { } send (msg) { - this.network.connectTo(this.id, (err) => { + this.network.connectTo(this.peerId, (err) => { if (err) { - log.error('cant connect to peer %s: %s', this.id.toB58String(), err.message) + log.error('cant connect to peer %s: %s', this.peerId.toB58String(), err.message) return } log('sending message') - // console.log('sending msg %s blocks, %s wants', msg.blocks.size, msg.wantlist.size) - this.network.sendMessage(this.id, msg, (err) => { + this.network.sendMessage(this.peerId, msg, (err) => { if (err) { log.error('send error: %s', err.message) return diff --git a/src/constants.js b/src/constants.js index b612d3bc..3edc1231 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,13 +1,13 @@ 'use strict' -const second = 1000 +const SECOND = 1000 module.exports = { maxProvidersPerRequest: 3, - providerRequestTimeout: 10 * second, - hasBlockTimeout: 15 * second, - provideTimeout: 15 * second, + providerRequestTimeout: 10 * SECOND, + hasBlockTimeout: 15 * SECOND, + provideTimeout: 15 * SECOND, kMaxPriority: Math.pow(2, 31) - 1, - rebroadcastDelay: 10 * second, + rebroadcastDelay: 10 * SECOND, maxListeners: 1000 } diff --git a/src/decision/index.js b/src/decision/index.js deleted file mode 100644 index 9e6bb4ad..00000000 --- a/src/decision/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -const Engine = require('./engine') - -exports.Engine = Engine diff --git a/src/decision/pq.js b/src/decision/pq.js deleted file mode 100644 index 427e71b5..00000000 --- a/src/decision/pq.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict' - -const Heap = require('heap') - -module.exports = class PriorityQueue { - constructor (cmp) { - this.q = new Heap((a, b) => { - return cmp(a, b) ? -1 : 1 - }) - } - - push (e) { - this.q.push(e) - } - - pop () { - return this.q.pop() - } - - update (e) { - this.q.updateItem(e) - } - - size () { - return this.q.size() - } - - isEmpty () { - return this.q.empty() - } -} diff --git a/src/index.js b/src/index.js index ad1e9557..e3831546 100644 --- a/src/index.js +++ b/src/index.js @@ -2,30 +2,30 @@ const series = require('async/series') const debug = require('debug') + const log = debug('bitswap') log.error = debug('bitswap:error') const EventEmitter = require('events').EventEmitter const pull = require('pull-stream') const paramap = require('pull-paramap') const defer = require('pull-defer/source') +const CID = require('cids') -const cs = require('./constants') -const WantManager = require('./wantmanager') -const Network = require('./network') -const decision = require('./decision') - -module.exports = class Bitwap { - constructor (p, libp2p, blockstore, peerBook) { - // the ID of the peer to act on behalf of - this.self = p +const CONSTANTS = require('./constants') +const WantManager = require('./components/want-manager') +const Network = require('./components/network') +const DecisionEngine = require('./components/decision-engine') +class Bitswap { + constructor (libp2p, blockstore, peerBook) { + this.libp2p = libp2p // the network delivers messages this.network = new Network(libp2p, peerBook, this) // local database this.blockstore = blockstore - this.engine = new decision.Engine(blockstore, this.network) + this.engine = new DecisionEngine(blockstore, this.network) // handle message sending this.wm = new WantManager(this.network) @@ -35,7 +35,7 @@ module.exports = class Bitwap { this.dupDataRecvd = 0 this.notifications = new EventEmitter() - this.notifications.setMaxListeners(cs.maxListeners) + this.notifications.setMaxListeners(CONSTANTS.maxListeners) } // handle messages received through the network @@ -46,79 +46,81 @@ module.exports = class Bitwap { log('failed to receive message', incoming) } - const iblocks = Array.from(incoming.blocks.values()) + const cidsAndBlocks = Array + .from(incoming.blocks.entries()) + .map((entry) => { + return { cid: new CID(entry[0]), block: entry[1] } + }) - if (iblocks.length === 0) { + if (cidsAndBlocks.length === 0) { return cb() } // quickly send out cancels, reduces chances of duplicate block receives - pull( - pull.values(iblocks), - pull.asyncMap((block, cb) => block.key(cb)), - pull.filter((key) => this.wm.wl.contains(key)), - pull.collect((err, keys) => { + pull.values(cidsAndBlocks), + pull.filter((cidAndBlock) => this.wm.wantlist.contains(cidAndBlock.cid)), + pull.collect((err, cidsAndBlocks) => { if (err) { return log.error(err) } - this.wm.cancelWants(keys) + const cids = cidsAndBlocks.map((entry) => entry.cid) + + this.wm.cancelWants(cids) }) ) pull( - pull.values(iblocks), + pull.values(cidsAndBlocks), paramap(this._handleReceivedBlock.bind(this, peerId), 10), pull.onEnd(cb) ) }) } - _handleReceivedBlock (peerId, block, cb) { + _handleReceivedBlock (peerId, cidAndBlock, callback) { series([ - (cb) => this._updateReceiveCounters(block, (err) => { + (cb) => this._updateReceiveCounters(cidAndBlock.block, (err) => { if (err) { // ignore, as these have been handled // in _updateReceiveCounters return cb() } + log('got block from %s', peerId.toB58String(), cidAndBlock.block.data.length) cb() }), - (cb) => block.key((err, key) => { - if (err) { - return cb(err) - } - this.put({data: block.data, key: key}, (err) => { + (cb) => { + this.put(cidAndBlock, (err) => { if (err) { log.error('receiveMessage put error: %s', err.message) } cb() }) - }) - ], cb) + } + ], callback) } - _updateReceiveCounters (block, cb) { - this.blocksRecvd ++ + _updateReceiveCounters (block, callback) { + this.blocksRecvd++ block.key((err, key) => { if (err) { - return cb(err) + return callback(err) } this.blockstore.has(key, (err, has) => { if (err) { log('blockstore.has error: %s', err.message) - return cb(err) + return callback(err) } if (has) { this.dupBlocksRecvd ++ this.dupDataRecvd += block.data.length - return cb(new Error('Already have block')) + return callback(new Error('Already have block')) } - cb() + callback() }) }) } @@ -144,16 +146,16 @@ module.exports = class Bitwap { return this.engine.wantlistForPeer(peerId) } - getStream (keys) { - if (!Array.isArray(keys)) { - return this._getStreamSingle(keys) + getStream (cids) { + if (!Array.isArray(cids)) { + return this._getStreamSingle(cids) } return pull( - pull.values(keys), - paramap((key, cb) => { + pull.values(cids), + paramap((cid, cb) => { pull( - this._getStreamSingle(key), + this._getStreamSingle(cid), pull.collect(cb) ) }), @@ -161,102 +163,107 @@ module.exports = class Bitwap { ) } - _getStreamSingle (key) { + _getStreamSingle (cid) { const unwantListeners = {} const blockListeners = {} - const keyS = key.toString() + const cidStr = cid.buffer.toString() + const unwantEvent = `unwant:${cidStr}` + const blockEvent = `block:${cidStr}` - const unwantEvent = () => `unwant:${keyS}` - const blockEvent = () => `block:${keyS}` const d = defer() const cleanupListener = () => { - if (unwantListeners[keyS]) { - this.notifications.removeListener(unwantEvent(), unwantListeners[keyS]) - delete unwantListeners[keyS] + if (unwantListeners[cidStr]) { + this.notifications.removeListener(unwantEvent, unwantListeners[cidStr]) + delete unwantListeners[cidStr] } - if (blockListeners[keyS]) { - this.notifications.removeListener(blockEvent(), blockListeners[keyS]) - delete blockListeners[keyS] + if (blockListeners[cidStr]) { + this.notifications.removeListener(blockEvent, blockListeners[cidStr]) + delete blockListeners[cidStr] } } const addListener = () => { - unwantListeners[keyS] = () => { - log(`manual unwant: ${keyS}`) + unwantListeners[cidStr] = () => { + log(`manual unwant: ${cidStr}`) cleanupListener() - this.wm.cancelWants([key]) + this.wm.cancelWants([cid]) d.resolve(pull.empty()) } - blockListeners[keyS] = (block) => { - this.wm.cancelWants([key]) - cleanupListener() + blockListeners[cidStr] = (block) => { + this.wm.cancelWants([cid]) + cleanupListener(cid) d.resolve(pull.values([block])) } - this.notifications.once(unwantEvent(), unwantListeners[keyS]) - this.notifications.once(blockEvent(), blockListeners[keyS]) + this.notifications.once(unwantEvent, unwantListeners[cidStr]) + this.notifications.once(blockEvent, blockListeners[cidStr]) } - this.blockstore.has(key, (err, exists) => { + this.blockstore.has(cid.multihash, (err, exists) => { if (err) { return d.resolve(pull.error(err)) } if (exists) { - log('already have block') - return d.resolve(this.blockstore.getStream(key)) + log('already have block: %s', cidStr) + return d.resolve(this.blockstore.getStream(cid.multihash)) } addListener() - this.wm.wantBlocks([key]) + this.wm.wantBlocks([cid]) }) return d } - // removes the given keys from the want list independent of any ref counts - unwant (keys) { - if (!Array.isArray(keys)) { - keys = [keys] + // removes the given cids from the wantlist independent of any ref counts + unwant (cids) { + if (!Array.isArray(cids)) { + cids = [cids] } - this.wm.unwantBlocks(keys) - keys.forEach((key) => { - this.notifications.emit(`unwant:${key.toString()}`) + this.wm.unwantBlocks(cids) + cids.forEach((cid) => { + this.notifications.emit(`unwant:${cid.buffer.toString()}`) }) } // removes the given keys from the want list - cancelWants (keys) { - if (!Array.isArray(keys)) { - keys = [keys] + cancelWants (cids) { + if (!Array.isArray(cids)) { + cids = [cids] } - this.wm.cancelWants(keys) + this.wm.cancelWants(cids) } putStream () { return pull( - pull.asyncMap((blockAndKey, cb) => { - this.blockstore.has(blockAndKey.key, (err, exists) => { + pull.asyncMap((blockAndCid, cb) => { + this.blockstore.has(blockAndCid.cid.multihash, (err, exists) => { if (err) { return cb(err) } - cb(null, [blockAndKey, exists]) + + cb(null, [blockAndCid, exists]) }) }), pull.filter((val) => !val[1]), pull.map((val) => { - const block = val[0] + const block = val[0].block + const cid = val[0].cid log('putting block') return pull( - pull.values([block]), + pull.values([{ + data: block.data, + key: cid.multihash + }]), this.blockstore.putStream(), pull.through(() => { log('put block') - this.notifications.emit(`block:${block.key.toString()}`, block) - this.engine.receivedBlocks([block.key]) + this.notifications.emit(`block:${cid.buffer.toString()}`, block) + this.engine.receivedBlocks([cid]) }) ) }), @@ -265,16 +272,16 @@ module.exports = class Bitwap { } // announces the existance of a block to this service - put (blockAndKey, cb) { + put (blockAndCid, callback) { pull( - pull.values([blockAndKey]), + pull.values([blockAndCid]), this.putStream(), - pull.onEnd(cb) + pull.onEnd(callback) ) } getWantlist () { - return this.wm.wl.entries() + return this.wm.wantlist.entries() } stat () { @@ -295,8 +302,10 @@ module.exports = class Bitwap { // Halt everything stop () { - this.wm.stop() + this.wm.stop(this.libp2p.peerInfo.id) this.network.stop() this.engine.stop() } } + +module.exports = Bitswap diff --git a/src/message/entry.js b/src/message/entry.js deleted file mode 100644 index 85e592c0..00000000 --- a/src/message/entry.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -const WantlistEntry = require('../wantlist').Entry - -module.exports = class BitswapMessageEntry { - constructor (key, priority, cancel) { - this.entry = new WantlistEntry(key, priority) - this.cancel = Boolean(cancel) - } - - get key () { - return this.entry.key - } - - set key (val) { - this.entry.key = val - } - - get priority () { - return this.entry.priority - } - - set priority (val) { - this.entry.priority = val - } - - get [Symbol.toStringTag] () { - return `BitswapMessageEntry ${this.toB58String()} ` - } - - toB58String () { - return this.entry.toB58String() - } - - equals (other) { - return (this.cancel === other.cancel) && this.entry.equals(other.entry) - } -} diff --git a/src/message/index.js b/src/message/index.js deleted file mode 100644 index 8f0e9db1..00000000 --- a/src/message/index.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict' - -const protobuf = require('protocol-buffers') -const Block = require('ipfs-block') -const isEqualWith = require('lodash.isequalwith') -const map = require('async/map') - -const pbm = protobuf(require('./message.proto')) -const Entry = require('./entry') - -class BitswapMessage { - constructor (full) { - this.full = full - this.wantlist = new Map() - this.blocks = new Map() - } - - get empty () { - return this.blocks.size === 0 && this.wantlist.size === 0 - } - - addEntry (key, priority, cancel) { - const e = this.wantlist.get(key.toString()) - - if (e) { - e.priority = priority - e.cancel = Boolean(cancel) - } else { - this.wantlist.set(key.toString(), new Entry(key, priority, cancel)) - } - } - - addBlock (block, cb) { - block.key((err, key) => { - if (err) { - return cb(err) - } - this.blocks.set(key.toString(), block) - cb() - }) - } - - addBlockWithKey (block, key) { - this.blocks.set(key.toString(), block) - } - - cancel (key) { - const keyS = key.toString() - if (this.wantlist.has(keyS)) { - this.wantlist.delete(keyS) - } else { - this.wantlist.set(keyS, new Entry(key, 0, true)) - } - } - - toProto () { - const msg = { - wantlist: { - entries: Array.from(this.wantlist.values()).map((e) => { - return { - block: e.key, - priority: Number(e.priority), - cancel: Boolean(e.cancel) - } - }) - }, - blocks: Array.from(this.blocks.values()) - .map((b) => b.data) - } - - if (this.full) { - msg.wantlist.full = true - } - - return pbm.Message.encode(msg) - } - - equals (other) { - const cmp = (a, b) => { - if (a.equals && typeof a.equals === 'function') { - return a.equals(b) - } - } - - if (this.full !== other.full || - !isEqualWith(this.wantlist, other.wantlist, cmp) || - !isEqualWith(this.blocks, other.blocks, cmp) - ) { - return false - } - - return true - } - - get [Symbol.toStringTag] () { - const list = Array.from(this.wantlist.keys()) - const blocks = Array.from(this.blocks.keys()) - return `BitswapMessage ` - } -} - -BitswapMessage.fromProto = (raw, callback) => { - const dec = pbm.Message.decode(raw) - const m = new BitswapMessage(dec.wantlist.full) - - dec.wantlist.entries.forEach((e) => { - m.addEntry(e.block, e.priority, e.cancel) - }) - - map(dec.blocks, (b, cb) => m.addBlock(new Block(b), cb), (err) => { - if (err) { - return callback(err) - } - callback(null, m) - }) -} - -BitswapMessage.Entry = Entry -module.exports = BitswapMessage diff --git a/src/message/message.proto.js b/src/message/message.proto.js deleted file mode 100644 index 6038b42a..00000000 --- a/src/message/message.proto.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -module.exports = `package bitswap.message.pb; - -message Message { - - message Wantlist { - - message Entry { - optional bytes block = 1; // the block key - optional int32 priority = 2; // the priority (normalized). default to 1 - optional bool cancel = 3; // whether this revokes an entry - } - - repeated Entry entries = 1; // a list of wantlist entries - optional bool full = 2; // whether this is the full wantlist. default to false - } - - optional Wantlist wantlist = 1; - repeated bytes blocks = 2; -}` diff --git a/src/types/message/entry.js b/src/types/message/entry.js new file mode 100644 index 00000000..079ade8a --- /dev/null +++ b/src/types/message/entry.js @@ -0,0 +1,40 @@ +'use strict' + +const WantlistEntry = require('../wantlist').Entry +const CID = require('cids') +const assert = require('assert') + +module.exports = class BitswapMessageEntry { + constructor (cid, priority, cancel) { + assert(CID.isCID(cid), 'needs valid cid') + this.entry = new WantlistEntry(cid, priority) + this.cancel = Boolean(cancel) + } + + get cid () { + return this.entry.cid + } + + set cid (cid) { + this.entry.cid = cid + } + + get priority () { + return this.entry.priority + } + + set priority (val) { + this.entry.priority = val + } + + get [Symbol.toStringTag] () { + const cidStr = this.cid.toBaseEncodedString() + + return `BitswapMessageEntry ${cidStr} ` + } + + equals (other) { + return (this.cancel === other.cancel) && + this.entry.equals(other.entry) + } +} diff --git a/src/types/message/index.js b/src/types/message/index.js new file mode 100644 index 00000000..5e9f14c3 --- /dev/null +++ b/src/types/message/index.js @@ -0,0 +1,210 @@ +'use strict' + +const protobuf = require('protocol-buffers') +const Block = require('ipfs-block') +const isEqualWith = require('lodash.isequalwith') +const assert = require('assert') +const map = require('async/map') +const CID = require('cids') +const codecName = require('multicodec/src/name-table') +const vd = require('varint-decoder') + +const pbm = protobuf(require('./message.proto')) +const Entry = require('./entry') + +class BitswapMessage { + constructor (full) { + this.full = full + this.wantlist = new Map() + this.blocks = new Map() + } + + get empty () { + return this.blocks.size === 0 && + this.wantlist.size === 0 + } + + addEntry (cid, priority, cancel) { + assert(cid && CID.isCID(cid), 'must be a valid cid') + const cidStr = cid.toBaseEncodedString() + + const entry = this.wantlist.get(cidStr) + + if (entry) { + entry.priority = priority + entry.cancel = Boolean(cancel) + } else { + this.wantlist.set(cidStr, new Entry(cid, priority, cancel)) + } + } + + addBlock (cid, block) { + assert(CID.isCID(cid), 'must be a valid cid') + const cidStr = cid.toBaseEncodedString() + this.blocks.set(cidStr, block) + } + + cancel (cid) { + assert(CID.isCID(cid), 'must be a valid cid') + const cidStr = cid.toBaseEncodedString() + this.wantlist.delete(cidStr) + this.addEntry(cid, 0, true) + } + + /* + * Serializes to Bitswap Message protobuf of + * version 1.0.0 + */ + serializeToBitswap100 () { + const msg = { + wantlist: { + entries: Array.from(this.wantlist.values()).map((entry) => { + return { + block: entry.cid.buffer, // cid + priority: Number(entry.priority), + cancel: Boolean(entry.cancel) + } + }) + }, + blocks: Array.from(this.blocks.values()) + .map((block) => block.data) + } + + if (this.full) { + msg.wantlist.full = true + } + + return pbm.Message.encode(msg) + } + + /* + * Serializes to Bitswap Message protobuf of + * version 1.1.0 + */ + serializeToBitswap110 () { + const msg = { + wantlist: { + entries: Array.from(this.wantlist.values()).map((entry) => { + return { + block: entry.cid.buffer, // cid + priority: Number(entry.priority), + cancel: Boolean(entry.cancel) + } + }) + }, + payload: [] + } + + if (this.full) { + msg.wantlist.full = true + } + + this.blocks.forEach((block, cidStr) => { + const cid = new CID(cidStr) + msg.payload.push({ + prefix: cid.prefix, + data: block.data + }) + }) + + return pbm.Message.encode(msg) + } + + equals (other) { + const cmp = (a, b) => { + if (a.equals && typeof a.equals === 'function') { + return a.equals(b) + } + } + + if (this.full !== other.full || + !isEqualWith(this.wantlist, other.wantlist, cmp) || + !isEqualWith(this.blocks, other.blocks, cmp) + ) { + return false + } + + return true + } + + get [Symbol.toStringTag] () { + const list = Array.from(this.wantlist.keys()) + const blocks = Array.from(this.blocks.keys()) + return `BitswapMessage ` + } +} + +BitswapMessage.deserialize = (raw, callback) => { + let decoded + try { + decoded = pbm.Message.decode(raw) + } catch (err) { + return setImmediate(() => callback(err)) + } + + const isFull = (decoded.wantlist && decoded.wantlist.full) || false + const msg = new BitswapMessage(isFull) + + if (decoded.wantlist) { + decoded.wantlist.entries.forEach((entry) => { + // note: entry.block is the CID here + const cid = new CID(entry.block) + msg.addEntry(cid, entry.priority, entry.cancel) + }) + } + + // Bitswap 1.0.0 + // decoded.blocks are just the byte arrays + if (decoded.blocks.length > 0) { + map(decoded.blocks, (b, cb) => { + const block = new Block(b) + block.key((err, key) => { + if (err) { + return cb(err) + } + const cid = new CID(key) + msg.addBlock(cid, block) + cb() + }) + }, (err) => { + if (err) { + return callback(err) + } + callback(null, msg) + }) + return + } + + // Bitswap 1.1.0 + if (decoded.payload.length > 0) { + map(decoded.payload, (p, cb) => { + if (!p.prefix || !p.data) { + cb() + } + const values = vd(p.prefix) + const cidVersion = values[0] + const multicodec = values[1] + const hashAlg = values[2] + // const hashLen = values[3] // We haven't need to use this so far + const block = new Block(p.data) + block.key(hashAlg, (err, multihash) => { + if (err) { + return cb(err) + } + const cid = new CID(cidVersion, codecName[multicodec.toString('16')], multihash) + msg.addBlock(cid, block) + cb() + }) + }, (err) => { + if (err) { + return callback(err) + } + callback(null, msg) + }) + return + } + callback(null, msg) +} + +BitswapMessage.Entry = Entry +module.exports = BitswapMessage diff --git a/src/types/message/message.proto.js b/src/types/message/message.proto.js new file mode 100644 index 00000000..770e6f75 --- /dev/null +++ b/src/types/message/message.proto.js @@ -0,0 +1,28 @@ +'use strict' + +// from: https://github.com/ipfs/go-ipfs/blob/master/exchange/bitswap/message/pb/message.proto + +module.exports = ` + message Message { + message Wantlist { + message Entry { + // changed from string to bytes, it makes a difference in JavaScript + optional bytes block = 1; // the block cid (cidV0 in bitswap 1.0.0, cidV1 in bitswap 1.1.0) + optional int32 priority = 2; // the priority (normalized). default to 1 + optional bool cancel = 3; // whether this revokes an entry + } + + repeated Entry entries = 1; // a list of wantlist entries + optional bool full = 2; // whether this is the full wantlist. default to false + } + + message Block { + optional bytes prefix = 1; // CID prefix (cid version, multicodec and multihash prefix (type + length) + optional bytes data = 2; + } + + optional Wantlist wantlist = 1; + repeated bytes blocks = 2; // used to send Blocks in bitswap 1.0.0 + repeated Block payload = 3; // used to send Blocks in bitswap 1.1.0 + } +` diff --git a/src/types/wantlist/entry.js b/src/types/wantlist/entry.js new file mode 100644 index 00000000..bf774b9b --- /dev/null +++ b/src/types/wantlist/entry.js @@ -0,0 +1,42 @@ +'use strict' + +const assert = require('assert') +const CID = require('cids') + +class WantListEntry { + constructor (cid, priority) { + assert(CID.isCID(cid), 'must be valid CID') + + // Keep track of how many requests we have for this key + this._refCounter = 1 + + this.cid = cid + this.priority = priority || 1 + } + + inc () { + this._refCounter += 1 + } + + dec () { + this._refCounter = Math.max(0, this._refCounter - 1) + } + + hasRefs () { + return this._refCounter > 0 + } + + // So that console.log prints a nice description of this object + get [Symbol.toStringTag] () { + const cidStr = this.cid.toBaseEncodedString() + return `WantlistEntry ` + } + + equals (other) { + return (this._refCounter === other._refCounter) && + this.cid.equals(other.cid) && + this.priority === other.priority + } +} + +module.exports = WantListEntry diff --git a/src/types/wantlist/index.js b/src/types/wantlist/index.js new file mode 100644 index 00000000..c12f8ebe --- /dev/null +++ b/src/types/wantlist/index.js @@ -0,0 +1,65 @@ +'use strict' + +const Entry = require('./entry') + +class Wantlist { + constructor () { + this.set = new Map() + } + + get length () { + return this.set.size + } + + add (cid, priority) { + const cidStr = cid.buffer.toString() + const entry = this.set.get(cidStr) + + if (entry) { + entry.inc() + entry.priority = priority + } else { + this.set.set(cidStr, new Entry(cid, priority)) + } + } + + remove (cid) { + const cidStr = cid.buffer.toString() + const entry = this.set.get(cidStr) + + if (!entry) { + return + } + + entry.dec() + + // only delete when no refs are held + if (entry.hasRefs()) { + return + } + + this.set.delete(cidStr) + } + + removeForce (cidStr) { + if (this.set.has(cidStr)) { + this.set.delete(cidStr) + } + } + + entries () { + return this.set.entries() + } + + sortedEntries () { + return new Map(Array.from(this.set.entries()).sort()) + } + + contains (cid) { + const cidStr = cid.buffer.toString() + return this.set.get(cidStr) + } +} + +Wantlist.Entry = Entry +module.exports = Wantlist diff --git a/src/wantlist/entry.js b/src/wantlist/entry.js deleted file mode 100644 index 1af066b8..00000000 --- a/src/wantlist/entry.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -const assert = require('assert') -const isUndefined = require('lodash.isundefined') -const mh = require('multihashes') - -module.exports = class WantlistEntry { - constructor (key, priority) { - assert(Buffer.isBuffer(key), 'key must be a buffer') - // Keep track of how many requests we have for this key - this._refCounter = 1 - - this._key = key - this.priority = isUndefined(priority) ? 1 : priority - this._keyB58String = '' - } - - get key () { - return this._key - } - - set key (val) { - throw new Error('immutable key') - } - - inc () { - this._refCounter += 1 - } - - dec () { - this._refCounter = Math.max(0, this._refCounter - 1) - } - - hasRefs () { - return this._refCounter > 0 - } - - toB58String () { - if (!this._keyB58String) { - this._keyB58String = mh.toB58String(this.key) - } - - return this._keyB58String - } - - get [Symbol.toStringTag] () { - return `WantlistEntry ` - } - - equals (other) { - return (this._refCounter === other._refCounter) && - this.key.equals(other.key) && - this.priority === other.priority - } -} diff --git a/src/wantlist/index.js b/src/wantlist/index.js deleted file mode 100644 index a6d4bdc1..00000000 --- a/src/wantlist/index.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict' - -const Entry = require('./entry') - -class Wantlist { - constructor () { - this.set = new Map() - } - - get length () { - return this.set.size - } - - add (key, priority) { - const e = this.set.get(key.toString()) - - if (e) { - e.inc() - e.priority = priority - } else { - this.set.set(key.toString(), new Entry(key, priority)) - } - } - - remove (key) { - const e = this.set.get(key.toString()) - - if (!e) return - - e.dec() - - // only delete when no refs are held - if (e.hasRefs()) return - - this.set.delete(key.toString()) - } - - removeForce (key) { - if (this.set.has(key.toString())) { - this.set.delete(key.toString()) - } - } - - entries () { - return this.set.entries() - } - - sortedEntries () { - return new Map(Array.from(this.set.entries()).sort()) - } - - contains (key) { - return this.set.get(key.toString()) - } -} - -Wantlist.Entry = Entry -module.exports = Wantlist diff --git a/test/browser.js b/test/browser.js index 6fbd837b..f5b811c0 100644 --- a/test/browser.js +++ b/test/browser.js @@ -65,4 +65,4 @@ const repo = { } require('./index-test')(repo) -require('./decision/engine-test')(repo) +require('./components/decision-engine/index-test')(repo) diff --git a/test/decision/engine-test.js b/test/components/decision-engine/index-test.js similarity index 51% rename from test/decision/engine-test.js rename to test/components/decision-engine/index-test.js index 2d32fc52..e09f43d8 100644 --- a/test/decision/engine-test.js +++ b/test/components/decision-engine/index-test.js @@ -12,29 +12,36 @@ const map = require('async/map') const eachSeries = require('async/eachSeries') const pull = require('pull-stream') const paramap = require('pull-paramap') +const CID = require('cids') -const Message = require('../../src/message') -const Engine = require('../../src/decision/engine') +const Message = require('../../../src/types/message') +const DecisionEngine = require('../../../src/components/decision-engine') -const mockNetwork = require('../utils').mockNetwork +const mockNetwork = require('../../utils').mockNetwork + +function messageToString (m) { + return Array.from(m[1].blocks.values()) + .map((b) => b.data.toString()) +} + +function stringifyMessages (messages) { + return _.flatten(messages.map(messageToString)) +} module.exports = (repo) => { - function newEngine (id, done) { + function newEngine (path, done) { parallel([ - (cb) => repo.create(id, cb), + (cb) => repo.create(path, cb), (cb) => PeerId.create(cb) ], (err, results) => { if (err) { return done(err) } const blockstore = results[0].blockstore - const engine = new Engine(blockstore, mockNetwork()) + const engine = new DecisionEngine(blockstore, mockNetwork()) engine.start() - done(null, { - peer: results[1], - engine - }) + done(null, { peer: results[1], engine }) }) } @@ -61,45 +68,30 @@ module.exports = (repo) => { }), paramap((block, cb) => { const m = new Message(false) - m.addBlock(block, (err) => { + block.key((err, key) => { if (err) { return cb(err) } - block.key((err, key) => { - if (err) { - return cb(err) - } - sender.engine.messageSent(receiver.peer, block, key) - receiver.engine.messageReceived(sender.peer, m, cb) - }) + const cid = new CID(key) + m.addBlock(cid, block) + sender.engine.messageSent(receiver.peer, block, cid) + receiver.engine.messageReceived(sender.peer, m, cb) }) }, 100), pull.onEnd((err) => { expect(err).to.not.exist - expect( - sender.engine.numBytesSentTo(receiver.peer) - ).to.be.above( - 0 - ) - - expect( - sender.engine.numBytesSentTo(receiver.peer) - ).to.be.eql( - receiver.engine.numBytesReceivedFrom(sender.peer) - ) - - expect( - receiver.engine.numBytesSentTo(sender.peer) - ).to.be.eql( - 0 - ) - - expect( - sender.engine.numBytesReceivedFrom(receiver.peer) - ).to.be.eql( - 0 - ) + expect(sender.engine.numBytesSentTo(receiver.peer)) + .to.be.above(0) + + expect(sender.engine.numBytesSentTo(receiver.peer)) + .to.eql(receiver.engine.numBytesReceivedFrom(sender.peer)) + + expect(receiver.engine.numBytesSentTo(sender.peer)) + .to.eql(0) + + expect(sender.engine.numBytesReceivedFrom(receiver.peer)) + .to.eql(0) done() }) @@ -115,30 +107,20 @@ module.exports = (repo) => { expect(err).to.not.exist const sanfrancisco = res[0] - const seatlle = res[1] + const seattle = res[1] const m = new Message(true) - sanfrancisco.engine.messageSent(seatlle.peer) - seatlle.engine.messageReceived(sanfrancisco.peer, m, (err) => { + sanfrancisco.engine.messageSent(seattle.peer) + seattle.engine.messageReceived(sanfrancisco.peer, m, (err) => { expect(err).to.not.exist - expect( - seatlle.peer.toHexString() - ).to.not.be.eql( - sanfrancisco.peer.toHexString() - ) - - expect( - sanfrancisco.engine.peers() - ).to.include( - seatlle.peer - ) - - expect( - seatlle.engine.peers() - ).to.include( - sanfrancisco.peer - ) + expect(seattle.peer.toHexString()) + .to.not.eql(sanfrancisco.peer.toHexString()) + + expect(sanfrancisco.engine.peers()).to.include(seattle.peer) + + expect(seattle.engine.peers()) + .to.include(sanfrancisco.peer) done() }) }) @@ -153,6 +135,35 @@ module.exports = (repo) => { [alphabet, _.difference(alphabet, vowels)] ] + function partnerWants (dEngine, values, partner, cb) { + const message = new Message(false) + const blocks = values.map((k) => new Block(k)) + + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + expect(err).to.not.exist + keys.forEach((key, i) => { + const cid = new CID(key) + message.addEntry(cid, Math.pow(2, 32) - 1 - i) + }) + + dEngine.messageReceived(partner, message, cb) + }) + } + + function partnerCancels (dEngine, values, partner, cb) { + const message = new Message(false) + const blocks = values.map((k) => new Block(k)) + + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + expect(err).to.not.exist + keys.forEach((key) => { + const cid = new CID(key) + message.cancel(cid) + }) + dEngine.messageReceived(partner, message, cb) + }) + } + repo.create('p', (err, repo) => { expect(err).to.not.exist @@ -164,58 +175,32 @@ module.exports = (repo) => { if (err) { return cb(err) } - cb(null, {data: block.data, key: key}) + cb(null, { data: block.data, key: key }) }) }), repo.blockstore.putStream(), pull.onEnd((err) => { expect(err).to.not.exist - const partnerWants = (e, keys, p, cb) => { - const add = new Message(false) - const blocks = keys.map((k) => new Block(k)) - map(blocks, (b, cb) => b.key(cb), (err, keys) => { - expect(err).to.not.exist - blocks.forEach((b, i) => { - add.addEntry(keys[i], Math.pow(2, 32) - 1 - i) - }) - - e.messageReceived(p, add, cb) - }) - } - - const partnerCancels = (e, keys, p, cb) => { - const cancels = new Message(false) - const blocks = keys.map((k) => new Block(k)) - map(blocks, (b, cb) => b.key(cb), (err, keys) => { - expect(err).to.not.exist - keys.forEach((k) => cancels.cancel(k)) - e.messageReceived(p, cancels, cb) - }) - } - eachSeries(_.range(numRounds), (i, cb) => { + // 2 test cases + // a) want alphabet - cancel vowels + // b) want alphabet - cancels everything except vowels + eachSeries(testCases, (testcase, innerCb) => { const set = testcase[0] const cancels = testcase[1] const keeps = _.difference(set, cancels) - const messageToString = (m) => { - return Array.from(m[1].blocks.values()) - .map((b) => b.data.toString()) - } - const stringifyMessages = (messages) => { - return _.flatten(messages.map(messageToString)) - } - const network = mockNetwork(1, (res) => { const msgs = stringifyMessages(res.messages) - expect(msgs.sort()).to.be.eql(keeps.sort()) + expect(msgs.sort()).to.eql(keeps.sort()) innerCb() }) - const e = new Engine(repo.blockstore, network) - e.start() + const dEngine = new DecisionEngine(repo.blockstore, network) + dEngine.start() + let partner series([ (cb) => PeerId.create((err, id) => { @@ -225,10 +210,10 @@ module.exports = (repo) => { partner = id cb() }), - (cb) => partnerWants(e, set, partner, cb), - (cb) => partnerCancels(e, cancels, partner, cb) + (cb) => partnerWants(dEngine, set, partner, cb), + (cb) => partnerCancels(dEngine, cancels, partner, cb) ], (err) => { - if (err) throw err + expect(err).to.not.exist }) }, cb) }, done) diff --git a/test/decision/ledger.spec.js b/test/components/decision-engine/ledger.spec.js similarity index 58% rename from test/decision/ledger.spec.js rename to test/components/decision-engine/ledger.spec.js index 712ff8c0..11c2a89d 100644 --- a/test/decision/ledger.spec.js +++ b/test/components/decision-engine/ledger.spec.js @@ -4,24 +4,25 @@ const expect = require('chai').expect const PeerId = require('peer-id') -const Ledger = require('../../src/decision/ledger') +const Ledger = require('../../../src/components/decision-engine/ledger') describe('Ledger', () => { - let p + let peerId let ledger before((done) => { - PeerId.create((err, id) => { + PeerId.create((err, _peerId) => { if (err) { return done(err) } - p = id + peerId = _peerId done() }) }) + beforeEach(() => { - ledger = new Ledger(p) + ledger = new Ledger(peerId) }) it('accounts', () => { @@ -30,11 +31,10 @@ describe('Ledger', () => { ledger.receivedBytes(223432) ledger.receivedBytes(2333) - expect( - ledger.accounting - ).to.be.eql({ - bytesSent: 100 + 12000, - bytesRecv: 223432 + 2333 - }) + expect(ledger.accounting) + .to.eql({ + bytesSent: 100 + 12000, + bytesRecv: 223432 + 2333 + }) }) }) diff --git a/test/network/gen-bitswap-network.node.js b/test/components/network/gen-bitswap-network.node.js similarity index 86% rename from test/network/gen-bitswap-network.node.js rename to test/components/network/gen-bitswap-network.node.js index b0e02d42..ad868337 100644 --- a/test/network/gen-bitswap-network.node.js +++ b/test/components/network/gen-bitswap-network.node.js @@ -12,7 +12,8 @@ const Block = require('ipfs-block') const Buffer = require('safe-buffer').Buffer const pull = require('pull-stream') const crypto = require('crypto') -const utils = require('../utils') +const utils = require('../../utils') +const CID = require('cids') describe('gen Bitswap network', function () { // CI is very slow @@ -40,13 +41,16 @@ describe('gen Bitswap network', function () { (cb) => { pull( pull.values(blocks), - pull.asyncMap((b, cb) => { - b.key((err, key) => { + pull.asyncMap((block, cb) => { + block.key((err, key) => { if (err) { return cb(err) } - cb(null, {data: b.data, key: key}) + cb(null, { + block: block, + cid: new CID(key) + }) }) }), node.bitswap.putStream(), @@ -55,10 +59,11 @@ describe('gen Bitswap network', function () { }, (cb) => { each(_.range(100), (i, cb) => { - map(blocks, (b, cb) => b.key(cb), (err, keys) => { + map(blocks, (block, cb) => block.key(cb), (err, keys) => { + const cids = keys.map((key) => new CID(key)) expect(err).to.not.exist pull( - node.bitswap.getStream(keys), + node.bitswap.getStream(cids), pull.collect((err, res) => { expect(err).to.not.exist expect(res).to.have.length(blocks.length) @@ -114,6 +119,7 @@ function round (nodeArr, n, cb) { if (err) { return cb(err) } + const cids = keys.map((k) => new CID(k)) let d series([ // put blockFactor amount of blocks per node @@ -123,8 +129,8 @@ function round (nodeArr, n, cb) { const data = _.map(_.range(blockFactor), (j) => { const index = i * blockFactor + j return { - data: blocks[index].data, - key: keys[index] + block: blocks[index], + cid: cids[index] } }) each( @@ -140,7 +146,7 @@ function round (nodeArr, n, cb) { // fetch all blocks on every node (cb) => parallel(_.map(nodeArr, (node, i) => (callback) => { pull( - node.bitswap.getStream(keys), + node.bitswap.getStream(cids), pull.collect((err, res) => { if (err) { return callback(err) diff --git a/test/components/network/network.node.js b/test/components/network/network.node.js new file mode 100644 index 00000000..b40fca81 --- /dev/null +++ b/test/components/network/network.node.js @@ -0,0 +1,365 @@ +/* eslint-env mocha */ +'use strict' + +const Node = require('libp2p-ipfs-nodejs') +const PeerInfo = require('peer-info') +const multiaddr = require('multiaddr') +const expect = require('chai').expect +const PeerBook = require('peer-book') +const Block = require('ipfs-block') +const lp = require('pull-length-prefixed') +const pull = require('pull-stream') +const parallel = require('async/parallel') +const CID = require('cids') + +const Network = require('../../../src/components/network') +const Message = require('../../../src/types/message') + +describe('network', () => { + let libp2pNodeA + let peerInfoA + let peerBookA + let networkA + + let libp2pNodeB + let peerInfoB + let peerBookB + let networkB + + let libp2pNodeC + let peerInfoC + let peerBookC + let networkC + + let blocks + + before((done) => { + let counter = 0 + parallel([ + (cb) => PeerInfo.create(cb), + (cb) => PeerInfo.create(cb), + (cb) => PeerInfo.create(cb) + ], (err, results) => { + if (err) { + return done(err) + } + + peerInfoA = results[0] + peerInfoB = results[1] + peerInfoC = results[2] + + blocks = ['hello', 'world'].map((b) => new Block(b)) + + const maA = multiaddr('/ip4/127.0.0.1/tcp/10100/ipfs/' + peerInfoA.id.toB58String()) + const maB = multiaddr('/ip4/127.0.0.1/tcp/10300/ipfs/' + peerInfoB.id.toB58String()) + const maC = multiaddr('/ip4/127.0.0.1/tcp/10500/ipfs/' + peerInfoC.id.toB58String()) + + peerInfoA.multiaddr.add(maA) + peerInfoB.multiaddr.add(maB) + peerInfoC.multiaddr.add(maC) + + peerBookA = new PeerBook() + peerBookB = new PeerBook() + peerBookC = new PeerBook() + + peerBookA.put(peerInfoB) + peerBookA.put(peerInfoC) + + peerBookB.put(peerInfoA) + peerBookB.put(peerInfoC) + + peerBookC.put(peerInfoA) + peerBookC.put(peerInfoB) + + libp2pNodeA = new Node(peerInfoA, peerBookA) + libp2pNodeA.start(started) + libp2pNodeB = new Node(peerInfoB, peerBookB) + libp2pNodeB.start(started) + libp2pNodeC = new Node(peerInfoC, peerBookC) + libp2pNodeC.start(started) + + function started () { + if (++counter === 3) { + done() + } + } + }) + }) + + after((done) => { + let counter = 0 + libp2pNodeA.stop(stopped) + libp2pNodeB.stop(stopped) + libp2pNodeC.stop(stopped) + + function stopped () { + if (++counter === 3) { + done() + } + } + }) + + let bitswapMockA = { + _receiveMessage: () => {}, + _receiveError: () => {}, + _onPeerConnected: () => {}, + _onPeerDisconnected: () => {} + } + + let bitswapMockB = { + _receiveMessage: () => {}, + _receiveError: () => {}, + _onPeerConnected: () => {}, + _onPeerDisconnected: () => {} + } + + let bitswapMockC = { + _receiveMessage: () => {}, + _receiveError: () => {}, + _onPeerConnected: () => {}, + _onPeerDisconnected: () => {} + } + + it('instantiate the network obj', (done) => { + networkA = new Network(libp2pNodeA, peerBookA, bitswapMockA) + networkB = new Network(libp2pNodeB, peerBookB, bitswapMockB) + // only bitswap100 + networkC = new Network(libp2pNodeC, peerBookC, bitswapMockC, true) + + expect(networkA).to.exist + expect(networkB).to.exist + expect(networkC).to.exist + + networkA.start() + networkB.start() + networkC.start() + + done() + }) + + it('connectTo fail', (done) => { + networkA.connectTo(peerInfoB.id, (err) => { + expect(err).to.exist + done() + }) + }) + + it('onPeerConnected success', (done) => { + var counter = 0 + + bitswapMockA._onPeerConnected = (peerId) => { + expect(peerId.toB58String()).to.equal(peerInfoB.id.toB58String()) + if (++counter === 2) { + finish() + } + } + + bitswapMockB._onPeerConnected = (peerId) => { + expect(peerId.toB58String()).to.equal(peerInfoA.id.toB58String()) + if (++counter === 2) { + finish() + } + } + + libp2pNodeA.dialByPeerInfo(peerInfoB, (err) => { + expect(err).to.not.exist + }) + + function finish () { + bitswapMockA._onPeerConnected = () => {} + bitswapMockB._onPeerConnected = () => {} + done() + } + }) + + it('connectTo success', (done) => { + networkA.connectTo(peerInfoB.id, (err) => { + expect(err).to.not.exist + done() + }) + }) + + it('._receiveMessage success from Bitswap 1.0.0', (done) => { + const msg = new Message(true) + const b1 = blocks[0] + const b2 = blocks[1] + + b1.key((err, key1) => { + expect(err).to.not.exist + const cid1 = new CID(key1) + msg.addEntry(cid1, 0, false) + msg.addBlock(cid1, b1) + + b2.key((err, key2) => { + expect(err).to.not.exist + const cid2 = new CID(key2) + + msg.addBlock(cid2, b2) + + bitswapMockB._receiveMessage = (peerId, msgReceived) => { + expect(msg).to.eql(msgReceived) + bitswapMockB._receiveMessage = () => {} + bitswapMockB._receiveError = () => {} + done() + } + + bitswapMockB._receiveError = (err) => { + expect(err).to.not.exist + } + + libp2pNodeA.dialByPeerInfo(peerInfoB, '/ipfs/bitswap/1.0.0', (err, conn) => { + expect(err).to.not.exist + + pull( + pull.values([ + msg.serializeToBitswap100() + ]), + lp.encode(), + conn + ) + }) + }) + }) + }) + + it('._receiveMessage success from Bitswap 1.1.0', (done) => { + const msg = new Message(true) + const b1 = blocks[0] + const b2 = blocks[1] + + b1.key((err, key1) => { + expect(err).to.not.exist + const cid1 = new CID(key1) + msg.addEntry(cid1, 0, false) + msg.addBlock(cid1, b1) + + b2.key((err, key2) => { + expect(err).to.not.exist + const cid2 = new CID(key2) + + msg.addBlock(cid2, b2) + + bitswapMockB._receiveMessage = (peerId, msgReceived) => { + expect(msg).to.eql(msgReceived) + bitswapMockB._receiveMessage = () => {} + bitswapMockB._receiveError = () => {} + done() + } + + bitswapMockB._receiveError = (err) => { + expect(err).to.not.exist + } + + libp2pNodeA.dialByPeerInfo(peerInfoB, '/ipfs/bitswap/1.1.0', (err, conn) => { + expect(err).to.not.exist + + pull( + pull.values([ + msg.serializeToBitswap110() + ]), + lp.encode(), + conn + ) + }) + }) + }) + }) + + it('.sendMessage on Bitswap 1.1.0', (done) => { + const msg = new Message(true) + const b1 = blocks[0] + const b2 = blocks[1] + + b1.key((err, key1) => { + expect(err).to.not.exist + const cid1 = new CID(key1) + msg.addEntry(cid1, 0, false) + msg.addBlock(cid1, b1) + + b2.key((err, key2) => { + expect(err).to.not.exist + const cid2 = new CID(key2) + + msg.addBlock(cid2, b2) + + bitswapMockB._receiveMessage = (peerId, msgReceived) => { + expect(msg).to.eql(msgReceived) + bitswapMockB._receiveMessage = () => {} + bitswapMockB._receiveError = () => {} + done() + } + + bitswapMockB._receiveError = (err) => { + expect(err).to.not.exist + } + + networkA.sendMessage(peerInfoB.id, msg, (err) => { + expect(err).to.not.exist + }) + }) + }) + }) + + it('dial to peer on Bitswap 1.0.0', (done) => { + let counter = 0 + + bitswapMockA._onPeerConnected = (peerId) => { + expect(peerId.toB58String()).to.equal(peerInfoC.id.toB58String()) + if (++counter === 2) { + finish() + } + } + + bitswapMockC._onPeerConnected = (peerId) => { + expect(peerId.toB58String()).to.equal(peerInfoA.id.toB58String()) + if (++counter === 2) { + finish() + } + } + + libp2pNodeA.dialByPeerInfo(peerInfoC, (err) => { + expect(err).to.not.exist + }) + + function finish () { + bitswapMockA._onPeerConnected = () => {} + bitswapMockC._onPeerConnected = () => {} + networkA.connectTo(peerInfoC.id, done) + } + }) + + it('.sendMessage on Bitswap 1.1.0', (done) => { + const msg = new Message(true) + const b1 = blocks[0] + const b2 = blocks[1] + + b1.key((err, key1) => { + expect(err).to.not.exist + const cid1 = new CID(key1) + msg.addEntry(cid1, 0, false) + msg.addBlock(cid1, b1) + + b2.key((err, key2) => { + expect(err).to.not.exist + const cid2 = new CID(key2) + + msg.addBlock(cid2, b2) + + bitswapMockC._receiveMessage = (peerId, msgReceived) => { + expect(msg).to.eql(msgReceived) + bitswapMockC._receiveMessage = () => {} + bitswapMockC._receiveError = () => {} + done() + } + + bitswapMockC._receiveError = (err) => { + expect(err).to.not.exist + } + + networkA.sendMessage(peerInfoC.id, msg, (err) => { + expect(err).to.not.exist + }) + }) + }) + }) +}) diff --git a/test/components/wantmanager/index.spec.js b/test/components/wantmanager/index.spec.js new file mode 100644 index 00000000..cd0b09e0 --- /dev/null +++ b/test/components/wantmanager/index.spec.js @@ -0,0 +1,97 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const PeerId = require('peer-id') +const parallel = require('async/parallel') +const series = require('async/series') +const map = require('async/map') +const Block = require('ipfs-block') +const CID = require('cids') + +const cs = require('../../../src/constants') +const Message = require('../../../src/types/message') +const WantManager = require('../../../src/components/want-manager') + +const mockNetwork = require('../../utils').mockNetwork + +describe('WantManager', () => { + it('sends wantlist to all connected peers', (done) => { + let cids + let blocks + + parallel([ + (cb) => PeerId.create(cb), + (cb) => PeerId.create(cb), + (cb) => { + const data = ['1', '2', '3'] + blocks = data.map((d) => new Block(d)) + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + if (err) { + return done(err) + } + cids = keys.map((key) => new CID(key)) + cb() + }) + } + ], (err, peerIds) => { + if (err) { + return done(err) + } + + const peer1 = peerIds[0] + const peer2 = peerIds[1] + const cid1 = cids[0] + const cid2 = cids[1] + const cid3 = cids[2] + + let wantManager + + const network = mockNetwork(6, (calls) => { + expect(calls.connects).to.have.length(6) + const m1 = new Message(true) + + m1.addEntry(cid1, cs.kMaxPriority) + m1.addEntry(cid2, cs.kMaxPriority - 1) + + const m2 = new Message(false) + + m2.cancel(cid2) + + const m3 = new Message(false) + + m3.addEntry(cid3, cs.kMaxPriority) + + const msgs = [m1, m1, m2, m2, m3, m3] + + calls.messages.forEach((m, i) => { + expect(m[0]).to.be.eql(calls.connects[i]) + expect(m[1].equals(msgs[i])).to.be.eql(true) + }) + + wantManager = null + done() + }) + + wantManager = new WantManager(network) + + wantManager.run() + wantManager.wantBlocks([cid1, cid2]) + + wantManager.connected(peer1) + wantManager.connected(peer2) + + series([ + (cb) => setTimeout(cb, 200), + (cb) => { + wantManager.cancelWants([cid2]) + cb() + }, + (cb) => setTimeout(cb, 200) + ], (err) => { + expect(err).to.not.exist + wantManager.wantBlocks([cid3]) + }) + }) + }) +}) diff --git a/test/components/wantmanager/msg-queue.spec.js b/test/components/wantmanager/msg-queue.spec.js new file mode 100644 index 00000000..5c4215cd --- /dev/null +++ b/test/components/wantmanager/msg-queue.spec.js @@ -0,0 +1,110 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const PeerId = require('peer-id') +const map = require('async/map') +const parallel = require('async/parallel') +const Block = require('ipfs-block') +const CID = require('cids') + +const Message = require('../../../src/types/message') +const MsgQueue = require('../../../src/components/want-manager/msg-queue') + +describe('MessageQueue', () => { + let peerId + let blocks + let cids + + before((done) => { + parallel([ + (cb) => { + PeerId.create((err, _peerId) => { + expect(err).to.not.exist + peerId = _peerId + cb() + }) + }, + (cb) => { + const data = ['1', '2', '3', '4', '5', '6'] + blocks = data.map((d) => new Block(d)) + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + if (err) { + return done(err) + } + cids = keys.map((key) => new CID(key)) + cb() + }) + } + ], done) + }) + + it('connects and sends messages', (done) => { + const msg = new Message(true) + const cid1 = cids[0] + const cid2 = cids[1] + const cid3 = cids[2] + const cid4 = cids[3] + const cid5 = cids[4] + const cid6 = cids[5] + + msg.addEntry(cid1, 3) + msg.addEntry(cid2, 1) + + const messages = [] + const connects = [] + let i = 0 + + const finish = () => { + i++ + if (i === 2) { + expect(connects).to.be.eql([peerId, peerId]) + + const m1 = new Message(false) + m1.addEntry(cid3, 1) + m1.addEntry(cid4, 2) + m1.cancel(cid5) + m1.cancel(cid6) + + expect( + messages + ).to.be.eql([ + [peerId, msg], + [peerId, m1] + ]) + + done() + } + } + + const network = { + connectTo (p, cb) { + connects.push(p) + cb() + }, + sendMessage (p, msg, cb) { + messages.push([p, msg]) + cb() + finish() + } + } + + const mq = new MsgQueue(peerId, network) + + expect(mq.refcnt).to.equal(1) + + const batch1 = [ + new Message.Entry(cid3, 1, false), + new Message.Entry(cid4, 2, false) + ] + + const batch2 = [ + new Message.Entry(cid5, 1, true), + new Message.Entry(cid6, 2, true) + ] + + mq.addEntries(batch1) + mq.addEntries(batch2) + mq.addMessage(msg) + }) +}) diff --git a/test/index-test.js b/test/index-test.js index 86142a06..fd15cd83 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -14,8 +14,9 @@ const PeerId = require('peer-id') const Block = require('ipfs-block') const PeerBook = require('peer-book') const pull = require('pull-stream') +const CID = require('cids') -const Message = require('../src/message') +const Message = require('../src/types/message') const Bitswap = require('../src') const utils = require('./utils') @@ -35,6 +36,7 @@ module.exports = (repo) => { describe('bitswap', () => { let store let blocks + let cids let ids before((done) => { @@ -51,7 +53,13 @@ module.exports = (repo) => { blocks = results[1] ids = results[2] - done() + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + if (err) { + return done(err) + } + cids = keys.map((key) => new CID(key)) + done() + }) }) }) @@ -61,82 +69,83 @@ module.exports = (repo) => { describe('receive message', () => { it('simple block message', (done) => { - const me = ids[0] const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) bs.start() const other = ids[1] + const b1 = blocks[0] const b2 = blocks[1] + const cid1 = cids[0] + const cid2 = cids[1] + const msg = new Message(false) - each([b1, b2], (b, cb) => msg.addBlock(b, cb), (err) => { - expect(err).to.not.exist + msg.addBlock(cid1, b1) + msg.addBlock(cid2, b2) - bs._receiveMessage(other, msg, (err) => { - if (err) { - throw err - } + bs._receiveMessage(other, msg, (err) => { + if (err) { + throw err + } - expect(bs.blocksRecvd).to.be.eql(2) - expect(bs.dupBlocksRecvd).to.be.eql(0) + expect(bs.blocksRecvd).to.equal(2) + expect(bs.dupBlocksRecvd).to.equal(0) - pull( - pull.values([b1, b2]), - pull.asyncMap((b, cb) => b.key(cb)), - pull.map((key) => store.getStream(key)), - pull.flatten(), - pull.collect((err, blocks) => { - if (err) return done(err) - - expect(blocks[0].data).to.be.eql(b1.data) - expect(blocks[1].data).to.be.eql(b2.data) - done() - }) - ) - }) + pull( + pull.values([cid1, cid2]), + pull.map((cid) => store.getStream(cid.multihash)), + pull.flatten(), + pull.collect((err, blocks) => { + if (err) { + return done(err) + } + + expect(blocks[0].data).to.eql(b1.data) + expect(blocks[1].data).to.eql(b2.data) + done() + }) + ) }) }) it('simple want message', (done) => { - const me = ids[0] const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) bs.start() const other = ids[1] - const b1 = blocks[2] - const b2 = blocks[3] + const cid1 = cids[0] + const cid2 = cids[1] + const msg = new Message(false) - parallel([ - (cb) => b1.key(cb), - (cb) => b2.key(cb) - ], (err, keys) => { - expect(err).to.not.exist - msg.addEntry(keys[0], 1, false) - msg.addEntry(keys[1], 1, false) + msg.addEntry(cid1, 1, false) + msg.addEntry(cid2, 1, false) - bs._receiveMessage(other, msg, (err) => { - expect(err).to.not.exist + bs._receiveMessage(other, msg, (err) => { + expect(err).to.not.exist - expect(bs.blocksRecvd).to.be.eql(0) - expect(bs.dupBlocksRecvd).to.be.eql(0) + expect(bs.blocksRecvd).to.be.eql(0) + expect(bs.dupBlocksRecvd).to.be.eql(0) - const wl = bs.wantlistForPeer(other) + const wl = bs.wantlistForPeer(other) - expect(wl.has(keys[0].toString())).to.be.eql(true) - expect(wl.has(keys[1].toString())).to.be.eql(true) + expect(wl.has(cid1.buffer.toString())).to.eql(true) + expect(wl.has(cid2.buffer.toString())).to.eql(true) - done() - }) + done() }) }) it('multi peer', (done) => { - const me = ids[0] const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) + + let others + let blocks + let cids + bs.start() parallel([ @@ -147,21 +156,32 @@ module.exports = (repo) => { return done(err) } - const others = results[0] - const blocks = results[1] + others = results[0] + blocks = results[1] + + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + if (err) { + return done(err) + } + cids = keys.map((key) => new CID(key)) + test() + }) + }) + function test () { map(_.range(5), (i, cb) => { - const m = new Message(false) - each( - [blocks[i], blocks[5 + i]], - (b, cb) => m.addBlock(b, cb), - (err) => { - if (err) { - return cb(err) - } - cb(null, m) - } - ) + const msg = new Message(false) + + each([ + { block: blocks[i], cid: cids[i] }, + { block: blocks[5 + i], cid: cids[5 + i] } + ], (blockAndCid, cb) => { + msg.addBlock(blockAndCid.cid, blockAndCid.block) + cb() + }, (err) => { + expect(err).to.not.exist + cb(null, msg) + }) }, (err, messages) => { expect(err).to.not.exist let i = 0 @@ -169,22 +189,24 @@ module.exports = (repo) => { const msg = messages[i] i++ bs._receiveMessage(other, msg, (err) => { - if (err) return cb(err) + expect(err).to.not.exist hasBlocks(msg, store, cb) }) }, done) }) - }) + } }) }) describe('getStream', () => { it('block exists locally', (done) => { - const me = ids[0] const block = blocks[4] + const cid = cids[4] + pull( - pull.values([block]), - pull.asyncMap(blockToStore), + pull.values([ + { data: block.data, key: cid.multihash } + ]), store.putStream(), pull.onEnd((err) => { if (err) { @@ -192,19 +214,16 @@ module.exports = (repo) => { } const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) pull( - pull.values([block]), - pull.asyncMap((b, cb) => b.key(cb)), - pull.map((key) => bs.getStream(key)), - pull.flatten(), + bs.getStream(cid), pull.collect((err, res) => { if (err) { return done(err) } - expect(res[0].data).to.be.eql(block.data) + expect(res[0].data).to.eql(block.data) done() }) ) @@ -213,85 +232,47 @@ module.exports = (repo) => { }) it('blocks exist locally', (done) => { - const me = ids[0] const b1 = blocks[5] const b2 = blocks[6] const b3 = blocks[7] + const cid1 = cids[5] + const cid2 = cids[6] + const cid3 = cids[7] pull( - pull.values([b1, b2, b3]), - pull.asyncMap(blockToStore), + pull.values([ + { data: b1.data, key: cid1.multihash }, + { data: b2.data, key: cid2.multihash }, + { data: b3.data, key: cid3.multihash } + ]), store.putStream(), pull.onEnd((err) => { expect(err).to.not.exist const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) pull( - pull.values([b1, b2, b3]), - pull.asyncMap((b, cb) => b.key(cb)), - pull.collect((err, keys) => { + bs.getStream([cid1, cid2, cid3]), + pull.collect((err, res) => { expect(err).to.not.exist - pull( - bs.getStream(keys), - pull.collect((err, res) => { - expect(err).to.not.exist - - expect(res[0].data).to.be.eql(b1.data) - expect(res[1].data).to.be.eql(b2.data) - expect(res[2].data).to.be.eql(b3.data) - done() - }) - ) + + expect(res[0].data).to.eql(b1.data) + expect(res[1].data).to.eql(b2.data) + expect(res[2].data).to.eql(b3.data) + done() }) ) }) ) }) - // Not sure if I understand what is going on here - // test fails because now the network is not properly mocked - // what are these net.stores and mockNet.bitswaps? - it.skip('block is retrived from peer', (done) => { - const block = blocks[8] - - let mockNet - waterfall([ - (cb) => utils.createMockNet(repo, 2, cb), - (net, cb) => { - mockNet = net - net.stores[1].put(block, cb) - }, - (val, cb) => { - mockNet.bitswaps[0]._onPeerConnected(mockNet.ids[1]) - mockNet.bitswaps[1]._onPeerConnected(mockNet.ids[0]) - pull( - pull.values([block]), - pull.asyncMap((b, cb) => b.key(cb)), - pull.map((key) => mockNet.bitswaps[0].getStream(key)), - pull.flatten(), - pull.collect((err, res) => { - if (err) { - return cb(err) - } - cb(null, res[0]) - }) - ) - }, - (res, cb) => { - expect(res).to.be.eql(block) - cb() - } - ], done) - }) - it('block is added locally afterwards', (done) => { - const me = ids[0] const block = blocks[9] const book = new PeerBook() - const bs = new Bitswap(me, libp2pMock, store, book) + const bs = new Bitswap(libp2pMock, store, book) const net = utils.mockNetwork() + bs.network = net bs.wm.network = net bs.engine.network = net @@ -299,8 +280,9 @@ module.exports = (repo) => { block.key((err, key) => { expect(err).to.not.exist + const cid = new CID(key) pull( - bs.getStream(key), + bs.getStream(cid), pull.collect((err, res) => { expect(err).to.not.exist expect(res[0].data).to.be.eql(block.data) @@ -310,8 +292,8 @@ module.exports = (repo) => { setTimeout(() => { bs.put({ - data: block.data, - key: key + block: block, + cid: cid }, () => {}) }, 200) }) @@ -360,7 +342,7 @@ module.exports = (repo) => { start () {}, stop () {} } - bs1 = new Bitswap(me, libp2pMock, store, new PeerBook()) + bs1 = new Bitswap(libp2pMock, store, new PeerBook()) utils.applyNetwork(bs1, n1) bs1.start() @@ -370,7 +352,7 @@ module.exports = (repo) => { (cb) => repo.create('world', cb), (repo, cb) => { store2 = repo.blockstore - bs2 = new Bitswap(other, libp2pMock, store2, new PeerBook()) + bs2 = new Bitswap(libp2pMock, store2, new PeerBook()) utils.applyNetwork(bs2, n2) bs2.start() bs1._onPeerConnected(other) @@ -378,8 +360,9 @@ module.exports = (repo) => { block.key((err, key) => { expect(err).to.not.exist + const cid = new CID(key) pull( - bs1.getStream(key), + bs1.getStream(cid), pull.collect((err, res) => { expect(err).to.not.exist cb(null, res[0]) @@ -388,14 +371,15 @@ module.exports = (repo) => { setTimeout(() => { bs2.put({ - data: block.data, - key: key + block: block, + cid: cid }) }, 1000) }) }, (res, cb) => { - expect(res).to.be.eql(res) + // TODO: Ask Fridel if this is what he really meant + expect(res).to.eql(res) cb() } ], done) @@ -404,8 +388,7 @@ module.exports = (repo) => { describe('stat', () => { it('has initial stats', () => { - const me = ids[0] - const bs = new Bitswap(me, libp2pMock, {}, new PeerBook()) + const bs = new Bitswap(libp2pMock, {}, new PeerBook()) const stats = bs.stat() expect(stats).to.have.property('wantlist') @@ -418,8 +401,7 @@ module.exports = (repo) => { describe('unwant', () => { it('removes blocks that are wanted multiple times', (done) => { - const me = ids[0] - const bs = new Bitswap(me, libp2pMock, store, new PeerBook()) + const bs = new Bitswap(libp2pMock, store, new PeerBook()) bs.start() const b = blocks[11] @@ -433,7 +415,7 @@ module.exports = (repo) => { b.key((err, key) => { expect(err).to.not.exist pull( - bs.getStream(key), + bs.getStream(new CID(key)), pull.collect((err, res) => { expect(err).to.not.exist expect(res).to.be.empty @@ -441,7 +423,7 @@ module.exports = (repo) => { }) ) pull( - bs.getStream(key), + bs.getStream(new CID(key)), pull.collect((err, res) => { expect(err).to.not.exist expect(res).to.be.empty @@ -449,7 +431,7 @@ module.exports = (repo) => { }) ) - setTimeout(() => bs.unwant(key), 10) + setTimeout(() => bs.unwant(new CID(key)), 10) }) }) }) @@ -470,13 +452,3 @@ function hasBlocks (msg, store, cb) { }) }, cb) } - -function blockToStore (b, cb) { - b.key((err, key) => { - if (err) { - return cb(err) - } - - cb(null, {data: b.data, key: key}) - }) -} diff --git a/test/message.spec.js b/test/message.spec.js deleted file mode 100644 index 1dfa2208..00000000 --- a/test/message.spec.js +++ /dev/null @@ -1,222 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const expect = require('chai').expect -const Block = require('ipfs-block') -const protobuf = require('protocol-buffers') -const mh = require('multihashes') -const series = require('async/series') -const map = require('async/map') -const pbm = protobuf(require('../src/message/message.proto')) - -const BitswapMessage = require('../src/message') - -describe('BitswapMessage', () => { - let blocks - let keys - - before((done) => { - const data = [ - 'foo', - 'hello', - 'world' - ] - blocks = data.map((d) => new Block(d)) - map(blocks, (b, cb) => b.key(cb), (err, res) => { - if (err) { - return done(err) - } - keys = res - done() - }) - }) - - it('go interop', (done) => { - const goEncoded = new Buffer('CioKKAoiEiAs8k26X7CjDiboOyrFueKeGxYeXB+nQl5zBDNik4uYJBAKGAA=', 'base64') - - const m = new BitswapMessage(false) - m.addEntry(mh.fromB58String('QmRN6wdp1S2A5EtjW9A3M1vKSBuQQGcgvuhoMUoEz4iiT5'), 10) - - BitswapMessage.fromProto(goEncoded, (err, res) => { - expect(err).to.not.exist - expect(res).to.be.eql(m) - - expect( - m.toProto() - ).to.be.eql( - goEncoded - ) - done() - }) - }) - - it('append wanted', () => { - const key = keys[1] - const m = new BitswapMessage(true) - m.addEntry(key, 1) - - expect( - pbm.Message.decode(m.toProto()).wantlist.entries[0] - ).to.be.eql({ - block: key, - priority: 1, - cancel: false - }) - }) - - it('encodes blocks', (done) => { - const block = blocks[1] - const m = new BitswapMessage(true) - m.addBlock(block, (err) => { - expect(err).to.not.exist - expect( - pbm.Message.decode(m.toProto()).blocks - ).to.be.eql([ - block.data - ]) - done() - }) - }) - - it('new message fromProto', (done) => { - const raw = pbm.Message.encode({ - wantlist: { - entries: [{ - block: new Buffer('hello'), - cancel: false - }], - full: true - }, - blocks: ['hello', 'world'] - }) - - BitswapMessage.fromProto(raw, (err, protoMessage) => { - expect(err).to.not.exist - expect( - protoMessage.full - ).to.be.eql( - true - ) - expect( - Array.from(protoMessage.wantlist) - ).to.be.eql([ - [(new Buffer('hello')).toString(), new BitswapMessage.Entry(new Buffer('hello'), 0, false)] - ]) - - const b1 = blocks[1] - const b2 = blocks[2] - const k1 = keys[1] - const k2 = keys[2] - - expect( - Array.from(protoMessage.blocks).map((b) => [b[0], b[1].data]) - ).to.be.eql([ - [k1.toString(), b1.data], - [k2.toString(), b2.data] - ]) - - done() - }) - }) - - it('duplicates', (done) => { - const b = blocks[0] - const key = keys[0] - const m = new BitswapMessage(true) - - m.addEntry(key, 1) - m.addEntry(key, 1) - - expect(m.wantlist.size).to.be.eql(1) - series([ - (cb) => m.addBlock(b, cb), - (cb) => m.addBlock(b, cb) - ], (err) => { - expect(err).to.not.exist - expect(m.blocks.size).to.be.eql(1) - done() - }) - }) - - it('empty', () => { - const m = new BitswapMessage(true) - - expect( - m.empty - ).to.be.eql( - true - ) - }) - - it('non full message', () => { - const m = new BitswapMessage(false) - - expect( - pbm.Message.decode(m.toProto()).wantlist.full - ).to.be.eql( - false - ) - }) - - describe('.equals', () => { - it('true, same message', (done) => { - const b = blocks[0] - const key = keys[0] - const m1 = new BitswapMessage(true) - const m2 = new BitswapMessage(true) - - m1.addEntry(key, 1) - m2.addEntry(key, 1) - - series([ - (cb) => m1.addBlock(b, cb), - (cb) => m2.addBlock(b, cb) - ], (err) => { - expect(err).to.not.exist - expect(m1.equals(m2)).to.be.eql(true) - done() - }) - }) - - it('false, different entries', (done) => { - const b = blocks[0] - const key = keys[0] - const m1 = new BitswapMessage(true) - const m2 = new BitswapMessage(true) - - m1.addEntry(key, 1) - m2.addEntry(key, 2) - - series([ - (cb) => m1.addBlock(b, cb), - (cb) => m2.addBlock(b, cb) - ], (err) => { - expect(err).to.not.exist - expect(m1.equals(m2)).to.be.eql(false) - done() - }) - }) - }) - - describe('Entry', () => { - it('exposes the wantlist entry properties', () => { - const entry = new BitswapMessage.Entry(new Buffer('hello'), 5, false) - - expect(entry).to.have.property('key') - expect(entry).to.have.property('priority', 5) - - expect(entry).to.have.property('cancel', false) - }) - - it('allows setting properties on the wantlist entry', () => { - const entry = new BitswapMessage.Entry(new Buffer('hello'), 5, false) - - expect(entry.entry).to.have.property('key') - expect(entry.entry).to.have.property('priority', 5) - - entry.priority = 2 - - expect(entry.entry).to.have.property('priority', 2) - }) - }) -}) diff --git a/test/network/network.node.js b/test/network/network.node.js deleted file mode 100644 index 0c5fc707..00000000 --- a/test/network/network.node.js +++ /dev/null @@ -1,203 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const Node = require('libp2p-ipfs-nodejs') -const PeerInfo = require('peer-info') -const multiaddr = require('multiaddr') -const expect = require('chai').expect -const PeerBook = require('peer-book') -const Block = require('ipfs-block') -const lp = require('pull-length-prefixed') -const pull = require('pull-stream') -const parallel = require('async/parallel') -const series = require('async/series') - -const Network = require('../../src/network') -const Message = require('../../src/message') - -describe('network', () => { - let libp2pNodeA - let libp2pNodeB - let peerInfoA - let peerInfoB - let peerBookA - let peerBookB - let networkA - let networkB - let blocks - - before((done) => { - let counter = 0 - parallel([ - (cb) => PeerInfo.create(cb), - (cb) => PeerInfo.create(cb) - ], (err, results) => { - if (err) { - return done(err) - } - - peerInfoA = results[0] - peerInfoB = results[1] - blocks = ['hello', 'world'].map((b) => new Block(b)) - - peerInfoA.multiaddr.add(multiaddr('/ip4/127.0.0.1/tcp/10100/ipfs/' + peerInfoA.id.toB58String())) - peerInfoB.multiaddr.add(multiaddr('/ip4/127.0.0.1/tcp/10500/ipfs/' + peerInfoB.id.toB58String())) - - peerBookA = new PeerBook() - peerBookB = new PeerBook() - - peerBookA.put(peerInfoB) - peerBookB.put(peerInfoA) - - libp2pNodeA = new Node(peerInfoA, peerBookA) - libp2pNodeA.start(started) - libp2pNodeB = new Node(peerInfoB, peerBookB) - libp2pNodeB.start(started) - - function started () { - if (++counter === 2) { - done() - } - } - }) - }) - - after((done) => { - let counter = 0 - libp2pNodeA.stop(stopped) - libp2pNodeB.stop(stopped) - - function stopped () { - if (++counter === 2) { - done() - } - } - }) - - let bitswapMockA = { - _receiveMessage: () => {}, - _receiveError: () => {}, - _onPeerConnected: () => {}, - _onPeerDisconnected: () => {} - } - - let bitswapMockB = { - _receiveMessage: () => {}, - _receiveError: () => {}, - _onPeerConnected: () => {}, - _onPeerDisconnected: () => {} - } - - it('instantiate the network obj', (done) => { - networkA = new Network(libp2pNodeA, peerBookA, bitswapMockA) - networkB = new Network(libp2pNodeB, peerBookB, bitswapMockB) - expect(networkA).to.exist - expect(networkB).to.exist - - networkA.start() - networkB.start() - done() - }) - - it('connectTo fail', (done) => { - networkA.connectTo(peerInfoB.id, (err) => { - expect(err).to.exist - done() - }) - }) - - it('onPeerConnected success', (done) => { - var counter = 0 - - bitswapMockA._onPeerConnected = (peerId) => { - expect(peerId.toB58String()).to.equal(peerInfoB.id.toB58String()) - if (++counter === 2) { - finish() - } - } - - bitswapMockB._onPeerConnected = (peerId) => { - expect(peerId.toB58String()).to.equal(peerInfoA.id.toB58String()) - if (++counter === 2) { - finish() - } - } - - libp2pNodeA.dialByPeerInfo(peerInfoB, (err) => { - expect(err).to.not.exist - }) - - function finish () { - bitswapMockA._onPeerConnected = () => {} - bitswapMockB._onPeerConnected = () => {} - done() - } - }) - - it('connectTo success', (done) => { - networkA.connectTo(peerInfoB.id, (err) => { - expect(err).to.not.exist - done() - }) - }) - - it('_receiveMessage success', (done) => { - const msg = new Message(true) - const b = blocks[0] - - b.key((err, key) => { - expect(err).to.not.exist - msg.addEntry(key, 0, false) - - series([ - (cb) => msg.addBlock(b, cb), - (cb) => msg.addBlock(blocks[1], cb) - ], (err) => { - expect(err).to.not.exist - bitswapMockB._receiveMessage = (peerId, msgReceived) => { - expect(msg).to.deep.equal(msgReceived) - bitswapMockB._receiveMessage = () => {} - bitswapMockB._receiveError = () => {} - done() - } - - bitswapMockB._receiveError = (err) => { - expect(err).to.not.exist - } - - libp2pNodeA.dialByPeerInfo(peerInfoB, '/ipfs/bitswap/1.0.0', (err, conn) => { - expect(err).to.not.exist - - pull( - pull.values([msg.toProto()]), - lp.encode(), - conn - ) - }) - }) - }) - }) - - it('sendMessage', (done) => { - const msg = new Message(true) - blocks[0].key((err, key) => { - expect(err).to.not.exist - msg.addEntry(key, 0, false) - series([ - (cb) => msg.addBlock(blocks[0], cb), - (cb) => msg.addBlock(blocks[1], cb) - ], (err) => { - expect(err).to.not.exist - bitswapMockB._receiveMessage = (peerId, msgReceived) => { - expect(msg).to.deep.equal(msgReceived) - bitswapMockB._receiveMessage = () => {} - done() - } - - networkA.sendMessage(peerInfoB.id, msg, (err) => { - expect(err).to.not.exist - }) - }) - }) - }) -}) diff --git a/test/node.js b/test/node.js index 1147b46a..91ec1c08 100644 --- a/test/node.js +++ b/test/node.js @@ -38,6 +38,6 @@ const repo = { } require('./index-test')(repo) -require('./decision/engine-test')(repo) -require('./network/network.node.js') -require('./network/gen-bitswap-network.node.js') +require('./components/decision-engine/index-test')(repo) +require('./components/network/network.node.js') +require('./components/network/gen-bitswap-network.node.js') diff --git a/test/test-data/serialized-from-go/bitswap110-message-full-wantlist b/test/test-data/serialized-from-go/bitswap110-message-full-wantlist new file mode 100644 index 0000000000000000000000000000000000000000..daa992c8fb74def41d3dbc01cacd5f70ae40ffbc GIT binary patch literal 301 zcmZ3@#I=HnOM^>ENWtrP+LEk|x6`?KC(oGYF=a0I1#g8_8?~hkENARmcS|aWDyhZ1(zb#sRTYwFwvcgw9{ALjAgkbi9jiz%Vcx)6WvOaiuM?&&p zNJZtnW0M3}VJgq&u-hCr-FB?&dt{681tzm!6?UiZ#9lh5q`gGCQ;$19fCZ+~HGH$z zzSnGlzca<0H(S=H?JU15<@vrkVeL$goU2tc3j~;9Dr4H8-S`vo$U$o5 { + let blocks + let cids + + before((done) => { + const data = ['foo', 'hello', 'world'] + blocks = data.map((d) => new Block(d)) + map(blocks, (b, cb) => b.key(cb), (err, keys) => { + if (err) { + return done(err) + } + cids = keys.map((key) => new CID(key)) + done() + }) + }) + + it('.addEntry - want block', () => { + const cid = cids[1] + const msg = new BitswapMessage(true) + msg.addEntry(cid, 1) + const serialized = msg.serializeToBitswap100() + + expect(pbm.Message.decode(serialized).wantlist.entries[0]).to.be.eql({ + block: cid.buffer, + priority: 1, + cancel: false + }) + }) + + it('.serializeToBitswap100', () => { + const block = blocks[1] + const cid = cids[1] + const msg = new BitswapMessage(true) + msg.addBlock(cid, block) + const serialized = msg.serializeToBitswap100() + expect(pbm.Message.decode(serialized).blocks).to.eql([block.data]) + }) + + it('.serializeToBitswap110', () => { + const block = blocks[1] + const cid = cids[1] + const msg = new BitswapMessage(true) + msg.addBlock(cid, block) + + const serialized = msg.serializeToBitswap110() + const decoded = pbm.Message.decode(serialized) + + expect(decoded.payload[0].data).to.eql(block.data) + }) + + it('.deserialize a Bitswap100 Message', (done) => { + const cid0 = cids[0] + const cid1 = cids[1] + const cid2 = cids[2] + + const b1 = blocks[1] + const b2 = blocks[2] + + const raw = pbm.Message.encode({ + wantlist: { + entries: [{ + block: cid0.buffer, + cancel: false + }], + full: true + }, + blocks: [ + b1.data, + b2.data + ] + }) + + BitswapMessage.deserialize(raw, (err, msg) => { + expect(err).to.not.exist + expect(msg.full).to.equal(true) + expect(Array.from(msg.wantlist)) + .to.eql([[ + cid0.toBaseEncodedString(), + new BitswapMessage.Entry(cid0, 0, false) + ]]) + + expect(Array.from(msg.blocks).map((b) => [b[0], b[1].data])) + .to.eql([ + [cid1.toBaseEncodedString(), b1.data], + [cid2.toBaseEncodedString(), b2.data] + ]) + + done() + }) + }) + + it('.deserialize a Bitswap110 Message', (done) => { + const cid0 = cids[0] + const cid1 = cids[1] + const cid2 = cids[2] + + const b1 = blocks[1] + const b2 = blocks[2] + + const raw = pbm.Message.encode({ + wantlist: { + entries: [{ + block: cid0.buffer, + cancel: false + }], + full: true + }, + payload: [{ + data: b1.data, + prefix: cid1.prefix + }, { + data: b2.data, + prefix: cid2.prefix + }] + }) + + BitswapMessage.deserialize(raw, (err, msg) => { + expect(err).to.not.exist + expect(msg.full).to.equal(true) + expect(Array.from(msg.wantlist)) + .to.eql([[ + cid0.toBaseEncodedString(), + new BitswapMessage.Entry(cid0, 0, false) + ]]) + + expect(Array.from(msg.blocks).map((b) => [b[0], b[1].data])) + .to.eql([ + [cid1.toBaseEncodedString(), b1.data], + [cid2.toBaseEncodedString(), b2.data] + ]) + + done() + }) + }) + + it('duplicates', (done) => { + const b = blocks[0] + const cid = cids[0] + const m = new BitswapMessage(true) + + m.addEntry(cid, 1) + m.addEntry(cid, 1) + + expect(m.wantlist.size).to.be.eql(1) + m.addBlock(cid, b) + m.addBlock(cid, b) + expect(m.blocks.size).to.be.eql(1) + done() + }) + + it('.empty', () => { + const m = new BitswapMessage(true) + expect(m.empty).to.equal(true) + }) + + it('non full wantlist message', () => { + const msg = new BitswapMessage(false) + const serialized = msg.serializeToBitswap100() + + expect(pbm.Message.decode(serialized).wantlist.full).to.equal(false) + }) + + describe('.equals', () => { + it('true, same message', (done) => { + const b = blocks[0] + const cid = cids[0] + const m1 = new BitswapMessage(true) + const m2 = new BitswapMessage(true) + + m1.addEntry(cid, 1) + m2.addEntry(cid, 1) + + m1.addBlock(cid, b) + m2.addBlock(cid, b) + expect(m1.equals(m2)).to.equal(true) + done() + }) + + it('false, different entries', (done) => { + const b = blocks[0] + const cid = cids[0] + const m1 = new BitswapMessage(true) + const m2 = new BitswapMessage(true) + + m1.addEntry(cid, 100) + m2.addEntry(cid, 3750) + + m1.addBlock(cid, b) + m2.addBlock(cid, b) + expect(m1.equals(m2)).to.equal(false) + done() + }) + }) + + describe('BitswapMessageEntry', () => { + it('exposes the wantlist entry properties', () => { + const cid = cids[0] + const entry = new BitswapMessage.Entry(cid, 5, false) + + expect(entry).to.have.property('cid') + expect(entry).to.have.property('priority', 5) + + expect(entry).to.have.property('cancel', false) + }) + + it('allows setting properties on the wantlist entry', () => { + const cid1 = cids[0] + const cid2 = cids[1] + + const entry = new BitswapMessage.Entry(cid1, 5, false) + + expect(entry.entry).to.have.property('cid') + expect(entry.entry).to.have.property('priority', 5) + + entry.cid = cid2 + entry.priority = 2 + + expect(entry.entry).to.have.property('cid') + expect(entry.entry.cid.equals(cid2)) + expect(entry.entry).to.have.property('priority', 2) + }) + }) + + describe('go interop', () => { + it('bitswap 1.0.0 message', (done) => { + const goEncoded = new Buffer('CioKKAoiEiAs8k26X7CjDiboOyrFueKeGxYeXB+nQl5zBDNik4uYJBAKGAA=', 'base64') + + const msg = new BitswapMessage(false) + const cid = new CID('QmRN6wdp1S2A5EtjW9A3M1vKSBuQQGcgvuhoMUoEz4iiT5') + msg.addEntry(cid, 10) + + BitswapMessage.deserialize(goEncoded, (err, res) => { + expect(err).to.not.exist + expect(res).to.eql(msg) + expect(msg.serializeToBitswap100()).to.eql(goEncoded) + done() + }) + }) + + describe.skip('bitswap 1.1.0 message', () => { + // TODO check with whyrusleeping the quality of the raw protobufs + // deserialization is just failing on the first and the second has a + // payload but empty + it('full wantlist message', (done) => { + BitswapMessage.deserialize(rawMessageFullWantlist, (err, message) => { + expect(err).to.not.exist + // TODO + // check the deserialised message + done() + }) + }) + + it('one block message', (done) => { + BitswapMessage.deserialize(rawMessageOneBlock, (err, message) => { + expect(err).to.not.exist + // TODO + // check the deserialised message + done() + }) + }) + }) + }) +}) diff --git a/test/wantlist.spec.js b/test/types/wantlist.spec.js similarity index 51% rename from test/wantlist.spec.js rename to test/types/wantlist.spec.js index 473fb7c4..3534f23a 100644 --- a/test/wantlist.spec.js +++ b/test/types/wantlist.spec.js @@ -4,8 +4,9 @@ const expect = require('chai').expect const Block = require('ipfs-block') const map = require('async/map') +const CID = require('cids') -const Wantlist = require('../src/wantlist') +const Wantlist = require('../../src/types/wantlist') describe('Wantlist', () => { let wm @@ -25,11 +26,15 @@ describe('Wantlist', () => { const b1 = blocks[0] const b2 = blocks[1] - map([b1, b2], (b, cb) => b.key(cb), (err, keys) => { + map([ + b1, + b2 + ], + (b, cb) => b.key(cb), + (err, keys) => { expect(err).to.not.exist - wm.add(keys[0], 2) - wm.add(keys[1], 1) - + wm.add(new CID(keys[0]), 2) + wm.add(new CID(keys[1]), 1) expect(wm).to.have.length(2) done() }) @@ -41,9 +46,8 @@ describe('Wantlist', () => { b.key((err, key) => { expect(err).to.not.exist - wm.add(key, 1) - wm.remove(key) - + wm.add(new CID(key), 1) + wm.remove(new CID(key)) expect(wm).to.have.length(0) done() }) @@ -53,24 +57,31 @@ describe('Wantlist', () => { const b1 = blocks[0] const b2 = blocks[1] - map([b1, b2], (b, cb) => b.key(cb), (err, keys) => { + map([ + b1, + b2 + ], + (b, cb) => b.key(cb), + (err, keys) => { expect(err).to.not.exist - wm.add(keys[0], 1) - wm.add(keys[1], 2) + const cid1 = new CID(keys[0]) + const cid2 = new CID(keys[1]) + + wm.add(cid1, 1) + wm.add(cid2, 2) expect(wm).to.have.length(2) - wm.remove(keys[1]) + wm.remove(cid2) expect(wm).to.have.length(1) - wm.add(keys[0], 2) - wm.remove(keys[0]) + wm.add(cid1, 2) + wm.remove(cid1) expect(wm).to.have.length(1) - wm.remove(keys[0]) - + wm.remove(cid1) expect(wm).to.have.length(0) done() }) @@ -81,9 +92,10 @@ describe('Wantlist', () => { b.key((err, key) => { expect(err).to.not.exist - wm.add(key, 1) - wm.remove(key) - wm.remove(key) + const cid = new CID(key) + wm.add(cid, 1) + wm.remove(cid) + wm.remove(cid) expect(wm).to.have.length(0) done() @@ -95,13 +107,15 @@ describe('Wantlist', () => { const b = blocks[0] b.key((err, key) => { expect(err).to.not.exist - wm.add(key, 2) + const cid = new CID(key) + wm.add(cid, 2) expect( Array.from(wm.entries()) - ).to.be.eql([ - [key.toString(), new Wantlist.Entry(key, 2)] - ]) + ).to.be.eql([[ + cid.buffer.toString(), + new Wantlist.Entry(cid, 2) + ]]) done() }) }) @@ -110,16 +124,24 @@ describe('Wantlist', () => { const b1 = blocks[0] const b2 = blocks[1] - map([b1, b2], (b, cb) => b.key(cb), (err, keys) => { + map([ + b1, + b2 + ], + (b, cb) => b.key(cb), + (err, keys) => { expect(err).to.not.exist - wm.add(keys[1], 1) - wm.add(keys[0], 1) + const cid1 = new CID(keys[0]) + const cid2 = new CID(keys[1]) + + wm.add(cid1, 1) + wm.add(cid2, 1) expect( Array.from(wm.sortedEntries()) ).to.be.eql([ - [keys[0].toString(), new Wantlist.Entry(keys[0], 1)], - [keys[1].toString(), new Wantlist.Entry(keys[1], 1)] + [cid1.buffer.toString(), new Wantlist.Entry(cid1, 1)], + [cid2.buffer.toString(), new Wantlist.Entry(cid2, 1)] ]) done() }) @@ -128,17 +150,38 @@ describe('Wantlist', () => { it('contains', (done) => { const b1 = blocks[0] const b2 = blocks[1] - map([b1, b2], (b, cb) => b.key(cb), (err, keys) => { + + map([ + b1, + b2 + ], + (b, cb) => b.key(cb), + (err, keys) => { expect(err).to.not.exist - wm.add(keys[0], 2) + const cid1 = new CID(keys[0]) + const cid2 = new CID(keys[1]) - expect( - wm.contains(keys[0]) - ).to.exist + wm.add(cid1, 2) + + expect(wm.contains(cid1)).to.exist + expect(wm.contains(cid2)).to.not.exist + done() + }) + }) + + it('with cidV1', (done) => { + const b = blocks[0] + b.key((err, key) => { + expect(err).to.not.exist + const cid = new CID(1, 'dag-pb', key) + wm.add(cid, 2) expect( - wm.contains(keys[1]) - ).to.not.exist + Array.from(wm.entries()) + ).to.be.eql([[ + cid.buffer.toString(), + new Wantlist.Entry(cid, 2) + ]]) done() }) }) diff --git a/test/utils.js b/test/utils.js index 207a8105..84868ca9 100644 --- a/test/utils.js +++ b/test/utils.js @@ -59,7 +59,7 @@ exports.createMockNet = (repo, count, cb) => { const ids = results[1] const hexIds = ids.map((id) => id.toHexString()) - const bitswaps = _.range(count).map((i) => new Bitswap(ids[i], {}, stores[i])) + const bitswaps = _.range(count).map((i) => new Bitswap({}, stores[i])) const networks = _.range(count).map((i) => { return { connectTo (id, cb) { @@ -153,7 +153,7 @@ exports.genBitswapNetwork = (n, callback) => { // create every BitSwap function createBitswaps () { netArray.forEach((net) => { - net.bitswap = new Bitswap(net.peerInfo, net.libp2p, net.repo.blockstore, net.peerBook) + net.bitswap = new Bitswap(net.libp2p, net.repo.blockstore, net.peerBook) }) establishLinks() } diff --git a/test/wantmanager/index.spec.js b/test/wantmanager/index.spec.js deleted file mode 100644 index 57ac69fd..00000000 --- a/test/wantmanager/index.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const expect = require('chai').expect -const PeerId = require('peer-id') -const parallel = require('async/parallel') -const series = require('async/series') - -const cs = require('../../src/constants') -const Message = require('../../src/message') -const Wantmanager = require('../../src/wantmanager') - -const mockNetwork = require('../utils').mockNetwork - -describe('Wantmanager', () => { - it('sends wantlist to all connected peers', (done) => { - parallel([ - (cb) => PeerId.create(cb), - (cb) => PeerId.create(cb) - ], (err, peers) => { - if (err) { - return done(err) - } - - const peer1 = peers[0] - const peer2 = peers[1] - - let wm - const network = mockNetwork(6, (calls) => { - expect(calls.connects).to.have.length(6) - const m1 = new Message(true) - m1.addEntry(new Buffer('hello'), cs.kMaxPriority) - m1.addEntry(new Buffer('world'), cs.kMaxPriority - 1) - - const m2 = new Message(false) - m2.cancel(new Buffer('world')) - - const m3 = new Message(false) - m3.addEntry(new Buffer('foo'), cs.kMaxPriority) - - const msgs = [m1, m1, m2, m2, m3, m3] - - calls.messages.forEach((m, i) => { - expect(m[0]).to.be.eql(calls.connects[i]) - expect(m[1].equals(msgs[i])).to.be.eql(true) - }) - - wm = null - done() - }) - - wm = new Wantmanager(network) - - wm.run() - wm.wantBlocks([new Buffer('hello'), new Buffer('world')]) - - wm.connected(peer1) - wm.connected(peer2) - - series([ - (cb) => setTimeout(cb, 200), - (cb) => { - wm.cancelWants([new Buffer('world')]) - cb() - }, - (cb) => setTimeout(cb, 200) - ], (err) => { - expect(err).to.not.exist - wm.wantBlocks([new Buffer('foo')]) - }) - }) - }) -}) diff --git a/test/wantmanager/msg-queue.spec.js b/test/wantmanager/msg-queue.spec.js deleted file mode 100644 index a1596139..00000000 --- a/test/wantmanager/msg-queue.spec.js +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const expect = require('chai').expect -const PeerId = require('peer-id') - -const Message = require('../../src/message') -const MsgQueue = require('../../src/wantmanager/msg-queue') - -describe('MsgQueue', () => { - it('connects and sends messages', (done) => { - PeerId.create((err, id) => { - if (err) { - return done(err) - } - - const msg = new Message(true) - msg.addEntry(new Buffer('hello world'), 3) - msg.addEntry(new Buffer('foo bar'), 1) - - const messages = [] - const connects = [] - let i = 0 - const finish = () => { - i++ - if (i === 2) { - expect( - connects - ).to.be.eql([ - id, id - ]) - - const m1 = new Message(false) - m1.addEntry(new Buffer('hello'), 1) - m1.addEntry(new Buffer('world'), 2) - m1.cancel(new Buffer('foo')) - m1.cancel(new Buffer('bar')) - - expect( - messages - ).to.be.eql([ - [id, msg], - [id, m1] - ]) - - done() - } - } - - const network = { - connectTo (p, cb) { - connects.push(p) - cb() - }, - sendMessage (p, msg, cb) { - messages.push([p, msg]) - cb() - finish() - } - } - const mq = new MsgQueue(id, network) - - expect(mq.refcnt).to.be.eql(1) - - const batch1 = [ - new Message.Entry(new Buffer('hello'), 1, false), - new Message.Entry(new Buffer('world'), 2, false) - ] - - const batch2 = [ - new Message.Entry(new Buffer('foo'), 1, true), - new Message.Entry(new Buffer('bar'), 2, true) - ] - - mq.addEntries(batch1) - mq.addEntries(batch2) - mq.addMessage(msg) - }) - }) -})