diff --git a/lib/bcoin/bip151.js b/lib/bcoin/bip151.js new file mode 100644 index 00000000..f3930e6f --- /dev/null +++ b/lib/bcoin/bip151.js @@ -0,0 +1,266 @@ +/*! + * bip151.js - peer-to-peer communication encryption. + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var bcoin = require('./env'); +var utils = require('./utils'); +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var chachapoly = require('./chachapoly'); + +function BIP151(cipher, key) { + if (!(this instanceof BIP151)) + return new BIP151(cipher, key); + + EventEmitter.call(this); + + this.publicKey = null; + this.privateKey = key || bcoin.ec.generatePrivateKey(); + this.cipher = cipher || 0; + this.secret = null; + this.k1 = null; + this.k2 = null; + this.sid = null; + this.chacha = new chachapoly.ChaCha20(); + this.aead = new chachapoly.AEAD(); + this.mac = null; + this.tag = null; + this.seq = 0; + + this.pendingHeader = []; + this.pendingHeaderTotal = 0; + this.hasHeader = false; + this.pending = []; + this.pendingTotal = 0; + this.waiting = 0; +} + +utils.inherits(BIP151, EventEmitter); + +BIP151.prototype.init = function init(publicKey) { + var p = bcoin.writer(); + + this.publicKey = publicKey; + this.secret = bcoin.ec.ecdh(this.publicKey, this.privateKey); + + p.writeBytes(this.secret); + p.writeU8(this.cipher); + + this.mac = utils.hmac('sha512', p.render(), 'encryption key'); + + this.k1 = this.mac.slice(0, 32); + this.k2 = this.mac.slice(32, 64); + + this.sid = utils.hmac('sha256', this.secret, 'session id'); + + this.seq = 0; + + this.chacha.init(this.k1, this.iv()); + this.aead.init(this.k2, this.iv()); + this.aead.aad(this.sid); +} + +BIP151.prototype.rekey = function rekey() { + this.mac = utils.hash256(this.mac); + this.k1 = this.mac.slice(0, 32); + this.k2 = this.mac.slice(32, 64); + this.seq = 0; + this.chacha.init(this.k1, this.iv()); + this.aead.init(this.k2, this.iv()); + this.aead.aad(this.sid); +}; + +BIP151.prototype.sequence = function sequence() { + this.seq++; + this.chacha.init(this.k1, this.iv()); + this.aead.init(this.k2, this.iv()); + this.aead.aad(this.sid); +}; + +BIP151.prototype.iv = function iv() { + var p = bcoin.writer(); + p.writeU64(this.seq); + p.writeU32(0); + return p.render(); +}; + +BIP151.prototype.getPublicKey = function getPublicKey() { + return bcoin.ec.publicKeyCreate(this.privateKey, true); +}; + +BIP151.prototype.encryptSize = function encryptSize(size) { + var b = new Buffer(4); + data.writeUInt32LE(size, 0, true); + return this.chacha.encrypt(data); +}; + +BIP151.prototype.decryptSize = function decryptSize(data) { + data = data.slice(0, 4); + this.chacha.encrypt(data); + return data.readUInt32LE(0, true); +}; + +BIP151.prototype.encrypt = function encrypt(data) { + return this.aead.encrypt(data); +}; + +BIP151.prototype.decrypt = function decrypt(data) { + return this.aead.decrypt(data); +}; + +BIP151.prototype.finish = function finish(data) { + this.tag = this.aead.finish(data); + return this.tag; +}; + +BIP151.prototype.verify = function verify(tag) { + return chachapoly.Poly1305.verify(this.tag, tag); +}; + +BIP151.prototype.toEncinit = function toEncinit(writer) { + var p = bcoin.writer(writer); + + p.writeBytes(this.getPublicKey()); + p.writeU8(this.cipher); + + if (!writer) + p = p.render(); + + return p; +}; + +BIP151.prototype.fromEncinit = function fromEncinit(data) { + var p = bcoin.reader(data); + var publicKey = p.readBytes(33); + this.cipher = p.readU8(); + this.init(publicKey); + return this; +}; + +BIP151.fromEncinit = function fromEncinit(data) { + return new BIP151().fromEncinit(data); +}; + +BIP151.prototype.toEncack = function toEncack(writer) { + var p = bcoin.writer(writer); + + p.writeBytes(this.getPublicKey()); + + if (!writer) + p = p.render(); + + return p; +}; + +BIP151.prototype.encack = function encack(data) { + var p = bcoin.reader(data); + var publicKey = p.readBytes(33); + var i; + + for (i = 0; i < publicKey.length; i++) { + if (publicKey[i] !== 0) + break; + } + + if (i === publicKey.length) + this.init(publicKey); + else + this.rekey(); + + return this; +}; + +BIP151.prototype.feed = function feed(data) { + var chunk, payload, tag, p, cmd, body; + + while (data) { + if (!this.hasHeader) { + this.pendingHeaderTotal += data.length; + this.pendingHeader.push(data); + data = null; + + if (this.pendingHeaderTotal < 4) + break; + + chunk = Buffer.concat(this.pendingHeader); + + this.pendingHeaderTotal = 0; + this.pendingHeader.length = 0; + + this.waiting = this.decryptSize(chunk) + 16; + + if (this.waiting - 32 > constants.MAX_MESSAGE) { + this.waiting = 0; + this.emit('error', new Error('Packet too large.')); + continue; + } + + this.hasHeader = true; + + data = chunk.slice(4); + + if (data.length === 0) + break; + } + + this.pendingTotal += data.length; + this.pending.push(data); + data = null; + + if (this.pendingTotal < this.waiting) + break; + + chunk = Buffer.concat(this.pending); + payload = chunk.slice(0, this.waiting - 16); + tag = chunk.slice(this.waiting - 16, this.waiting); + data = chunk.slice(this.waiting); + + if (data.length === 0) + data = null; + + this.decrypt(payload); + this.finish(); + this.sequence(); + + this.pendingTotal = 0; + this.pending.length = 0; + this.hasHeader = false; + this.waiting = 0; + + if (!this.verify(tag)) { + this.emit('error', new Error('Bad tag.')); + continue; + } + + p = bcoin.reader(payload, true); + cmd = p.readVarString('ascii'); + body = p.readBytes(p.readU32()); + + this.emit('packet', cmd, body); + } +}; + +BIP151.prototype.frame = function frame(cmd, body) { + var p = bcoin.writer(); + var payload, packet; + + p.writeVarString(cmd, 'ascii'); + p.writeU32(body.length); + p.writeBytes(body); + + payload = p.render(); + + packet = new Buffer(4 + payload.length + 16); + + this.encryptSize(payload.length).copy(packet, 0); + this.encrypt(payload).copy(packet, 4); + this.finish().copy(packet, 4 + payload.length); + this.sequence(); + + return packet; +}; diff --git a/lib/bcoin/bip152.js b/lib/bcoin/bip152.js new file mode 100644 index 00000000..adc3cf26 --- /dev/null +++ b/lib/bcoin/bip152.js @@ -0,0 +1,568 @@ +/*! + * bip152.js - compact block object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('./env'); +var utils = require('./utils'); +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var siphash = require('./siphash'); + +/** + * Represents a compact block (bip152): `cmpctblock` packet. + * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki + * @exports CompactBlock + * @constructor + * @extends AbstractBlock + * @param {NakedBlock} options + * @property {TX[]} available - Available transaction vector. + */ + +function CompactBlock(options) { + if (!(this instanceof CompactBlock)) + return new CompactBlock(options); + + bcoin.abstractblock.call(this, options); + + this.keyNonce = null; + this.ids = []; + this.ptx = []; + + this.available = []; + this.idMap = {}; + this.count = 0; + this.k0 = null; + this.k1 = null; + + if (options) + this.fromOptions(options); +} + +utils.inherits(CompactBlock, bcoin.abstractblock); + +CompactBlock.prototype._verify = function _verify(ret) { + return this.verifyHeaders(ret); +}; + +CompactBlock.prototype.fromOptions = function fromOptions(options) { + this.keyNonce = options.keyNonce; + this.ids = options.ids || []; + this.ptx = options.ptx || []; + this.available = options.available || []; + this.idMap = options.idMap || {}; + this.count = options.count || 0; + this.k0 = options.k0; + this.k1 = options.k1; + + this.initKey(); + this.init(); + + return this; +}; + +CompactBlock.fromOptions = function fromOptions(options) { + return new CompactBlock().fromOptions(options); +}; + +CompactBlock.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var last = 0; + var i, count, index, tx; + + this.version = p.readU32(); // Technically signed + this.prevBlock = p.readHash('hex'); + this.merkleRoot = p.readHash('hex'); + this.ts = p.readU32(); + this.bits = p.readU32(); + this.nonce = p.readU32(); + + this.keyNonce = p.readU64(); + + this.initKey(); + + count = p.readVarint2(); + + this.totalTX += count; + + for (i = 0; i < count; i++) + this.ids.push(p.readU32() + p.readU16() * 0x100000000); + + count = p.readVarint2(); + + this.totalTX += count; + + for (i = 0; i < count; i++) { + index = p.readVarint2(); + assert(index <= 0xffff); + assert(index < this.totalTX); + tx = bcoin.tx.fromRaw(p); + this.ptx.push([index, tx]); + } + + this.init(); + + return this; +}; + +CompactBlock.fromRaw = function fromRaw(data) { + return new CompactBlock().fromRaw(data); +}; + +CompactBlock.prototype.toRaw = function toRaw(witness, writer) { + var p = bcoin.writer(writer); + var i, id, lo, hi, ptx; + + p.write32(this.version); + p.writeHash(this.prevBlock); + p.writeHash(this.merkleRoot); + p.writeU32(this.ts); + p.writeU32(this.bits); + p.writeU32(this.nonce); + + p.writeU64(this.keyNonce); + + p.writeVarint2(this.ids.length); + + for (i = 0; i < this.ids.length; i++) { + id = this.ids[i]; + lo = id % 0x100000000; + hi = (id - lo) / 0x100000000; + hi &= 0xffff; + p.writeU32(lo); + p.writeU16(hi); + } + + p.writeVarint2(this.ptx.length); + + for (i = 0; i < this.ptx.length; i++) { + ptx = this.ptx[i]; + p.writeVarint2(ptx[0]); + if (!witness) + ptx[1].toNormal(p); + else + ptx[1].toRaw(p); + } + + if (!writer) + p = p.render(); + + return p; +}; + +CompactBlock.prototype.toRequest = function toRequest() { + return BlockTXRequest.fromCompact(this); +}; + +CompactBlock.prototype.fillMempool = function fillMempool(mempool, callback) { + var self = this; + var have = {}; + var id, index; + + mempool.getSnapshot(function(err, hashes) { + if (err) + return callback(err); + + utils.forEachSerial(hashes, function(hash, next) { + id = self.sid(hash); + index = self.idMap[id]; + + if (index == null) + return next(); + + if (have[index]) { + // Siphash collision, just request it. + self.available[index] = null; + self.count--; + return next(); + } + + mempool.getTX(hash, function(err, tx) { + if (err) + return callback(err); + + // Race condition: tx + // fell out of mempool. + if (!tx) + return next(); + + self.available[index] = tx; + have[index] = true; + self.count++; + + // We actually may have a siphash collision + // here, but exit early anyway for perf. + if (self.count === self.totalTX) + return callback(null, true); + + next(); + }); + }, callback); + }); +}; + +CompactBlock.prototype.fillMissing = function fillMissing(missing) { + var offset = 0; + var i, tx; + + for (i = 0; i < this.available.length; i++) { + if (this.available[i]) + continue; + + if (offset >= missing.length) + return false; + + this.available = missing[offset++]; + } + + return offset === missing.length; +}; + +CompactBlock.prototype.sid = function sid(hash) { + if (typeof hash === 'string') + hash = new Buffer(hash, 'hex'); + + hash = siphash(hash, this.k0, this.k1); + + return hash.readUInt32LE(2, true) + + hash.readUInt16LE(6, true) + * 0x100000000; +}; + +CompactBlock.prototype.hasIndex = function hasIndex(index) { + return this.available[index] != null; +}; + +CompactBlock.prototype.initKey = function initKey() { + var nonce = this.keyNonce.toArrayLike(Buffer, 'be', 8); + var data = Buffer.concat([this.abbr(), nonce]); + var hash = utils.sha256(data); + this.k0 = hash.slice(0, 8); + this.k1 = hash.slice(8, 16); +}; + +CompactBlock.prototype.init = function init() { + var i, last, ptx, offset; + + if (this.totalTX === 0) + throw new Error('Empty vectors.'); + + if (this.totalTX > constants.block.MAX_SIZE / 10) + throw new Error('Compact block too big.'); + + // No sparse arrays here, v8. + for (i = 0; i < this.totalTX; i++) + this.available.push(null); + + last = -1; + + for (i = 0; i < this.ptx.length; i++) { + ptx = this.ptx[i]; + assert(ptx); + last += ptx[0] + 1; + assert(last <= 0xffff); + assert(last <= this.ids.length + i); + this.available[last] = ptx[1]; + this.count++; + } + + offset = 0; + + for (i = 0; i < this.ids.length; i++) { + while (this.available[i + offset]) + offset++; + this.idMap[this.ids[i]] = i + offset; + // We're supposed to fail here if there's + // more than 12 hash collisions, but we + // don't have lowlevel access to our hash + // table. Hopefully we don't get hashdos'd. + } + + // Fails on siphash collision + assert(this.ids.length === Object.keys(this.idMap).length); +}; + +CompactBlock.prototype.toBlock = function toBlock() { + var block = new bcoin.block(); + var i, tx; + + block.version = this.version; + block.prevBlock = this.prevBlock; + block.merkleRoot = this.merkleRoot; + block.ts = this.ts; + block.bits = this.bits; + block.nonce = this.nonce; + block.totalTX = this.totalTX; + block.txs = new Array(this.ptx.length); + block._hash = this._hash; + block._valid = this._valid; + + for (i = 0; i < this.available.length; i++) { + tx = this.available[i]; + assert(tx, 'Compact block is not full.'); + tx.setBlock(block, i); + block.txs[i] = tx; + } + + return block; +}; + +CompactBlock.prototype.fromBlock = function fromBlock(block) { + var i, tx, id; + + this.version = block.version; + this.prevBlock = block.prevBlock; + this.merkleRoot = block.merkleRoot; + this.ts = block.ts; + this.bits = block.bits; + this.nonce = block.nonce; + this.totalTX = block.totalTX; + + this.keyNonce = utils.nonce(); + + this.initKey(); + + for (i = 1; i < block.txs.length; i++) { + tx = block.txs[i]; + id = this.sid(tx.hash()); + this.ids.push(id); + } + + this.ptx.push([0, block.txs[0]]); + + return this; +}; + +CompactBlock.fromBlock = function fromBlock(block) { + return new CompactBlock().fromBlock(block); +}; + +/** + * Represents a BlockTransactionsRequest (bip152): `getblocktxn` packet. + * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki + * @constructor + */ + +function BlockTXRequest(options) { + if (!(this instanceof BlockTXRequest)) + return new BlockTXRequest(options); + + this.hash = null; + this.indexes = []; + + if (options) + this.fromOptions(options); +} + +BlockTXRequest.prototype.fromOptions = function fromOptions(options) { + this.hash = options.hash; + this.indexes = options.indexes || []; + return this; +}; + +BlockTXRequest.fromOptions = function fromOptions(options) { + return new BlockTXRequest().fromOptions(options); +}; + +BlockTXRequest.prototype.fromCompact = function fromCompact(block) { + var i; + + this.hash = block.hash('hex'); + + for (i = 0; i < block.available.length; i++) { + if (!block.available[i]) + this.indexes.push(i); + } + + return this; +}; + +BlockTXRequest.fromCompact = function fromCompact(block) { + return new BlockTXRequest().fromCompact(block); +}; + +BlockTXRequest.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var i, count, index, offset; + + this.hash = p.readHash('hex'); + + count = p.readVarint2(); + + for (i = 0; i < count; i++) { + index = p.readVarint2(); + assert(index <= 0xffff); + this.indexes.push(index); + } + + offset = 0; + + for (i = 0; i < count; i++) { + index = this.indexes[i]; + index += offset; + assert(index <= 0xffff); + this.indexes[i] = index; + offset = index + 1; + } + + return this; +}; + +BlockTXRequest.fromRaw = function fromRaw(data) { + return new BlockTXRequest().fromRaw(data); +}; + +BlockTXRequest.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + var i, index; + + p.writeHash(this.hash); + + p.writeVarint2(this.indexes.length); + + for (i = 0; i < this.indexes.length; i++) { + index = this.indexes[i] - (i === 0 ? 0 : this.indexes[i - 1] + 1); + p.writeVarint2(index); + } + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Represents BlockTransactions (bip152): `blocktxn` packet. + * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki + * @constructor + */ + +function BlockTX() { + if (!(this instanceof BlockTX)) + return new BlockTX(options); + + this.hash = null; + this.txs = []; + + if (options) + this.fromOptions(options); +} + +BlockTX.prototype.fromOptions = function fromOptions(options) { + this.hash = options.hash; + this.txs = options.txs || []; + return this; +}; + +BlockTX.fromOptions = function fromOptions(options) { + return new BlockTX().fromOptions(options); +}; + +BlockTX.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var i, count; + + this.hash = p.readHash('hex'); + + count = p.readVarint2(); + + for (i = 0; i < count; i++) + this.txs.push(bcoin.tx.fromRaw(p)); + + return this; +}; + +BlockTX.fromRaw = function fromRaw(data) { + return new BlockTX().fromRaw(data); +}; + +BlockTX.prototype.fromBlock = function fromBlock(block, request) { + var i, index; + + this.hash = request.hash; + + for (i = 0; i < request.indexes.length; i++) { + index = request.indexes[i]; + if (index >= block.txs.length) + return; + this.txs.push(block.txs[index]); + } + + return this; +}; + +BlockTX.fromBlock = function fromBlock(block, request) { + return new BlockTX().fromBlock(block, request); +}; + +BlockTX.prototype.toRaw = function toRaw(witness, writer) { + var p = bcoin.writer(writer); + var i, tx; + + p.writeHash(this.hash); + + p.writeVarint2(this.txs.length); + + for (i = 0; i < this.txs.length; i++) { + tx = this.txs[i]; + if (!witness) + tx.toNormal(p); + else + tx.toRaw(p); + } + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Represents a SendCompact message (bip152): `sendcmpct` packet. + * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki + * @constructor + */ + +// NOTE TO SELF: Protocol version >= 70014 +function SendCompact(mode, version) { + if (!(this instanceof SendCompact)) + return new SendCompact(mode, version); + + this.mode = mode || 0; + this.version = version || 1; +} + +SendCompact.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + this.mode = p.readU8(); + this.version = p.readU53(); + return this; +}; + +SendCompact.fromRaw = function fromRaw(data) { + return new SendCompact().fromRaw(data); +}; + +SendCompact.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.writeU8(this.mode); + p.writeU64(this.version); + + if (!writer) + p = p.render(); + + return p; +}; + +/* + * Expose + */ + +exports.CompactBlock = CompactBlock; +exports.BlockTXRequest = BlockTXRequest; +exports.BlockTX = BlockTX; +exports.SendCompact = SendCompact;