diff --git a/lib/bcoin/abstractblock.js b/lib/bcoin/abstractblock.js index f0f78020..b1567eeb 100644 --- a/lib/bcoin/abstractblock.js +++ b/lib/bcoin/abstractblock.js @@ -11,6 +11,7 @@ var bcoin = require('./env'); var constants = bcoin.protocol.constants; var utils = bcoin.utils; var assert = utils.assert; +var InvItem = bcoin.packets.InvItem; /** * The class which all block-like objects inherit from. @@ -237,10 +238,7 @@ AbstractBlock.prototype.__defineGetter__('rhash', function() { */ AbstractBlock.prototype.toInv = function toInv() { - return { - type: constants.inv.BLOCK, - hash: this.hash('hex') - }; + return new InvItem(constants.inv.BLOCK, this.hash('hex')); }; /** diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 735e567c..c67b9c59 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -362,7 +362,7 @@ Address.fromScript = function fromScript(script) { */ Address.prototype.fromHash = function fromHash(hash, type, version, network) { - var p, prefix, nversion; + var prefix, nversion; if (typeof hash === 'string') hash = new Buffer(hash, 'hex'); diff --git a/lib/bcoin/chainentry.js b/lib/bcoin/chainentry.js index 6225f296..79eb6006 100644 --- a/lib/bcoin/chainentry.js +++ b/lib/bcoin/chainentry.js @@ -14,6 +14,7 @@ var utils = require('./utils'); var assert = utils.assert; var BufferWriter = require('./writer'); var BufferReader = require('./reader'); +var InvItem = bcoin.packets.InvItem; /** * Represents an entry in the chain. Unlike @@ -505,10 +506,7 @@ ChainEntry.prototype.toHeaders = function toHeaders() { */ ChainEntry.prototype.toInv = function toInv() { - return { - type: constants.inv.BLOCK, - hash: this.hash - }; + return new InvItem(constants.inv.BLOCK, this.hash); }; /** diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index 4fec7975..e50147d4 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -142,6 +142,7 @@ function Environment(options) { this.uri = require('./uri'); this.protocol = require('./protocol'); + this.packets = this.protocol.packets; this.network = require('./network'); this.errors = require('./errors'); this.ldb = require('./ldb'); @@ -182,7 +183,6 @@ function Environment(options) { this.walletdb = require('./walletdb'); this.provider = this.walletdb.Provider; this.peer = require('./peer'); - this.networkaddress = this.peer.NetworkAddress; this.pool = require('./pool'); this.miner = require('./miner'); this.minerblock = this.miner.MinerBlock; diff --git a/lib/bcoin/input.js b/lib/bcoin/input.js index 41f7b70c..dc807c64 100644 --- a/lib/bcoin/input.js +++ b/lib/bcoin/input.js @@ -472,13 +472,18 @@ Input.prototype.inspect = function inspect() { */ Input.prototype.toJSON = function toJSON() { + var address = this.getAddress(); + + if (address) + address = address.toBase58(); + return { prevout: this.prevout.toJSON(), coin: this.coin ? this.coin.toJSON() : null, script: this.script.toJSON(), witness: this.witness.toJSON(), sequence: this.sequence, - address: this.getAddress().toBase58() + address: address }; }; diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index 6a27f5b7..b156c3ca 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -965,7 +965,7 @@ MTX.prototype.selectCoins = function selectCoins(coins, options) { var index = 0; var tx = this.clone(); var outputValue = tx.getOutputValue(); - var tryFree, i, size, change, fee, min, output; + var tryFree, size, change, fee; if (!options) options = {}; diff --git a/lib/bcoin/output.js b/lib/bcoin/output.js index 8fa240fd..e06a047c 100644 --- a/lib/bcoin/output.js +++ b/lib/bcoin/output.js @@ -164,10 +164,15 @@ Output.prototype.inspect = function inspect() { */ Output.prototype.toJSON = function toJSON() { + var address = this.getAddress(); + + if (address) + address = address.toBase58(); + return { value: utils.btc(this.value), script: this.script.toJSON(), - address: this.getAddress().toBase58() + address: address }; }; diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index 1a4abba5..31b90ab7 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -13,6 +13,11 @@ var utils = require('./utils'); var IP = require('./ip'); var assert = utils.assert; var constants = bcoin.protocol.constants; +var InvItem = bcoin.packets.InvItem; +var VersionPacket = bcoin.packets.VersionPacket; +var GetBlocksPacket = bcoin.packets.GetBlocksPacket; +var RejectPacket = bcoin.packets.RejectPacket; +var NetworkAddress = bcoin.packets.NetworkAddress; /** * Represents a remote peer. @@ -288,17 +293,7 @@ Peer.prototype._onConnect = function _onConnect() { }); // Say hello. - this.write(this.framer.version({ - version: constants.VERSION, - services: constants.LOCAL_SERVICES, - ts: bcoin.now(), - remote: new NetworkAddress(), - local: this.pool.address, - nonce: this.pool.localNonce, - agent: constants.USER_AGENT, - height: this.chain.height, - relay: this.options.relay, - })); + this.sendVersion(); // Advertise our address. if (this.pool.address.host !== '0.0.0.0' @@ -402,8 +397,7 @@ Peer.prototype.announce = function announce(items) { */ Peer.prototype.sendInv = function sendInv(items) { - var inv = []; - var i, item, chunk; + var i, chunk; if (this.destroyed) return; @@ -411,20 +405,17 @@ Peer.prototype.sendInv = function sendInv(items) { if (!Array.isArray(items)) items = [items]; - for (i = 0; i < items.length; i++) { - item = items[i]; - this.invFilter.add(item.hash, 'hex'); - inv.push(item); - } + for (i = 0; i < items.length; i++) + this.invFilter.add(items[i].hash, 'hex'); - if (inv.length === 0) + if (items.length === 0) return; bcoin.debug('Serving %d inv items to %s.', - inv.length, this.hostname); + items.length, this.hostname); - for (i = 0; i < inv.length; i += 50000) { - chunk = inv.slice(i, i + 50000); + for (i = 0; i < items.length; i += 50000) { + chunk = items.slice(i, i + 50000); this.write(this.framer.inv(chunk)); } }; @@ -435,8 +426,7 @@ Peer.prototype.sendInv = function sendInv(items) { */ Peer.prototype.sendHeaders = function sendHeaders(items) { - var headers = []; - var i, item, chunk; + var i, chunk; if (this.destroyed) return; @@ -444,24 +434,41 @@ Peer.prototype.sendHeaders = function sendHeaders(items) { if (!Array.isArray(items)) items = [items]; - for (i = 0; i < items.length; i++) { - item = items[i]; - this.invFilter.add(item.hash()); - headers.push(item); - } + for (i = 0; i < items.length; i++) + this.invFilter.add(items[i].hash()); - if (headers.length === 0) + if (items.length === 0) return; bcoin.debug('Serving %d headers to %s.', - headers.length, this.hostname); + items.length, this.hostname); - for (i = 0; i < headers.length; i += 2000) { - chunk = headers.slice(i, i + 2000); + for (i = 0; i < items.length; i += 2000) { + chunk = items.slice(i, i + 2000); this.write(this.framer.headers(chunk)); } }; +/** + * Send a `version` packet. + */ + +Peer.prototype.sendVersion = function sendVersion() { + var packet = new VersionPacket({ + version: constants.VERSION, + services: constants.LOCAL_SERVICES, + ts: bcoin.now(), + recv: new NetworkAddress(), + from: this.pool.address, + nonce: this.pool.localNonce, + agent: constants.USER_AGENT, + height: this.chain.height, + relay: this.options.relay + }); + + this.write(this.framer.version(packet)); +}; + /** * Send a `ping` packet. */ @@ -678,7 +685,17 @@ Peer.prototype.response = function response(cmd, payload) { */ Peer.prototype.getData = function getData(items) { - this.write(this.framer.getData(items)); + var data = new Array(items.length); + var i, item; + + for (i = 0; i < items.length; i++) { + item = items[i]; + if (item.toInv) + item = item.toInv(); + data[i] = item; + } + + this.write(this.framer.getData(data)); }; /** @@ -1121,7 +1138,7 @@ Peer.prototype._handleGetHeaders = function _handleGetHeaders(payload) { }); } - if (!payload.locator) + if (payload.locator.length === 0) return collect(null, payload.stop); this.chain.findLocator(payload.locator, function(err, hash) { @@ -1185,7 +1202,7 @@ Peer.prototype._handleGetBlocks = function _handleGetBlocks(payload) { if (!hash) return done(); - blocks.push({ type: constants.inv.BLOCK, hash: hash }); + blocks.push(new InvItem(constants.inv.BLOCK, hash)); if (hash === payload.stop) return done(); @@ -1207,27 +1224,25 @@ Peer.prototype._handleGetBlocks = function _handleGetBlocks(payload) { * @param {Object} */ -Peer.prototype._handleVersion = function _handleVersion(payload) { +Peer.prototype._handleVersion = function _handleVersion(version) { var self = this; - var version = payload.version; - var services = payload.services; if (this.network.type !== 'regtest') { - if (payload.nonce.cmp(this.pool.localNonce) === 0) { + if (version.nonce.cmp(this.pool.localNonce) === 0) { this._error('We connected to ourself. Oops.'); this.setMisbehavior(100); return; } } - if (version < constants.MIN_VERSION) { + if (version.version < constants.MIN_VERSION) { this._error('Peer doesn\'t support required protocol version.'); this.setMisbehavior(100); return; } if (this.options.headers) { - if (version < 31800) { + if (!version.hasHeaders()) { this._error('Peer doesn\'t support getheaders.'); this.setMisbehavior(100); return; @@ -1235,7 +1250,7 @@ Peer.prototype._handleVersion = function _handleVersion(payload) { } if (this.options.network) { - if (!(services & constants.services.NETWORK)) { + if (!version.hasNetwork()) { this._error('Peer does not support network services.'); this.setMisbehavior(100); return; @@ -1243,7 +1258,7 @@ Peer.prototype._handleVersion = function _handleVersion(payload) { } if (this.options.spv) { - if (version < 70011 || !(services & constants.services.BLOOM)) { + if (!version.hasBloom()) { this._error('Peer does not support bip37.'); this.setMisbehavior(100); return; @@ -1251,7 +1266,7 @@ Peer.prototype._handleVersion = function _handleVersion(payload) { } if (this.options.witness) { - if (!(services & constants.services.WITNESS)) { + if (!version.hasWitness()) { this.request('havewitness', function(err) { if (err) { self._error('Peer does not support segregated witness.'); @@ -1261,16 +1276,16 @@ Peer.prototype._handleVersion = function _handleVersion(payload) { } } - if (payload.witness) + if (version.hasWitness()) this.haveWitness = true; - if (payload.relay === false) + if (version.relay === false) this.relay = false; // ACK this.write(this.framer.verack()); - this.version = payload; - this.fire('version', payload); + this.version = version; + this.fire('version', version); }; /** @@ -1310,7 +1325,7 @@ Peer.prototype._handleMempool = function _handleMempool() { return done(err); for (i = 0; i < hashes.length; i++) - items.push({ type: constants.inv.TX, hash: hashes[i] }); + items.push(new InvItem(constants.inv.TX, hashes[i])); bcoin.debug('Sending mempool snapshot (%s).', self.hostname); @@ -1396,7 +1411,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { if (type === constants.inv.TX) { if (!self.mempool) { - notfound.push({ type: constants.inv.TX, hash: hash }); + notfound.push(new InvItem(constants.inv.TX, hash)); return next(); } return self.mempool.getEntry(hash, function(err, entry) { @@ -1430,11 +1445,11 @@ Peer.prototype._handleGetData = function _handleGetData(items) { if (type === constants.inv.BLOCK) { if (self.chain.db.options.spv) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } if (self.chain.db.options.prune) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } return self.chain.db.getBlock(hash, function(err, block) { @@ -1442,7 +1457,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { return next(err); if (!block) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } @@ -1453,10 +1468,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { self.write(data); if (hash === self.hashContinue) { - self.sendInv({ - type: constants.inv.BLOCK, - hash: self.chain.tip.hash - }); + self.sendInv(new InvItem(constants.inv.BLOCK, self.chain.tip.hash)); self.hashContinue = null; } @@ -1466,11 +1478,11 @@ Peer.prototype._handleGetData = function _handleGetData(items) { if (type === constants.inv.FILTERED_BLOCK) { if (self.chain.db.options.spv) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } if (self.chain.db.options.prune) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } return self.chain.db.getBlock(hash, function(err, block) { @@ -1478,7 +1490,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { return next(err); if (!block) { - notfound.push({ type: constants.inv.BLOCK, hash: hash }); + notfound.push(new InvItem(constants.inv.BLOCK, hash)); return next(); } @@ -1497,10 +1509,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { } if (hash === self.hashContinue) { - self.sendInv({ - type: constants.inv.BLOCK, - hash: self.chain.tip.hash - }); + self.sendInv(new InvItem(constants.inv.BLOCK, self.chain.tip.hash)); self.hashContinue = null; } @@ -1508,7 +1517,7 @@ Peer.prototype._handleGetData = function _handleGetData(items) { }); } - notfound.push({ type: type, hash: hash }); + notfound.push(new InvItem(type, hash)); return next(); }, function(err) { @@ -1725,24 +1734,21 @@ Peer.prototype._handleReject = function _handleReject(payload) { * @param {Object} */ -Peer.prototype._handleAlert = function _handleAlert(details) { - this.invFilter.add(details.hash, 'hex'); - this.fire('alert', details); +Peer.prototype._handleAlert = function _handleAlert(alert) { + this.invFilter.add(alert.hash()); + this.fire('alert', alert); }; /** * Send an `alert` to peer. - * @param {AlertPacket} details - * @param {Buffer|KeyPair} [key=network.alertPrivateKey] + * @param {AlertPacket} alert */ -Peer.prototype.sendAlert = function sendAlert(details, key) { - var data = bcoin.protocol.framer.alert(details, key); - - if (!this.invFilter.added(details.hash, 'hex')) +Peer.prototype.sendAlert = function sendAlert(alert) { + if (!this.invFilter.added(alert.hash())) return; - this.write(this.framer.packet('alert', data)); + this.write(this.framer.alert(alert)); }; /** @@ -1753,6 +1759,8 @@ Peer.prototype.sendAlert = function sendAlert(details, key) { */ Peer.prototype.sendGetHeaders = function sendGetHeaders(locator, stop) { + var packet = new GetBlocksPacket(locator, stop); + bcoin.debug( 'Requesting headers packet from peer with getheaders (%s).', this.hostname); @@ -1762,7 +1770,7 @@ Peer.prototype.sendGetHeaders = function sendGetHeaders(locator, stop) { locator && locator.length ? utils.revHex(locator[0]) : 0, stop ? utils.revHex(stop) : 0); - this.write(this.framer.getHeaders({ locator: locator, stop: stop })); + this.write(this.framer.getHeaders(packet)); }; /** @@ -1772,6 +1780,8 @@ Peer.prototype.sendGetHeaders = function sendGetHeaders(locator, stop) { */ Peer.prototype.sendGetBlocks = function getBlocks(locator, stop) { + var packet = new GetBlocksPacket(locator, stop); + bcoin.debug( 'Requesting inv packet from peer with getblocks (%s).', this.hostname); @@ -1781,7 +1791,7 @@ Peer.prototype.sendGetBlocks = function getBlocks(locator, stop) { locator && locator.length ? utils.revHex(locator[0]) : 0, stop ? utils.revHex(stop) : 0); - this.write(this.framer.getBlocks({ locator: locator, stop: stop })); + this.write(this.framer.getBlocks(packet)); }; /** @@ -1798,15 +1808,27 @@ Peer.prototype.sendMempool = function sendMempool() { /** * Send `reject` to peer. - * @param {Object} details - See {@link Framer.reject}. + * @param {Object} reject - See {@link Framer.reject}. */ -Peer.prototype.sendReject = function sendReject(details) { +Peer.prototype.sendReject = function sendReject(code, reason, obj) { + var reject = RejectPacket.fromReason(code, reason, obj); + + if (obj) { + bcoin.debug('Rejecting %s %s (%s): ccode=%s reason=%s.', + reject.message, obj.rhash, this.hostname, code, reason); + + this.pool.rejects.add(obj.hash()); + } else { + bcoin.debug('Rejecting packet from %s: ccode=%s reason=%s.', + this.hostname, code, reason); + } + bcoin.debug( 'Sending reject packet to peer (%s).', this.hostname); - this.write(this.framer.reject(details)); + this.write(this.framer.reject(reject)); }; /** @@ -1837,34 +1859,8 @@ Peer.prototype.setMisbehavior = function setMisbehavior(score) { */ Peer.prototype.reject = function reject(obj, code, reason, score) { - var type; - - if (obj) { - type = (obj instanceof bcoin.tx) ? 'tx' : 'block'; - - bcoin.debug('Rejecting %s %s (%s): ccode=%s reason=%s.', - type, obj.rhash, this.hostname, code, reason); - - this.sendReject({ - message: type, - ccode: code, - reason: reason, - data: obj.hash() - }); - - this.pool.rejects.add(obj.hash()); - } else { - bcoin.debug('Rejecting packet from %s: ccode=%s reason=%s.', - this.hostname, code, reason); - - this.sendReject({ - ccode: code, - reason: reason, - data: null - }); - } - - if (score != null) + this.sendReject(code, reason, obj); + if (score > 0) this.setMisbehavior(score); }; @@ -1994,224 +1990,6 @@ Peer.prototype.inspect = function inspect() { + '>'; }; -/** - * Represents a network address. - * @exports NetworkAddress - * @constructor - * @param {NakedNetworkAddress} options - * @property {Number} id - * @property {Host} host - * @property {Number} port - * @property {Number} services - * @property {Number} ts - */ - -function NetworkAddress(options) { - if (!(this instanceof NetworkAddress)) - return new NetworkAddress(options); - - this.id = NetworkAddress.uid++; - this.host = '0.0.0.0'; - this.port = 0; - this.services = 0; - this.ts = 0; - - if (options) - this.fromOptions(options); -} - -/** - * Globally incremented unique id. - * @private - * @type {Number} - */ - -NetworkAddress.uid = 0; - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -NetworkAddress.prototype.fromOptions = function fromOptions(options) { - var host = options.host; - - if (IP.version(host) !== -1) - host = IP.normalize(host); - - assert(typeof options.host === 'string'); - assert(typeof options.port === 'number'); - assert(typeof options.services === 'number'); - assert(typeof options.ts === 'number'); - - this.host = host; - this.port = options.port; - this.services = options.services; - this.ts = options.ts; - - return this; -}; - -/** - * Instantiate network address from options. - * @param {Object} options - * @returns {NetworkAddress} - */ - -NetworkAddress.fromOptions = function fromOptions(options) { - return new NetworkAddress().fromOptions(options); -}; - -/** - * Test whether the `host` field is an ip address. - * @returns {Boolean} - */ - -NetworkAddress.prototype.isIP = function isIP() { - return IP.version(this.host) !== -1; -}; - -/** - * Test whether the NETWORK service bit is set. - * @returns {Boolean} - */ - -NetworkAddress.prototype.hasNetwork = function hasNetwork() { - return (this.services & constants.services.NETWORK) !== 0; -}; - -/** - * Test whether the BLOOM service bit is set. - * @returns {Boolean} - */ - -NetworkAddress.prototype.hasBloom = function hasBloom() { - return (this.services & constants.services.BLOOM) !== 0; -}; - -/** - * Test whether the GETUTXO service bit is set. - * @returns {Boolean} - */ - -NetworkAddress.prototype.hasUTXO = function hasUTXO() { - return (this.services & constants.services.GETUTXO) !== 0; -}; - -/** - * Test whether the WITNESS service bit is set. - * @returns {Boolean} - */ - -NetworkAddress.prototype.hasWitness = function hasWitness() { - return (this.services & constants.services.WITNESS) !== 0; -}; - -/** - * Inspect the network address. - * @returns {Object} - */ - -NetworkAddress.prototype.inspect = function inspect() { - return ''; -}; - -/** - * Inject properties from hostname and network. - * @private - * @param {String} hostname - * @param {Network|NetworkType} network - */ - -NetworkAddress.prototype.fromHostname = function fromHostname(hostname, network) { - var address = IP.parseHost(hostname); - - network = bcoin.network.get(network); - - this.host = address.host; - this.port = address.port || network.port; - this.services = constants.services.NETWORK - | constants.services.BLOOM - | constants.services.WITNESS; - this.ts = bcoin.now(); - - return this; -}; - -/** - * Instantiate a network address - * from a hostname (i.e. 127.0.0.1:8333). - * @param {String} hostname - * @param {(Network|NetworkType)?} network - * @returns {NetworkAddress} - */ - -NetworkAddress.fromHostname = function fromHostname(hostname, network) { - return new NetworkAddress().fromHostname(hostname, network); -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - * @param {Boolean?} full - Include timestamp. - */ - -NetworkAddress.prototype.fromRaw = function fromRaw(data, full) { - var p = bcoin.reader(data); - var now = bcoin.now(); - - // only version >= 31402 - this.ts = full ? p.readU32() : 0; - this.services = p.readU53(); - this.host = IP.toString(p.readBytes(16)); - this.port = p.readU16BE(); - - if (this.ts <= 100000000 || this.ts > now + 10 * 60) - this.ts = now - 5 * 24 * 60 * 60; - - return this; -}; - -/** - * Insantiate a network address from serialized data. - * @param {Buffer} data - * @param {Boolean?} full - Include timestamp. - * @returns {NetworkAddress} - */ - -NetworkAddress.fromRaw = function fromRaw(data, full) { - return new NetworkAddress().fromRaw(data, full); -}; - -/** - * Serialize network address. - * @param {Boolean} full - Include timestamp. - * @returns {Buffer} - */ - -NetworkAddress.prototype.toRaw = function toRaw(full, writer) { - var p = bcoin.writer(writer); - - if (full) - p.writeU32(this.ts); - - p.writeU64(this.services); - p.writeBytes(IP.toBuffer(this.host)); - p.writeU16BE(this.port); - - if (!writer) - p = p.render(); - - return p; -}; - /* * Helpers */ @@ -2224,7 +2002,4 @@ function compare(a, b) { * Expose */ -exports = Peer; -exports.NetworkAddress = NetworkAddress; - -module.exports = exports; +module.exports = Peer; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 0ba280e4..7cf25027 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -14,7 +14,8 @@ var IP = require('./ip'); var assert = utils.assert; var constants = bcoin.protocol.constants; var VerifyError = bcoin.errors.VerifyError; -var NetworkAddress = bcoin.peer.NetworkAddress; +var NetworkAddress = bcoin.packets.NetworkAddress; +var InvItem = bcoin.packets.InvItem; /** * A pool of peers for handling all network activity. @@ -887,21 +888,20 @@ Pool.prototype.sendMempool = function sendMempool() { /** * Send `alert` to all peers. - * @param {AlertPacket} details - * @param {Buffer|KeyPair} [key=network.alertPrivateKey] + * @param {AlertPacket} alert */ -Pool.prototype.sendAlert = function sendAlert(details, key) { +Pool.prototype.sendAlert = function sendAlert(alert) { var i; if (this.peers.load) - this.peers.load.sendAlert(details, key); + this.peers.load.sendAlert(alert); for (i = 0; i < this.peers.regular.length; i++) - this.peers.regular[i].sendAlert(details, key); + this.peers.regular[i].sendAlert(alert); for (i = 0; i < this.peers.leeches.length; i++) - this.peers.leeches[i].sendAlert(details, key); + this.peers.leeches[i].sendAlert(alert); }; /** @@ -1012,8 +1012,8 @@ Pool.prototype._createPeer = function _createPeer(options) { self.emit('reject', payload, peer); }); - peer.on('alert', function(details) { - self._handleAlert(details, peer); + peer.on('alert', function(alert) { + self._handleAlert(alert, peer); }); peer.on('notfound', function(items) { @@ -1120,38 +1120,36 @@ Pool.prototype._createPeer = function _createPeer(options) { /** * Handle an alert packet. * @private - * @param {AlertPacket} details + * @param {AlertPacket} alert * @param {Peer} peer */ -Pool.prototype._handleAlert = function _handleAlert(details, peer) { - var hash = new Buffer(details.hash, 'hex'); - var signature = details.signature; +Pool.prototype._handleAlert = function _handleAlert(alert, peer) { var now = bcoin.now(); - if (!this.rejects.added(hash)) + if (!this.rejects.added(alert.hash())) return; - if (!bcoin.ec.verify(hash, signature, this.network.alertKey)) { + if (!alert.verify(this.network.alertKey)) { bcoin.debug('Peer sent a phony alert packet (%s).', peer.hostname); // Let's look at it because why not? - bcoin.debug(details); + bcoin.debug(alert); peer.setMisbehavior(100); return; } - if (now >= details.relayUntil || now >= details.expiration) { + if (now >= alert.relayUntil || now >= alert.expiration) { bcoin.debug('Peer sent an expired alert packet (%s).', peer.hostname); - bcoin.debug(details); + bcoin.debug(alert); return; } bcoin.debug('Received alert from peer (%s).', peer.hostname); - bcoin.debug(details); + bcoin.debug(alert); - this.sendAlert(details); + this.sendAlert(alert); - this.emit('alert', details, peer); + this.emit('alert', alert, peer); }; /** @@ -2111,6 +2109,15 @@ LoadRequest.prototype.inspect = function inspect() { + '>'; }; +/** + * Convert load request to an inv item. + * @returns {InvItem} + */ + +LoadRequest.prototype.toInv = function toInv() { + return new InvItem(this.type, this.hash); +}; + /** * Represents an item that is broadcasted via an inv/getdata cycle. * @exports BroadcastItem @@ -2256,7 +2263,7 @@ BroadcastItem.prototype.send = function send(peer, witness) { // Failsafe - we never want to relay coinbases. // They are an insta-ban from any bitcoind node. if (this.msg.isCoinbase()) { - peer.write(peer.framer.notFound([this])); + peer.write(peer.framer.notFound([this.toInv()])); bcoin.debug('Failsafe: tried to relay a coinbase.'); this.finish(new Error('Coinbase.')); return true; @@ -2317,6 +2324,7 @@ BroadcastItem.prototype.reject = function reject(peer) { /** * Inspect the broadcast item. + * @returns {String} */ BroadcastItem.prototype.inspect = function inspect() { @@ -2327,6 +2335,15 @@ BroadcastItem.prototype.inspect = function inspect() { + '>'; }; +/** + * Convert broadcast item to an inv item. + * @returns {InvItem} + */ + +BroadcastItem.prototype.toInv = function toInv() { + return new InvItem(this.type, this.hash); +}; + /* * Helpers */ diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index dcb1a625..22c4a08b 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -8,7 +8,6 @@ 'use strict'; var bcoin = require('../env'); -var constants = require('./constants'); var utils = require('../utils'); var assert = utils.assert; var BufferWriter = require('../writer'); @@ -391,9 +390,8 @@ Framer.prototype.addr = function addr(peers) { * @returns {Buffer} alert packet. */ -Framer.prototype.alert = function alert(options) { - options.network = this.network; - return this.packet('alert', Framer.alert(options)); +Framer.prototype.alert = function _alert(alert) { + return this.packet('alert', Framer.alert(alert)); }; /** @@ -413,23 +411,8 @@ Framer.prototype.feeFilter = function feeFilter(options) { * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. */ -Framer.version = function version(options, writer) { - var p = new BufferWriter(writer); - - p.write32(options.version); - p.writeU64(options.services); - p.write64(options.ts); - options.remote.toRaw(false, p); - options.local.toRaw(false, p); - p.writeU64(options.nonce); - p.writeVarString(options.agent, 'ascii'); - p.write32(options.height || 0); - p.writeU8(options.relay ? 1 : 0); - - if (!writer) - p = p.render(); - - return p; +Framer.version = function _version(version, writer) { + return version.toRaw(writer); }; /** @@ -441,7 +424,7 @@ Framer.verack = function verack() { return DUMMY; }; -/** + /** * Create an inv, getdata, or notfound packet. * @private * @param {InvItem[]} items @@ -450,21 +433,14 @@ Framer.verack = function verack() { Framer._inv = function _inv(items, writer) { var p = new BufferWriter(writer); - var type; var i; assert(items.length <= 50000); p.writeVarint(items.length); - for (i = 0; i < items.length; i++) { - type = items[i].type; - if (typeof type === 'string') - type = constants.inv[items[i].type.toUpperCase()]; - assert(constants.invByVal[type] != null); - p.writeU32(type); - p.writeHash(items[i].hash); - } + for (i = 0; i < items.length; i++) + items[i].toRaw(p); if (!writer) p = p.render(); @@ -550,7 +526,7 @@ Framer.filterLoad = function filterLoad(filter, writer) { */ Framer.getHeaders = function getHeaders(data, writer) { - return Framer._getBlocks(data, writer, true); + return data.toRaw(writer); }; /** @@ -561,51 +537,7 @@ Framer.getHeaders = function getHeaders(data, writer) { */ Framer.getBlocks = function getBlocks(data, writer) { - return Framer._getBlocks(data, writer, false); -}; - -/** - * Create a getblocks or getheaders packet. - * @private - * @param {GetBlocksPacket} data - * @param {BufferWriter|null} writer - * @param {Boolean} headers - * @returns {Buffer} - */ - -Framer._getBlocks = function _getBlocks(data, writer, headers) { - var version = data.version; - var locator = data.locator; - var stop = data.stop; - var p, i; - - if (!version) - version = constants.VERSION; - - if (!locator) { - if (headers) - locator = []; - else - assert(false, 'getblocks requires a locator'); - } - - if (!stop) - stop = constants.ZERO_HASH; - - p = new BufferWriter(writer); - - p.writeU32(version); - p.writeVarint(locator.length); - - for (i = 0; i < locator.length; i++) - p.writeHash(locator[i]); - - p.writeHash(stop); - - if (!writer) - p = p.render(); - - return p; + return data.toRaw(writer); }; /** @@ -694,30 +626,8 @@ Framer.headers = function _headers(headers, writer) { * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. */ -Framer.reject = function reject(details, writer) { - var p = new BufferWriter(writer); - var ccode = details.ccode; - - if (typeof ccode === 'string') - ccode = constants.reject[ccode.toUpperCase()]; - - if (!ccode) - ccode = constants.reject.INVALID; - - if (ccode >= constants.reject.INTERNAL) - ccode = constants.reject.INVALID; - - p.writeVarString(details.message || '', 'ascii'); - p.writeU8(ccode); - p.writeVarString(details.reason || '', 'ascii'); - - if (details.data) - p.writeHash(details.data); - - if (!writer) - p = p.render(); - - return p; +Framer.reject = function _reject(reject, writer) { + return reject.toRaw(writer); }; /** @@ -750,104 +660,8 @@ Framer.addr = function addr(hosts, writer) { * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. */ -Framer.alert = function alert(data, key, writer) { - var network = bcoin.network.get(data.network); - var p, i, payload, hash, time; - var version, relayUntil, expiration, id, cancel; - var cancels, minVer, maxVer, subVers; - var priority, comment, statusBar, reserved; - - if (!key && network.alertPrivateKey) - key = network.alertPrivateKey; - - if (!data.payload) { - p = new BufferWriter(); - time = bcoin.now() + 7 * 86400; - - version = data.version; - relayUntil = data.relayUntil; - expiration = data.expiration; - id = data.id; - cancels = data.cancels || []; - minVer = data.minVer; - maxVer = data.maxVer; - subVers = data.subVers || []; - priority = data.priority; - comment = data.comment || ''; - statusBar = data.statusBar || ''; - reserved = data.reserved || ''; - - if (version == null) - version = 1; - - if (relayUntil == null) - relayUntil = time; - - if (expiration == null) - expiration = time; - - if (id == null) - id = 1; - - if (cancel == null) - cancel = 0; - - if (minVer == null) - minVer = 10000; - - if (maxVer == null) - maxVer = constants.VERSION; - - if (priority == null) - priority = 100; - - p.write32(version); - p.write64(relayUntil); - p.write64(expiration); - p.write32(id); - p.write32(cancel); - - p.writeVarint(cancels.length); - for (i = 0; i < cancels.length; i++) - p.write32(cancels[i]); - - p.write32(minVer); - p.write32(maxVer); - - p.writeVarint(subVers.length); - for (i = 0; i < subVers.length; i++) - p.writeVarString(subVers[i], 'ascii'); - - p.write32(priority); - p.writeVarString(comment, 'ascii'); - p.writeVarString(statusBar, 'ascii'); - p.writeVarString(reserved, 'ascii'); - - payload = p.render(); - } else { - payload = data.payload; - } - - if (!data.signature) { - assert(key, 'No key or signature.'); - hash = utils.hash256(payload); - data.signature = bcoin.ec.sign(hash, key); - } - - if (!data.hash) { - if (!hash) - hash = utils.hash256(payload); - data.hash = hash.toString('hex'); - } - - p = new BufferWriter(writer); - p.writeVarBytes(payload); - p.writeVarBytes(data.signature); - - if (!writer) - p = p.render(); - - return p; +Framer.alert = function _alert(alert, writer) { + return alert.toRaw(writer); }; /** @@ -879,7 +693,7 @@ Framer.getAddr = function getAddr() { Framer.getUTXOs = function getUTXOs(data, writer) { var p = new BufferWriter(writer); - var i, prevout; + var i; p.writeU8(data.mempool ? 1 : 0); p.writeVarint(data.prevout.length); diff --git a/lib/bcoin/protocol/index.js b/lib/bcoin/protocol/index.js index 2ed8f258..a8014554 100644 --- a/lib/bcoin/protocol/index.js +++ b/lib/bcoin/protocol/index.js @@ -11,3 +11,4 @@ exports.constants = require('./constants'); exports.network = require('./network'); exports.framer = require('./framer'); exports.parser = require('./parser'); +exports.packets = require('./packets'); diff --git a/lib/bcoin/protocol/packets.js b/lib/bcoin/protocol/packets.js new file mode 100644 index 00000000..34a264bb --- /dev/null +++ b/lib/bcoin/protocol/packets.js @@ -0,0 +1,1025 @@ +/*! + * packets.js - packets for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('../env'); +var constants = require('./constants'); +var utils = require('../utils'); +var bn = require('bn.js'); +var IP = require('../ip'); +var assert = utils.assert; + +/** + * Version Packet + * @constructor + * @exports VersionPacket + * @param {Object?} options + * @param {Number} options.version - Protocol version. + * @param {Number} options.services - Service bits. + * @param {Number} options.ts - Timestamp of discovery. + * @param {NetworkAddress} options.local - Our address. + * @param {NetworkAddress} options.remote - Their address. + * @param {BN} options.nonce + * @param {String} options.agent - User agent string. + * @param {Number} options.height - Chain height. + * @param {Boolean} options.relay - Whether transactions + * should be relayed immediately. + * @property {Number} version - Protocol version. + * @property {Number} services - Service bits. + * @property {Number} ts - Timestamp of discovery. + * @property {NetworkAddress} local - Our address. + * @property {NetworkAddress} remote - Their address. + * @property {BN} nonce + * @property {String} agent - User agent string. + * @property {Number} height - Chain height. + * @property {Boolean} relay - Whether transactions + * should be relayed immediately. + */ + +function VersionPacket(options) { + if (!(this instanceof VersionPacket)) + return new VersionPacket(options); + + this.version = constants.VERSION; + this.services = constants.LOCAL_SERVICES; + this.ts = 0; + this.recv = null; + this.from = null; + this.nonce = null; + this.agent = constants.USER_AGENT; + this.height = 0; + this.relay = true; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options. + * @private + * @param {Object} options + */ + +VersionPacket.prototype.fromOptions = function fromOptions(options) { + this.version = options.version != null + ? options.version + : constants.VERSION; + this.services = options.services != null + ? options.services + : constants.LOCAL_SERVICES; + this.ts = options.ts != null + ? options.ts + : bcoin.now(); + this.recv = options.recv || new NetworkAddress(); + this.from = options.from || new NetworkAddress(); + this.nonce = options.nonce || utils.nonce(); + this.agent = options.agent || constants.USER_AGENT; + this.height = options.height || 0; + this.relay = options.relay != null ? options.relay : true; + return this; +}; + +/** + * Instantiate version packet from options. + * @param {Object} options + * @returns {VersionPacket} + */ + +VersionPacket.fromOptions = function fromOptions(options) { + return new VersionPacket().fromOptions(options); +}; + +/** + * Serialize version packet. + * @returns {Buffer} + */ + +VersionPacket.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.write32(this.version); + p.writeU64(this.services); + p.write64(this.ts); + this.recv.toRaw(false, p); + this.from.toRaw(false, p); + p.writeU64(this.nonce); + p.writeVarString(this.agent, 'ascii'); + p.write32(this.height); + p.writeU8(this.relay ? 1 : 0); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Test whether the NETWORK service bit is set. + * @returns {Boolean} + */ + +VersionPacket.prototype.hasNetwork = function hasNetwork() { + return (this.services & constants.services.NETWORK) !== 0; +}; + +/** + * Test whether the BLOOM service bit is set. + * @returns {Boolean} + */ + +VersionPacket.prototype.hasBloom = function hasBloom() { + return this.version >= 70011 + && (this.services & constants.services.BLOOM) !== 0; +}; + +/** + * Test whether the GETUTXO service bit is set. + * @returns {Boolean} + */ + +VersionPacket.prototype.hasUTXO = function hasUTXO() { + return (this.services & constants.services.GETUTXO) !== 0; +}; + +/** + * Test whether the WITNESS service bit is set. + * @returns {Boolean} + */ + +VersionPacket.prototype.hasWitness = function hasWitness() { + return (this.services & constants.services.WITNESS) !== 0; +}; + +/** + * Test whether the protocol version supports getheaders. + * @returns {Boolean} + */ + +VersionPacket.prototype.hasHeaders = function hasHeaders() { + return this.version >= 31800; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +VersionPacket.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + + this.version = p.read32(); + this.services = p.readU53(); + this.ts = p.read53(); + this.recv = NetworkAddress.fromRaw(p, false); + + if (p.left() > 0) { + this.from = NetworkAddress.fromRaw(p, false); + this.nonce = p.readU64(); + } else { + this.from = new NetworkAddress(); + this.nonce = new bn(0); + } + + if (p.left() > 0) + this.agent = p.readVarString('ascii', 256); + else + this.agent = ''; + + if (p.left() > 0) + this.height = p.read32(); + else + this.height = 0; + + if (p.left() > 0) + this.relay = p.readU8() === 1; + else + this.relay = true; + + if (this.version === 10300) + this.version = 300; + + assert(this.version >= 0, 'Version is negative.'); + assert(this.ts >= 0, 'Timestamp is negative.'); + assert(this.height >= 0, 'Height is negative.'); + + return this; +}; + +/** + * Instantiate version packet from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {VersionPacket} + */ + +VersionPacket.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new VersionPacket().fromRaw(data, enc); +}; + +/** + * Inv Item + * @param {Number} type + * @param {Hash} hash + * @property {InvType} type + * @property {Hash} hash + */ + +function InvItem(type, hash) { + if (!(this instanceof InvItem)) + return new InvItem(type, hash); + + this.type = type; + this.hash = hash; +} + +/** + * Serialize inv item. + * @returns {Buffer} + */ + +InvItem.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.writeU32(this.type); + p.writeHash(this.hash); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @param {Buffer} data + */ + +InvItem.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + this.type = p.readU32(); + this.hash = p.readHash('hex'); + return this; +}; + +/** + * Instantiate inv item from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {InvItem} + */ + +InvItem.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new InvItem().fromRaw(data); +}; + +/** + * Test whether the inv item has the witness bit set. + * @returns {Boolean} + */ + +InvItem.prototype.isWitness = function isWitness() { + return (this.type & constants.WITNESS_MASK) !== 0; +}; + +/** + * Represents a `getblocks` packet. + * @exports GetBlocksPacket + * @constructor + * @param {Hash[]} locator + * @param {Hash?} stop + * @property {Hash[]} locator + * @property {Hash|null} stop + */ + +function GetBlocksPacket(locator, stop) { + if (!(this instanceof GetBlocksPacket)) + return new GetBlocksPacket(locator, stop); + + this.version = constants.VERSION; + this.locator = locator || []; + this.stop = stop || null; +} + +/** + * Serialize getblocks packet. + * @returns {Buffer} + */ + +GetBlocksPacket.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + var i; + + p.writeU32(this.version); + p.writeVarint(this.locator.length); + + for (i = 0; i < this.locator.length; i++) + p.writeHash(this.locator[i]); + + p.writeHash(this.stop || constants.ZERO_HASH); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +GetBlocksPacket.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var i, count; + + this.version = p.readU32(); + + count = p.readVarint(); + + for (i = 0; i < count; i++) + this.locator.push(p.readHash('hex')); + + this.stop = p.readHash('hex'); + + if (this.stop === constants.NULL_HASH) + this.stop = null; + + return this; +}; + +/** + * Instantiate getblocks packet from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {GetBlocksPacket} + */ + +GetBlocksPacket.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new GetBlocksPacket().fromRaw(data); +}; + +/** + * Alert Packet + * @exports AlertPacket + * @constructor + * @param {Object} options + * @property {Number} version + * @property {Number} relayUntil + * @property {Number} expiration + * @property {Number} id + * @property {Number} cancel + * @property {Number[]} cancels + * @property {Number} minVer + * @property {Number} maxVer + * @property {String[]} subVers + * @property {Number} priority + * @property {String} comment + * @property {String} statusBar + * @property {String?} reserved + * @property {Buffer?} signature - Payload signature. + */ + +function AlertPacket(options) { + if (!(this instanceof AlertPacket)) + return new AlertPacket(options); + + this.version = 0; + this.relayUntil = 0; + this.expiration = 0; + this.id = 0; + this.cancel = 0; + this.cancels = []; + this.minVer = 0; + this.maxVer = constants.VERSION; + this.subVers = []; + this.priority = 0; + this.comment = ''; + this.statusBar = ''; + this.reserved = ''; + this.signature = null; + + this._payload = null; + this._hash = null; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +AlertPacket.prototype.fromOptions = function fromOptions(options) { + var time = bcoin.now() + 7 * 86400; + + this.version = options.version != null ? options.version : 1; + this.relayUntil = options.relayUntil != null ? options.relayUntil : time; + this.expiration = options.expiration != null ? options.expiration : time; + this.id = options.id != null ? options.id : 0; + this.cancel = options.cancel != null ? options.cancel : 0; + this.cancels = options.cancels || []; + this.minVer = options.minVer != null ? options.minVer : 10000; + this.maxVer = options.maxVer != null ? options.maxVer : constants.VERSION; + this.subVers = options.subVers || []; + this.priority = options.priority != null ? options.priority : 100; + this.comment = options.comment || ''; + this.statusBar = options.statusBar || ''; + this.reserved = options.reserved || ''; + this.signature = options.signature; + + return this; +}; + +/** + * Instantiate alert packet from options. + * @param {Object} options + * @returns {AlertPacket} + */ + +AlertPacket.fromOptions = function fromOptions(options) { + return new AlertPacket().fromOptions(options); +}; + +/** + * Get the hash256 of the alert payload. + * @param {String?} enc + * @returns {Hash} + */ + +AlertPacket.prototype.hash = function hash(enc) { + if (!this._hash) + this._hash = utils.hash256(this.toPayload()); + return enc === 'hex' ? this._hash.toString('hex') : this._hash; +}; + +/** + * Serialize the packet to its payload. + * @returns {Buffer} + */ + +AlertPacket.prototype.toPayload = function toPayload() { + if (!this._payload) + this._payload = this.framePayload(); + + return this._payload; +}; + +/** + * Sign the alert packet payload. + * @param {Buffer} key - Private key. + */ + +AlertPacket.prototype.sign = function sign(key) { + this.signature = bcoin.ec.sign(this.hash(), key); +}; + +/** + * Verify the alert packet. + * @param {Buffer} key - Public key. + * @returns {Boolean} + */ + +AlertPacket.prototype.verify = function verify(key) { + return bcoin.ec.verify(this.hash(), this.signature, key); +}; + +/** + * Serialize the alert packet (includes payload _and_ signature). + * @returns {Buffer} + */ + +AlertPacket.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + p.writeVarBytes(this.toPayload()); + p.writeVarBytes(this.signature); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Serialize the alert packet payload. + * @private + * @returns {Buffer} + */ + +AlertPacket.prototype.framePayload = function framePayload(writer) { + var p = bcoin.writer(writer); + var i; + + p.write32(this.version); + p.write64(this.relayUntil); + p.write64(this.expiration); + p.write32(this.id); + p.write32(this.cancel); + + p.writeVarint(this.cancels.length); + for (i = 0; i < this.cancels.length; i++) + p.write32(this.cancels[i]); + + p.write32(this.minVer); + p.write32(this.maxVer); + + p.writeVarint(this.subVers.length); + for (i = 0; i < this.subVers.length; i++) + p.writeVarString(this.subVers[i], 'ascii'); + + p.write32(this.priority); + p.writeVarString(this.comment, 'ascii'); + p.writeVarString(this.statusBar, 'ascii'); + p.writeVarString(this.reserved, 'ascii'); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +AlertPacket.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + var i, count; + + this._payload = p.readVarBytes(); + this.signature = p.readVarBytes(); + + p = bcoin.reader(this._payload); + + this.version = p.read32(); + this.relayUntil = p.read53(); + this.expiration = p.read53(); + this.id = p.read32(); + this.cancel = p.read32(); + + count = p.readVarint(); + for (i = 0; i < count; i++) + this.cancels.push(p.read32()); + + this.minVer = p.read32(); + this.maxVer = p.read32(); + + count = p.readVarint(); + for (i = 0; i < count; i++) + this.subVers.push(p.readVarString('ascii')); + + this.priority = p.read32(); + this.comment = p.readVarString('ascii'); + this.statusBar = p.readVarString('ascii'); + this.reserved = p.readVarString('ascii'); + + return this; +}; + +/** + * Instantiate alert packet from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {AlertPacket} + */ + +AlertPacket.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new AlertPacket().fromRaw(data, enc); +}; + +/** + * Reject Packet + * @exports RejectPacket + * @constructor + * @property {(Number|String)?} code - Code + * (see {@link constants.reject}). + * @property {String?} msg - Message. + * @property {String?} reason - Reason. + * @property {(Hash|Buffer)?} data - Transaction or block hash. + */ + +function RejectPacket(options) { + if (!(this instanceof RejectPacket)) + return new RejectPacket(options); + + this.message = ''; + this.code = constants.reject.INVALID; + this.reason = ''; + this.data = null; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +RejectPacket.prototype.fromOptions = function fromOptions(options) { + var code = options.code; + + if (typeof code === 'string') + code = constants.reject[code.toUpperCase()]; + + if (!code) + code = constants.reject.INVALID; + + if (code >= constants.reject.INTERNAL) + code = constants.reject.INVALID; + + this.message = options.message || ''; + this.code = code; + this.reason = options.reason || ''; + this.data = options.data || null; + + return this; +}; + +/** + * Instantiate reject packet from options. + * @param {Object} options + * @returns {RejectPacket} + */ + +RejectPacket.fromOptions = function fromOptions(options) { + return new RejectPacket().fromOptions(options); +}; + +/** + * Serialize reject packet. + * @returns {Buffer} + */ + +RejectPacket.prototype.toRaw = function toRaw(writer) { + var p = bcoin.writer(writer); + + assert(this.message.length <= 12); + assert(this.reason.length <= 111); + + p.writeVarString(this.message, 'ascii'); + p.writeU8(this.code); + p.writeVarString(this.reason, 'ascii'); + + if (this.data) + p.writeHash(this.data); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + */ + +RejectPacket.prototype.fromRaw = function fromRaw(data) { + var p = bcoin.reader(data); + + this.message = p.readVarString('ascii', 12); + this.code = p.readU8(); + this.reason = p.readVarString('ascii', 111); + + if (this.message === 'block' || this.message === 'tx') + this.data = p.readHash('hex'); + else + this.data = null; + + return this; +}; + +/** + * Instantiate reject packet from serialized data. + * @param {Buffer} data + * @param {String?} enc + * @returns {RejectPacket} + */ + +RejectPacket.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new RejectPacket().fromRaw(data, enc); +}; + +/** + * Inject properties from reason message and object. + * @private + * @param {Number} code + * @param {String} reason + * @param {(TX|Block)?} obj + */ + +RejectPacket.prototype.fromReason = function fromReason(code, reason, obj) { + if (typeof code === 'string') + code = constants.reject[code.toUpperCase()]; + + if (!code) + code = constants.reject.INVALID; + + if (code >= constants.reject.INTERNAL) + code = constants.reject.INVALID; + + this.message = ''; + this.code = code; + this.reason = reason; + + if (obj) { + this.message = (obj instanceof bcoin.tx) ? 'tx' : 'block'; + this.data = obj.hash('hex'); + } + + return this; +}; + +/** + * Instantiate reject packet from reason message. + * @param {Number} code + * @param {String} reason + * @param {(TX|Block)?} obj + * @returns {RejectPacket} + */ + +RejectPacket.fromReason = function fromReason(code, reason, obj) { + return new RejectPacket().fromReason(code, reason, obj); +}; + +/** + * Instantiate reject packet from verify error. + * @param {VerifyError} err + * @param {(TX|Block)?} obj + * @returns {RejectPacket} + */ + +RejectPacket.fromError = function fromError(err, obj) { + return RejectPacket.fromReason(err.code, err.reason, obj); +}; + +/** + * Inspect reject packet. + * @returns {String} + */ + +RejectPacket.prototype.inspect = function inspect() { + return ''; +}; + +/** + * Represents a network address. + * @exports NetworkAddress + * @constructor + * @param {Object} options + * @param {Number?} options.ts - Timestamp. + * @param {Number?} options.services - Service bits. + * @param {String?} options.host - IP address (IPv6 or IPv4). + * @param {Number?} options.port - Port. + * @property {Number} id + * @property {Host} host + * @property {Number} port + * @property {Number} services + * @property {Number} ts + */ + +function NetworkAddress(options) { + if (!(this instanceof NetworkAddress)) + return new NetworkAddress(options); + + this.id = NetworkAddress.uid++; + this.host = '0.0.0.0'; + this.port = 0; + this.services = 0; + this.ts = 0; + + if (options) + this.fromOptions(options); +} + +/** + * Globally incremented unique id. + * @private + * @type {Number} + */ + +NetworkAddress.uid = 0; + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +NetworkAddress.prototype.fromOptions = function fromOptions(options) { + var host = options.host; + + if (IP.version(host) !== -1) + host = IP.normalize(host); + + assert(typeof options.host === 'string'); + assert(typeof options.port === 'number'); + assert(typeof options.services === 'number'); + assert(typeof options.ts === 'number'); + + this.host = host; + this.port = options.port; + this.services = options.services; + this.ts = options.ts; + + return this; +}; + +/** + * Instantiate network address from options. + * @param {Object} options + * @returns {NetworkAddress} + */ + +NetworkAddress.fromOptions = function fromOptions(options) { + return new NetworkAddress().fromOptions(options); +}; + +/** + * Test whether the `host` field is an ip address. + * @returns {Boolean} + */ + +NetworkAddress.prototype.isIP = function isIP() { + return IP.version(this.host) !== -1; +}; + +/** + * Test whether the NETWORK service bit is set. + * @returns {Boolean} + */ + +NetworkAddress.prototype.hasNetwork = function hasNetwork() { + return (this.services & constants.services.NETWORK) !== 0; +}; + +/** + * Test whether the BLOOM service bit is set. + * @returns {Boolean} + */ + +NetworkAddress.prototype.hasBloom = function hasBloom() { + return (this.services & constants.services.BLOOM) !== 0; +}; + +/** + * Test whether the GETUTXO service bit is set. + * @returns {Boolean} + */ + +NetworkAddress.prototype.hasUTXO = function hasUTXO() { + return (this.services & constants.services.GETUTXO) !== 0; +}; + +/** + * Test whether the WITNESS service bit is set. + * @returns {Boolean} + */ + +NetworkAddress.prototype.hasWitness = function hasWitness() { + return (this.services & constants.services.WITNESS) !== 0; +}; + +/** + * Inspect the network address. + * @returns {Object} + */ + +NetworkAddress.prototype.inspect = function inspect() { + return ''; +}; + +/** + * Inject properties from hostname and network. + * @private + * @param {String} hostname + * @param {Network|NetworkType} network + */ + +NetworkAddress.prototype.fromHostname = function fromHostname(hostname, network) { + var address = IP.parseHost(hostname); + + network = bcoin.network.get(network); + + this.host = address.host; + this.port = address.port || network.port; + this.services = constants.services.NETWORK + | constants.services.BLOOM + | constants.services.WITNESS; + this.ts = bcoin.now(); + + return this; +}; + +/** + * Instantiate a network address + * from a hostname (i.e. 127.0.0.1:8333). + * @param {String} hostname + * @param {(Network|NetworkType)?} network + * @returns {NetworkAddress} + */ + +NetworkAddress.fromHostname = function fromHostname(hostname, network) { + return new NetworkAddress().fromHostname(hostname, network); +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} data + * @param {Boolean?} full - Include timestamp. + */ + +NetworkAddress.prototype.fromRaw = function fromRaw(data, full) { + var p = bcoin.reader(data); + var now = bcoin.now(); + + // only version >= 31402 + this.ts = full ? p.readU32() : 0; + this.services = p.readU53(); + this.host = IP.toString(p.readBytes(16)); + this.port = p.readU16BE(); + + if (this.ts <= 100000000 || this.ts > now + 10 * 60) + this.ts = now - 5 * 24 * 60 * 60; + + return this; +}; + +/** + * Insantiate a network address from serialized data. + * @param {Buffer} data + * @param {Boolean?} full - Include timestamp. + * @returns {NetworkAddress} + */ + +NetworkAddress.fromRaw = function fromRaw(data, full) { + return new NetworkAddress().fromRaw(data, full); +}; + +/** + * Serialize network address. + * @param {Boolean} full - Include timestamp. + * @returns {Buffer} + */ + +NetworkAddress.prototype.toRaw = function toRaw(full, writer) { + var p = bcoin.writer(writer); + + if (full) + p.writeU32(this.ts); + + p.writeU64(this.services); + p.writeBytes(IP.toBuffer(this.host)); + p.writeU16BE(this.port); + + if (!writer) + p = p.render(); + + return p; +}; + +/* + * Expose + */ + +exports.VersionPacket = VersionPacket; +exports.InvItem = InvItem; +exports.GetBlocksPacket = GetBlocksPacket; +exports.AlertPacket = AlertPacket; +exports.RejectPacket = RejectPacket; +exports.NetworkAddress = NetworkAddress; diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index 8558d51d..eed1eca2 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -8,7 +8,6 @@ 'use strict'; var bcoin = require('../env'); -var bn = require('bn.js'); var EventEmitter = require('events').EventEmitter; var utils = require('../utils'); var assert = utils.assert; @@ -101,8 +100,8 @@ Parser.prototype.parse = function parse(chunk) { return this._error('Packet too large: %dmb.', utils.mb(chunk.length)); } - if (this.packet === null) { - this.packet = this.parseHeader(chunk) || {}; + if (!this.packet) { + this.packet = this.parseHeader(chunk); return; } @@ -138,7 +137,7 @@ Parser.prototype.parse = function parse(chunk) { */ Parser.prototype.parseHeader = function parseHeader(h) { - var i, magic, cmd; + var i, magic, cmd, chk; magic = h.readUInt32LE(0, true); @@ -160,11 +159,9 @@ Parser.prototype.parseHeader = function parseHeader(h) { return this._error('Packet length too large: %dmb', utils.mb(this.waiting)); } - return { - cmd: cmd, - length: this.waiting, - checksum: h.readUInt32LE(20, true) - }; + chk = h.readUInt32LE(20, true); + + return new Packet(cmd, this.waiting, chk); }; /** @@ -457,56 +454,7 @@ Parser.parsePong = function parsePong(p) { */ Parser.parseVersion = function parseVersion(p) { - var version, services, ts, recv, from, nonce, agent, height, relay; - - p = new BufferReader(p); - - version = p.read32(); - services = p.readU53(); - ts = p.read53(); - recv = bcoin.networkaddress.fromRaw(p, false); - - if (p.left() > 0) { - from = bcoin.networkaddress.fromRaw(p, false); - nonce = p.readU64(); - } else { - from = {}; - nonce = new bn(0); - } - - if (p.left() > 0) - agent = p.readVarString('ascii', 256); - else - agent = ''; - - if (p.left() > 0) - height = p.read32(); - else - height = 0; - - if (p.left() > 0) - relay = p.readU8() === 1; - else - relay = true; - - if (version === 10300) - version = 300; - - assert(version >= 0, 'Version is negative.'); - assert(ts >= 0, 'Timestamp is negative.'); - assert(height >= 0, 'Height is negative.'); - - return { - version: version, - services: services, - ts: ts, - local: recv, - remote: from, - nonce: nonce, - agent: agent, - height: height, - relay: relay - }; + return bcoin.packets.VersionPacket.fromRaw(p); }; /** @@ -539,30 +487,6 @@ Parser.parseGetData = function parseGetData(p) { return Parser.parseInv(p); }; -Parser._parseGetBlocks = function _parseGetBlocks(p) { - var version, count, locator, i, stop; - - p = new BufferReader(p); - - version = p.readU32(); - count = p.readVarint(); - locator = []; - - for (i = 0; i < count; i++) - locator.push(p.readHash('hex')); - - stop = p.readHash('hex'); - - if (stop === constants.NULL_HASH) - stop = null; - - return { - version: version, - locator: locator, - stop: stop - }; -}; - /** * Parse getblocks packet. * @param {Buffer|BufferReader} p @@ -570,9 +494,7 @@ Parser._parseGetBlocks = function _parseGetBlocks(p) { */ Parser.parseGetBlocks = function parseGetBlocks(p) { - var data = Parser._parseGetBlocks(p); - assert(data.locator.length > 0, 'getblocks requires a locator.'); - return data; + return bcoin.packets.GetBlocksPacket.fromRaw(p); }; /** @@ -582,10 +504,7 @@ Parser.parseGetBlocks = function parseGetBlocks(p) { */ Parser.parseGetHeaders = function parseGetHeaders(p) { - var data = Parser._parseGetBlocks(p); - if (data.locator.length === 0) - data.locator = null; - return data; + return bcoin.packets.GetBlocksPacket.fromRaw(p); }; /** @@ -604,12 +523,8 @@ Parser.parseInv = function parseInv(p) { assert(count <= 50000, 'Item count too high.'); - for (i = 0; i < count; i++) { - items.push({ - type: p.readU32(), - hash: p.readHash('hex') - }); - } + for (i = 0; i < count; i++) + items.push(bcoin.packets.InvItem.fromRaw(p)); return items; }; @@ -682,25 +597,7 @@ Parser.parseTX = function parseTX(p) { */ Parser.parseReject = function parseReject(p) { - var message, ccode, reason, data; - - p = new BufferReader(p); - - message = p.readVarString('ascii', 12); - ccode = p.readU8(); - reason = p.readVarString('ascii', 111); - - if (message === 'block' || message === 'tx') - data = p.readHash('hex'); - else - data = null; - - return { - message: message, - ccode: constants.rejectByVal[ccode] || ccode, - reason: reason, - data: data - }; + return bcoin.packets.RejectPacket.fromRaw(p); }; /** @@ -720,7 +617,7 @@ Parser.parseAddr = function parseAddr(p) { assert(count <= 10000, 'Too many addresses.'); for (i = 0; i < count; i++) - addrs.push(bcoin.networkaddress.fromRaw(p, true)); + addrs.push(bcoin.packets.NetworkAddress.fromRaw(p, true)); return addrs; }; @@ -742,56 +639,7 @@ Parser.parseMempool = function parseMempool(p) { */ Parser.parseAlert = function parseAlert(p) { - var version, relayUntil, expiration, id, cancel; - var cancels, count, i, minVer, maxVer, subVers; - var priority, comment, statusBar, reserved; - var payload, signature; - - p = new BufferReader(p); - - payload = p.readVarBytes(); - signature = p.readVarBytes(); - - p = new BufferReader(payload); - - version = p.read32(); - relayUntil = p.read53(); - expiration = p.read53(); - id = p.read32(); - cancel = p.read32(); - cancels = []; - count = p.readVarint(); - for (i = 0; i < count; i++) - cancels.push(p.read32()); - minVer = p.read32(); - maxVer = p.read32(); - subVers = []; - count = p.readVarint(); - for (i = 0; i < count; i++) - subVers.push(p.readVarString('ascii')); - priority = p.read32(); - comment = p.readVarString('ascii'); - statusBar = p.readVarString('ascii'); - reserved = p.readVarString('ascii'); - - return { - hash: utils.hash256(payload).toString('hex'), - version: version, - relayUntil: relayUntil, - expiration: expiration, - id: id, - cancel: cancel, - cancels: cancels, - minVer: minVer, - maxVer: maxVer, - subVers: subVers, - priority: priority, - comment: comment, - statusBar: statusBar, - reserved: reserved, - payload: payload, - signature: signature - }; + return bcoin.packets.AlertPacket.fromRaw(p); }; /** @@ -807,6 +655,19 @@ Parser.parseFeeFilter = function parseFeeFilter(p) { }; }; +/** + * Packet + * @constructor + * @private + */ + +function Packet(cmd, size, checksum) { + this.cmd = cmd; + this.size = size; + this.checksum = checksum; + this.payload = null; +} + /* * Expose */ diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index cf874600..0b2cd90b 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -14,6 +14,7 @@ var constants = bcoin.protocol.constants; var Script = bcoin.script; var Stack = bcoin.stack; var BufferWriter = require('./writer'); +var InvItem = bcoin.packets.InvItem; /** * A static transaction object. @@ -1802,10 +1803,7 @@ TX.prototype.__defineGetter__('wtxid', function() { */ TX.prototype.toInv = function toInv() { - return { - type: constants.inv.TX, - hash: this.hash('hex') - }; + return new InvItem(constants.inv.TX, this.hash('hex')); }; /** diff --git a/lib/bcoin/types.js b/lib/bcoin/types.js index aa157b32..3bdc0827 100644 --- a/lib/bcoin/types.js +++ b/lib/bcoin/types.js @@ -13,13 +13,6 @@ * @global */ -/** - * @typedef {Object} InvItem - * @property {Number|String} type - Inv type. See {@link constants.inv}. - * @property {Hash|Buffer} hash - * @global - */ - /** * One of {@link module:constants.inv}. * @typedef {Number|String} InvType @@ -41,29 +34,15 @@ * @global */ -/** - * @typedef {Object} ParsedAddress - * @property {Number?} version - Witness program version (-1 if not present). - * @property {AddressType} type - * @property {Buffer} hash - * @global - */ - /** * A bitfield containing locktime flags. * @typedef {Number} LockFlags * @global */ -/** - * A map of addresses ({@link Base58Address} -> value). - * @typedef {Object} AddressMap - * @global - */ - /** * A map of address hashes ({@link Hash} -> value). - * @typedef {Object} AddressHashMap + * @typedef {Object} AddressMap * @global */ @@ -96,8 +75,8 @@ */ /** - * Hex-string hash. - * @typedef {String} Hash + * Buffer or hex-string hash. + * @typedef {Buffer|String} Hash * @global */ @@ -246,38 +225,6 @@ * @global */ -/** - * @typedef {Object} NakedNetworkAddress - * @property {Number?} ts - Timestamp. - * @property {Number?} services - Service bits. - * @property {String?} host - IP address (IPv6 or IPv4). - * @property {Number?} port - Port. - * @global - */ - -/** - * @typedef {Object} VersionPacket - * @property {Number} version - Protocol version. - * @property {Number} services - Service bits. - * @property {Number} ts - Timestamp of discovery. - * @property {NakedNetworkAddress} local - Our address. - * @property {NakedNetworkAddress} remote - Their address. - * @property {BN} nonce - * @property {String} agent - User agent string. - * @property {Number} height - Chain height. - * @property {Boolean} relay - Whether transactions - * should be relayed immediately. - * @global - */ - -/** - * @typedef {Object} GetBlocksPacket - * @property {Number} version - Protocol version. - * @property {Hash[]} locator - Chain locator. - * @property {Hash} stop - Hash to stop at. - * @global - */ - /** * @typedef {Object} NakedBlock * @property {Number} version - Transaction version. Note that BCoin reads @@ -345,38 +292,7 @@ /** * @typedef {Object} NakedWitness - * @param {Buffer[]} items - Stack items. - * @global - */ - -/** - * @typedef {Object} RejectPacket - * @param {(Number|String)?} ccode - Code - * (see {@link constants.reject}). - * @param {String?} msg - Message. - * @param {String?} reason - Reason. - * @param {(Hash|Buffer)?} data - Transaction or block hash. - * @global - */ - -/** - * @typedef {Object} AlertPacket - * @property {Number} version - * @property {Number} relayUntil - * @property {Number} expiration - * @property {Number} id - * @property {Number} cancel - * @property {Number[]} cancels - * @property {Number} minVer - * @property {Number} maxVer - * @property {String[]} subVers - * @property {Number} priority - * @property {String} comment - * @property {String} statusBar - * @property {String?} reserved - * @property {Buffer?} payload - Payload. - * @property {Buffer?} signature - Payload signature. - * @property {Buffer?} key - Private key to sign with. + * @property {Buffer[]} items - Stack items. * @global */ diff --git a/test/protocol-test.js b/test/protocol-test.js index c7e5666f..da6a11cb 100644 --- a/test/protocol-test.js +++ b/test/protocol-test.js @@ -31,7 +31,7 @@ describe('Protocol', function() { }); } - var v1 = { + var v1 = bcoin.packets.VersionPacket.fromOptions({ version: constants.VERSION, services: constants.LOCAL_SERVICES, ts: bcoin.now(), @@ -41,7 +41,7 @@ describe('Protocol', function() { agent: constants.USER_AGENT, height: 0, relay: false - }; + }); packetTest('version', v1, function(payload) { assert.equal(payload.version, constants.VERSION); @@ -50,7 +50,7 @@ describe('Protocol', function() { assert.equal(payload.relay, false); }); - var v2 = { + var v2 = bcoin.packets.VersionPacket.fromOptions({ version: constants.VERSION, services: constants.LOCAL_SERVICES, ts: bcoin.now(), @@ -60,7 +60,7 @@ describe('Protocol', function() { agent: constants.USER_AGENT, height: 10, relay: true - }; + }); packetTest('version', v2, function(payload) { assert.equal(payload.version, constants.VERSION); @@ -205,14 +205,13 @@ describe('Protocol', function() { var p = new bcoin.reader(alertData); p.start(); while (p.left()) { - var details = bcoin.protocol.parser.parseAlert(p); - var hash = utils.hash256(details.payload); - var signature = details.signature; - assert(bcoin.ec.verify(hash, signature, network.alertKey)); - delete details.payload; - var data = bcoin.protocol.framer.alert(details); - details = bcoin.protocol.parser.parseAlert(data); - assert(bcoin.ec.verify(hash, signature, network.alertKey)); + var alert = bcoin.protocol.parser.parseAlert(p); + assert(alert.verify(network.alertKey)); + alert._payload = null; + alert._hash = null; + var data = bcoin.protocol.framer.alert(alert); + alert = bcoin.protocol.parser.parseAlert(data); + assert(alert.verify(network.alertKey)); } p.end(); });