diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..b75293c --- /dev/null +++ b/benchmark.js @@ -0,0 +1,46 @@ +var bench = require('nanobench') +var protocol = require('./') + +bench('send 1.000.000 messages', function (b) { + var stream1 = protocol() + var stream2 = protocol() + + var ch1 = stream1.open(new Buffer('01234567012345670123456701234567')) + var ch2 = stream2.open(new Buffer('01234567012345670123456701234567')) + + var missing = 1000000 + ch2.on('request', function () { + if (--missing) return + b.end() + }) + + for (var i = 0; i < 1000000; i++) { + ch1.request({ + block: 42 + }) + } + + stream1.pipe(stream2).pipe(stream1) +}) + +bench('send 1.000.000 messages (no encryption)', function (b) { + var stream1 = protocol({encrypt: false}) + var stream2 = protocol({encrypt: false}) + + var ch1 = stream1.open(new Buffer('01234567012345670123456701234567')) + var ch2 = stream2.open(new Buffer('01234567012345670123456701234567')) + + var missing = 1000000 + ch2.on('request', function () { + if (--missing) return + b.end() + }) + + for (var i = 0; i < 1000000; i++) { + ch1.request({ + block: 42 + }) + } + + stream1.pipe(stream2).pipe(stream1) +}) diff --git a/index.js b/index.js index 637e89d..ac78fd7 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,15 @@ var events = require('events') +var channels = require('stream-channels') var inherits = require('inherits') -var duplexify = require('duplexify') -var lpstream = require('length-prefixed-stream') -var stream = require('readable-stream') -var randomBytes = require('randombytes') -var createHmac = require('create-hmac') -var encryption = require('sodium-encryption') var increment = require('increment-buffer') -var equals = require('buffer-equals') -var varint = require('varint') -var xtend = require('xtend') var pe = require('passthrough-encoding') -var debug = require('debug')('hypercore-protocol') -var prettyHash = require('pretty-hash') +var xtend = require('xtend') +var sodium = require('sodium-native') var messages = require('./messages') -var KEEP_ALIVE = Buffer([0]) +// increment = function ( buf) { + +// } var DEFAULT_TYPES = [ messages.Handshake, @@ -57,85 +51,123 @@ function use (extensions) { var types = DEFAULT_TYPES.slice(0) var eventNames = DEFAULT_EVENTS.slice(0) - function Channel (protocol) { + function Channel (stream) { events.EventEmitter.call(this) - this.state = null // set by someone else - this.protocol = protocol - this.opened = false - this.closed = false + this.stream = stream + this.outgoing = stream.createChannel({preallocated: true}) + this.outgoing.state = this + this.nonce = random(sodium.crypto_stream_NONCEBYTES) + this.state = null + this.remoteNonce = null this.key = null this.discoveryKey = null - this.local = -1 - this.remote = -1 - this.buffer = [] + this.closed = false + this.opened = false - this._nonce = randomBytes(24) - this._remoteNonce = null - this._ready = false + this._buffer = [] - this._firstNonce = Buffer(24) - this._nonce.copy(this._firstNonce) - this.on('handshake', this._onhandshake) + var self = this + + this.outgoing.on('close', onclose) + this.outgoing.on('end', onclose) + + function onclose () { + self.close() + } } inherits(Channel, events.EventEmitter) Channel.prototype.remoteSupports = function (name) { - return this.protocol.remoteSupports(name) + return this.stream.remoteSupports(name) } - Channel.prototype.handshake = function (message) { - return this.protocol._send(this, 0, message) - } + Channel.prototype._onhandshake = function (handshake) { + if (this.stream.remoteId) return + this.stream.remoteId = handshake.id - Channel.prototype.have = function (message) { - return this.protocol._send(this, 1, message) - } + var exts = handshake.extensions + // extensions *must* be sorted + var local = 0 + var remote = 0 - Channel.prototype.want = function (message) { - return this.protocol._send(this, 2, message) - } + while (local < extensionNames.length && remote < exts.length && remote < 64) { + if (extensionNames[local] === exts[remote]) { + this.stream._localExtensions[local] = remote + this.stream._remoteExtensions[remote] = local + local++ + remote++ + } else if (extensionNames[local] < exts[remote]) { + local++ + } else { + remote++ + } + } - Channel.prototype.request = function (message) { - return this.protocol._send(this, 3, message) + this.stream.emit('handshake') + this.emit('handshake') } - Channel.prototype.data = function (message) { - return this.protocol._send(this, 4, message) - } + Channel.prototype._ondata = function (data) { + if (!data.length || this.closed) return // ignore - Channel.prototype.cancel = function (message) { - return this.protocol._send(this, 5, message) - } + if (!this.key || this._buffer.length) { + if (this._buffer.length === 16) { + return this.stream.destroy(new Error('Buffer overflow')) + } - Channel.prototype.pause = function () { - return this.protocol._send(this, 6, null) - } + this._buffer.push(data) + return + } - Channel.prototype.resume = function () { - return this.protocol._send(this, 7, null) - } + if (this.stream.encrypted) { + sodium.crypto_stream_xor(data, data, this.remoteNonce, this.key) + increment(this.remoteNonce) + } - Channel.prototype.end = function () { // graceful close - return this.protocol._send(this, 8, null) - } + var type = this._parseType(data[0]) + if (type === -1 || type >= types.length) return // ignore + + var enc = types[type] + var message = null + + if (enc && !(message = decode(enc, data, 1))) { + this.stream.destroy(new Error('Invalid message')) + return + } + + if (type === 0) { + this._onhandshake(message) + return + } - Channel.prototype.unhave = function (message) { - return this.protocol._send(this, 9, message) + var name = eventNames[type] + this.emit(name, message) } - Channel.prototype.unwant = function (message) { - return this.protocol._send(this, 10, message) + Channel.prototype._onopenmaybe = function () { + if (!this.opened && this.key && this.remoteNonce && !this.closed) { + this.opened = true + this.emit('open') + } } - Channel.prototype.close = function () { // non graceful close - this.protocol._close(this) + Channel.prototype._parseType = function (type) { + if (type > 127) return -1 + if (type < 64) return type + if (type - 64 >= this.stream._remoteExtensions.length) return -1 + + type = this.stream._remoteExtensions[type - 64] + if (type === -1) return -1 + return type + 64 } - Channel.prototype._onhandshake = function (handshake) { - niceDebug('handshaked', { channel: this }) - this.protocol._onhandshake(handshake) + Channel.prototype.close = function () { + if (this.closed) return + this.closed = true + if (!this.stream.destroyed) this.outgoing.end() + this.emit('close') } extensionNames.forEach(function (name, type) { @@ -143,85 +175,73 @@ function use (extensions) { throw new Error('Invalid extension name') } - var enc = isEncoder(extensions[name]) ? extensions[name] : pe - - types.push(enc) + types.push(isEncoder(extensions[name]) ? extensions[name] : pe) eventNames.push(name) + }) + eventNames.forEach(function (name, type) { + // TODO: faster if inlined using generate-function? Channel.prototype[name] = function (message) { - return this.protocol._send(this, DEFAULT_TYPES.length + type, message) + var enc = types[type] + var length = 1 + (enc ? enc.encodingLength(message) : 0) + var data = this.outgoing.preallocate(length) + var payload = data.slice(data.length - length) + + payload[0] = type + if (enc) enc.encode(message, payload, 1) + + if (this.stream.encrypted) { + sodium.crypto_stream_xor(payload, payload, this.nonce, this.key) + increment(this.nonce) + } + + return this.outgoing.write(data) } }) function Protocol (opts, onopen) { if (!(this instanceof Protocol)) return new Protocol(opts, onopen) - if (typeof opts === 'function') { - onopen = opts - opts = null - } if (!opts) opts = {} - var self = this - duplexify.call(this) - - var encrypt = opts.private !== undefined ? opts.private : opts.encrypt + channels.call(this, onchannel) - this.channels = {} - this.private = encrypt !== false + var self = this + if (!onopen) onopen = opts.onopen - this.id = opts.id || randomBytes(32) + this.id = opts.id || random(32) this.remoteId = null + this.extensions = extensionNames + this.channels = {} + this.debugging = !!(opts.debug || opts.debugMode) + this.encrypted = opts.encrypt !== false - this._finalized = false - this._paused = 0 - this._local = [] - this._remote = [] - - this._remoteExtensions = new Array(extensions.length) - this._localExtensions = new Array(extensions.length) - for (var i = 0; i < extensions.length; i++) { + this._remoteExtensions = new Array(extensionNames.length) + this._localExtensions = new Array(extensionNames.length) + for (var i = 0; i < extensionNames.length; i++) { this._remoteExtensions[i] = this._localExtensions[i] = -1 } - this._keepAlive = 0 - this._remoteKeepAlive = 0 - this._interval = null - - this._encode = stream.Readable() - this._encode._read = noop - - this._decode = lpstream.decode({allowEmpty: true, limit: opts.limit || 8 * 1024 * 1024}) - this._decode.on('data', parse) - - this.setReadable(this._encode) - this.setWritable(this._decode) - - this.on('close', onfinalize) - this.on('end', onfinalize) - this.on('finish', this.finalize) - + this.on('close', this._channelCleanup) + this.on('end', this.destroy) if (onopen) this.on('open', onopen) - function onfinalize () { - if (self._finalized) return - self._finalized = true - self.destroyed = true // defined by duplexify - - clearInterval(this._interval) + function onchannel (incoming) { + incoming.on('data', ondata) + incoming.on('end', onclose) + incoming.on('close', onclose) + } - var keys = Object.keys(self.channels) - for (var i = 0; i < keys.length; i++) { - niceDebug('closed', { channel: self.channels[keys[i]] }) - self._close(self.channels[keys[i]]) - } + function ondata (data) { + if (this.state) this.state._ondata(data) + else self._onopen(this, data) } - function parse (data) { - self._parse(data) + function onclose () { + if (this.state) this.state.close() } } - inherits(Protocol, duplexify) + inherits(Protocol, channels) Protocol.use = function (ext) { if (typeof ext === 'string') ext = toObject(ext) @@ -229,43 +249,9 @@ function use (extensions) { } Protocol.parseDiscoveryKey = function (buf) { - if (buf[0] !== 0) throw Error('Invalid message') - return messages.Open.decode(buf, 1).feed - } - - Protocol.prototype.finalize = function () { - this._encode.push(null) - } - - Protocol.prototype.setTimeout = function (ms, ontimeout) { - if (this.destroyed) return - if (ontimeout) this.once('timeout', ontimeout) - - var self = this - - this._keepAlive = 0 - this._remoteKeepAlive = 0 - - clearInterval(this._interval) - this._interval = setInterval(kick, (ms / 4) | 0) - if (this._interval.unref) this._interval.unref() - - function kick () { - if (self._remoteKeepAlive > 4) { - clearInterval(self._interval) - self.emit('timeout') - return - } - - self._tick() - self._remoteKeepAlive++ - if (self._keepAlive > 2) { - self._encode.push(KEEP_ALIVE) - self._keepAlive = 0 - } else { - self._keepAlive++ - } - } + var disc = messages.Open.decode(buf, 1).discoveryKey + if (disc.length !== 32) throw new Error('Invalid message') + return disc } Protocol.prototype.remoteSupports = function (name) { @@ -273,339 +259,92 @@ function use (extensions) { return i > -1 && this._localExtensions[i] > -1 } - Protocol.prototype.open = function (key, opts) { - if (this.destroyed) { - niceDebug('Open() called after finalized, aborting', { key: key }) - return null // already finalized - } - if (!opts) opts = {} - - var d = opts.discoveryKey || discoveryKey(key) - var keyHex = d.toString('hex') - var ch = this.channels[keyHex] + Protocol.prototype._onopen = function (incoming, data) { + var open = decode(messages.Open, data, 0) - if (!ch) { - ch = new Channel(this) - ch.discoveryKey = d - this.channels[keyHex] = ch - } - - if (ch.local > -1) return ch - - if (opts.state) ch.state = opts.state - - ch.key = key - ch.local = this._local.indexOf(null) - if (ch.local === -1) ch.local = this._local.push(null) - 1 - this._local[ch.local] = ch - niceDebug('open()', { channel: ch }) - - var open = messages.Open.encode({ - feed: ch.discoveryKey, - nonce: ch._nonce - }) - - this._sendRaw(ch, open) - - if (!this.remoteId && opts.handshake !== false) { - ch.handshake({ - id: this.id, - extensions: extensionNames - }) - } - - this.emit('channel', ch) - this._open(ch) - if (ch.buffer.length) this._parseSoon(ch) - - ch._ready = true // to avoid premature events - - return ch - } - - Protocol.prototype._send = function (channel, type, message) { - if (channel.closed) { - niceDebug('Send called after close, discarding', { channel: channel, type: type, message: message }) - return false - } - niceDebug('send()', { channel: channel, type: type, message: message }) - - var enc = types[type] - var len = enc ? enc.encodingLength(message) : 0 - var buf = Buffer(len + 1) - - buf[0] = type - if (enc) enc.encode(message, buf, 1) - - if (this.private) buf = this._encrypt(channel, buf) - - return this._sendRaw(channel, buf) - } - - Protocol.prototype._sendRaw = function (channel, buf) { - this._keepAlive = 0 - - var len = buf.length + varint.encodingLength(channel.local) - var box = Buffer(varint.encodingLength(len) + len) - var offset = 0 - - varint.encode(len, box, offset) - offset += varint.encode.bytes - - varint.encode(channel.local, box, offset) - offset += varint.encode.bytes - - buf.copy(box, offset) - - return this._encode.push(box) - } - - Protocol.prototype._pause = function () { - debug('pause()') - if (!this._paused++) this._decode.pause() - } - - Protocol.prototype._resume = function () { - debug('resume()') - if (!--this._paused) this._decode.resume() - } - - Protocol.prototype._parseSoon = function (channel) { - var self = this - - this._pause() - process.nextTick(drain) - - function drain () { - if (self.destroyed || channel.closed) return - - var buffer = channel.buffer - channel.buffer = [] - - while (buffer.length) self._parse(buffer.shift()) - if (!self.destroyed) self._resume() - } - } - - Protocol.prototype._parse = function (data) { - this._remoteKeepAlive = 0 - - if (!data.length) return - if (this.destroyed) { - debug('Received message after destroy(), discarding') + if (!open || open.discoveryKey.length !== 32 || open.nonce.length !== 24) { + this.destroy(new Error('Invalid open message')) return } - var remote = varint.decode(data, 0) - var offset = varint.decode.bytes - - if (remote >= this._remote.length) this._remote.push(null) - if (remote > this._remote.length) return this.destroy(new Error('Unexpected channel number')) - - if (!this._remote[remote]) this._onopen(remote, data, offset) - else if (offset !== data.length) this._onmessage(remote, data, offset) - else this._onclose(remote) - } - - Protocol.prototype._tick = function () { - for (var i = 0; i < this._local.length; i++) { - var ch = this._local[i] - if (ch) ch.emit('tick') - } - } - - Protocol.prototype._parseType = function (type) { - if (type > 127) return -1 - if (type < 64) return type - if (type - 64 >= this._remoteExtensions.length) return -1 - - type = this._remoteExtensions[type - 64] - if (type === -1) return -1 - return type + 64 - } - - Protocol.prototype._onopen = function (remote, data, offset) { - try { - var open = messages.Open.decode(data, offset) - } catch (err) { - return this.destroy(err) - } - - if (open.feed.length !== 32 || open.nonce.length !== 24) return this.destroy(new Error('Invalid open message')) + var hex = open.discoveryKey.toString('hex') + var channel = this.channels[hex] || new Channel(this) - var keyHex = open.feed.toString('hex') - var ch = this.channels[keyHex] + channel.remoteNonce = open.nonce + channel.discoveryKey = open.discoveryKey + this.channels[hex] = channel + incoming.state = channel - if (!ch) { - ch = new Channel(this) - ch.discoveryKey = open.feed - this.channels[keyHex] = ch - } - - if (ch.remote > -1) { - debug('Double open error, closing channel', { channel: ch, message: open }) - return this.destroy(new Error('Double open for same channel')) - } - niceDebug('opened', { channel: ch, message: open }) - - ch.remote = remote - ch._remoteNonce = open.nonce - this._remote[remote] = ch - this._open(ch) - - if (!this.destroyed && ch.local === -1) this.emit('open', ch.discoveryKey) + if (!channel.key) this.emit('open', open.discoveryKey) + channel._onopenmaybe() } - Protocol.prototype._onmessage = function (remote, data, offset) { - var channel = this._remote[remote] - - if (!channel.key || channel.buffer.length || !channel._ready) { - if (channel.buffer.length === 16) { - niceDebug('Buffer overflow in received message, closing channel', { channel: channel }) - return this.destroy(new Error('Buffer overflow')) - } - channel.buffer.push(data) - return - } - - var box = this._decrypt(channel, data.slice(offset)) - if (!box || !box.length) { - niceDebug('Received invalid message, closing channel', { channel: channel }) - return this.destroy(new Error('Invalid message')) - } - - var type = this._parseType(box[0]) - if (type < 0) { - niceDebug('Received invalid message type, discarding', { channel: channel, type: type }) - return - } - - if (type && !this.remoteId) { - niceDebug('Received message without handshake, destroying channel', { channel: channel }) - return this.destroy(new Error('Did not receive handshake')) - } - if (type >= types.length) { - niceDebug('Received invalid message type, discarding', { channel: channel, type: type }) - return - } - - var enc = types[type] + Protocol.prototype.open = function (key, disc) { + if (this.destroyed) return null - try { - var message = enc ? enc.decode(box, 1) : null - } catch (err) { - return this.destroy(err) - } + if (!disc) disc = discoveryKey(key) + var hex = disc.toString('hex') + var channel = this.channels[hex] || new Channel(this) - niceDebug('recv()', { channel: channel, type: type, message: message }) - channel.emit(eventNames[type], message) - } + channel.key = key + channel.discoveryKey = disc + this.channels[hex] = channel - Protocol.prototype._onclose = function (remote) { - var channel = this._remote[remote] - niceDebug('closed by remote', { channel: channel }) + var length = messages.Open.encodingLength(channel) + var buffer = channel.outgoing.preallocate(length) + messages.Open.encode(channel, buffer, buffer.length - length) + channel.outgoing.write(buffer) - this._remote[remote] = null - channel.remote = -1 + if (!this.remoteId) channel.handshake(this) - if (channel.local > -1) this._close(channel) + channel._onopenmaybe() + if (channel._buffer.length) this._shiftBuffer(channel) - var keyHex = channel.discoveryKey.toString('hex') - if (this.channels[keyHex] === channel) delete this.channels[keyHex] + return channel } - Protocol.prototype._onhandshake = function (handshake) { - if (this.remoteId) return // already handshaked - this.remoteId = handshake.id - - var exts = handshake.extensions - // extensions *must* be sorted - var local = 0 - var remote = 0 - - while (local < extensionNames.length && remote < exts.length && remote < 64) { - if (extensionNames[local] === exts[remote]) { - this._localExtensions[local] = remote - this._remoteExtensions[remote] = local - local++ - remote++ - } else if (extensionNames[local] < exts[remote]) { - local++ - } else { - remote++ - } + Protocol.prototype._channelCleanup = function () { + var keys = Object.keys(this.channels) + for (var i = 0; i < keys.length; i++) { + this.channels[keys[i]].close() } - - this.emit('handshake') } - Protocol.prototype._open = function (channel) { - if (channel.local === -1 || channel.remote === -1) return - if (equals(channel._remoteNonce, channel._firstNonce)) return this.destroy(new Error('Remote echoed nonce')) - channel.opened = true - channel.emit('open') - } - - Protocol.prototype._close = function (channel) { - if (channel.closed) return - channel.closed = true - - if (!this.destroyed) this._sendRaw(channel, Buffer(0)) - - this._local[channel.local] = null - channel.local = -1 - channel.emit('close') - } - - Protocol.prototype._encrypt = function (channel, buf) { - if (!this.private) return buf - var box = encryption.encrypt(buf, channel._nonce, channel.key) - increment(channel._nonce) - return box + Protocol.prototype._shiftBuffer = function (channel) { + process.nextTick(function () { + var buf = channel._buffer + channel._buffer = [] + while (buf.length) channel._ondata(buf.shift()) + }) } - Protocol.prototype._decrypt = function (channel, box) { - if (!this.private) return box - var buf = box.length < 16 ? null : encryption.decrypt(box, channel._remoteNonce, channel.key) - if (!buf) return null - increment(channel._remoteNonce) - return buf - } + return Protocol +} - function discoveryKey (key) { - return createHmac('sha256', key).update('hypercore').digest() - } +function discoveryKey (key) { + return require('crypto').createHmac('sha256', key).update('hypercore').digest() +} - function isEncoder (val) { - return val && typeof val.encode === 'function' +function decode (enc, data, offset) { + try { + return enc.decode(data, offset) + } catch (err) { + return null } +} - function toObject (name) { - var tmp = {} - tmp[name] = true - return tmp - } +function isEncoder (val) { + return val && typeof val.encode === 'function' +} - function noop () {} - - function niceDebug (label, data) { - if (!debug.enabled) return - if (data) { - var parts = [] - if (data.channel) parts.push('chan=' + prettyHash(data.channel.discoveryKey)) - parts.push(label) - if ('type' in data) { - var type = (types[data.type] && types[data.type].name) ? types[data.type].name : data.type - if (type === 6) type = 'Pause' - if (type === 7) type = 'Resume' - parts.push('type=' + type) - } - if (data.key) parts.push('key=' + prettyHash(data.key)) - debug(parts.join(' ')) - } else { - debug(label) - } - } +function toObject (name) { + var tmp = {} + tmp[name] = true + return tmp +} - return Protocol +function random (n) { + var buf = new Buffer(n) + sodium.randombytes_buf(buf) + return buf } diff --git a/messages.js b/messages.js index 9c50a02..b7cd5de 100644 --- a/messages.js +++ b/messages.js @@ -1,5 +1,56 @@ var protobuf = require('protocol-buffers') -var fs = require('fs') -var path = require('path') -module.exports = protobuf(fs.readFileSync(path.join(__dirname, 'schema.proto'))) +module.exports = protobuf(` + message Open { + required bytes nonce = 1; + required bytes discoveryKey = 2; + } + + message Handshake { + required bytes id = 1; + repeated string extensions = 2; + } + + message Have { + required uint64 start = 1; + optional uint64 end = 2; + optional bytes bitfield = 3; + } + + message Unhave { + required uint64 start = 1; + optional uint64 end = 2; + } + + message Unwant { + required uint64 start = 1; + optional uint64 end = 2; + } + + message Request { + required uint64 block = 1; + optional uint64 bytes = 2; + + optional bool hash = 3; + optional uint64 nodes = 4; + } + + message Data { + message Node { + required uint64 index = 1; + required bytes hash = 2; + required uint64 size = 3; + } + + required uint64 block = 1; + optional bytes value = 2; + repeated Node nodes = 3; + optional bytes signature = 4; + } + + message Cancel { + required uint64 block = 1; + optional uint64 bytes = 2; + optional bool hash = 3; + } +`) diff --git a/package.json b/package.json index f54917a..5179bbf 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,15 @@ { "name": "hypercore-protocol", - "version": "5.1.1", + "version": "5.1.2", "description": "Stream that implements the hypercore protocol", "main": "index.js", "dependencies": { - "brfs": "^1.4.3", - "buffer-equals": "^1.0.3", - "create-hmac": "^1.1.4", - "debug": "^2.3.2", - "duplexify": "^3.4.3", - "increment-buffer": "^1.0.0", - "inherits": "^2.0.1", - "length-prefixed-stream": "^1.5.0", + "increment-buffer": "^1.0.1", + "inherits": "^2.0.3", "passthrough-encoding": "^1.2.0", - "pretty-hash": "^1.0.0", - "protocol-buffers": "^3.1.6", - "randombytes": "^2.0.3", - "readable-stream": "^2.1.4", - "sodium-encryption": "^1.1.0", - "varint": "^4.0.0", + "protocol-buffers": "^3.2.1", + "sodium-native": "^1.3.1", + "stream-channels": "^1.4.0", "xtend": "^4.0.1" }, "devDependencies": { diff --git a/schema.proto b/schema.proto deleted file mode 100644 index e650701..0000000 --- a/schema.proto +++ /dev/null @@ -1,57 +0,0 @@ -message Open { - required bytes feed = 1; - required bytes nonce = 2; -} - -message Handshake { - required bytes id = 1; - repeated string extensions = 2; -} - -message Have { - required uint64 start = 1; - optional uint64 end = 2; - optional bytes bitfield = 3; -} - -message Want { - required uint64 start = 1; - optional uint64 end = 2; -} - -message Request { - required uint64 block = 1; - optional uint64 bytes = 2; - - optional bool hash = 3; - optional uint64 nodes = 4; -} - -message Data { - message Node { - required uint64 index = 1; - required bytes hash = 2; - required uint64 size = 3; - } - - required uint64 block = 1; - optional bytes value = 2; - repeated Node nodes = 3; - optional bytes signature = 4; -} - -message Cancel { - required uint64 block = 1; - optional uint64 bytes = 2; - optional bool hash = 3; -} - -message Unhave { - required uint64 start = 1; - optional uint64 end = 2; -} - -message Unwant { - required uint64 start = 1; - optional uint64 end = 2; -}