From a486bd3a184d1cc063196e8cfa9a1b597daa307e Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 14 Jan 2017 14:41:55 -0800 Subject: [PATCH] refactor: options. --- lib/blockchain/chain.js | 160 ++++++++++- lib/blockchain/chaindb.js | 120 ++++----- lib/http/base.js | 150 +++++++++-- lib/http/rpc.js | 36 +-- lib/http/server.js | 184 ++++++++++--- lib/mempool/mempool.js | 294 +++++++++++--------- lib/mining/mine.js | 2 +- lib/mining/miner.js | 215 +++++++++------ lib/mining/minerblock.js | 4 +- lib/net/bip150.js | 21 +- lib/net/hostlist.js | 77 ++++-- lib/net/peer.js | 42 +-- lib/net/pool.js | 551 ++++++++++++++++++++++---------------- lib/net/tcp-browser.js | 19 +- lib/node/fullnode.js | 21 +- lib/node/logger.js | 158 +++++++---- lib/node/node.js | 20 ++ lib/node/nodeclient.js | 3 +- lib/node/spvnode.js | 15 +- lib/utils/lru.js | 92 ++++--- lib/utils/util.js | 11 - lib/wallet/walletdb.js | 212 +++++++++++---- lib/workers/workerpool.js | 73 +++-- test/http-test.js | 2 +- 24 files changed, 1640 insertions(+), 842 deletions(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 96666f8e..45f56d2b 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -34,13 +34,11 @@ var VerifyResult = errors.VerifyResult; * @param {String?} options.location - Database file location. * @param {String?} options.db - Database backend (`"leveldb"` by default). * @param {Number?} options.orphanLimit - * @param {Number?} options.pendingLimit * @param {Boolean?} options.spv * @property {Boolean} loaded * @property {ChainDB} db - Note that Chain `options` will be passed * to the instantiated ChainDB. * @property {Number} total - * @property {Number} orphanLimit * @property {Lock} locker * @property {Object} invalid * @property {ChainEntry?} tip @@ -69,22 +67,20 @@ function Chain(options) { AsyncObject.call(this); - if (!options) - options = {}; + this.options = new ChainOptions(options); - this.options = options; - - this.network = Network.get(options.network); - this.logger = options.logger || Logger.global; + this.network = this.options.network; + this.logger = this.options.logger; this.db = new ChainDB(this); - this.total = 0; - this.orphanLimit = options.orphanLimit || (20 << 20); + this.locker = new Lock(true); this.invalid = new LRU(100); + this.state = new DeploymentState(); + this.tip = null; this.height = -1; this.synced = false; - this.state = new DeploymentState(); + this.total = 0; this.startTime = util.hrtime(); this.orphanMap = {}; @@ -1492,7 +1488,7 @@ Chain.prototype.purgeOrphans = function purgeOrphans() { Chain.prototype.pruneOrphans = function pruneOrphans() { var i, hashes, hash, orphan, height, best, last; - if (this.orphanSize <= this.orphanLimit) + if (this.orphanSize <= this.options.orphanLimit) return false; hashes = Object.keys(this.orphanPrev); @@ -2264,6 +2260,146 @@ Chain.prototype.verifyLocks = co(function* verifyLocks(prev, tx, view, flags) { return true; }); +/** + * ChainOptions + * @constructor + * @param {Object} options + */ + +function ChainOptions(options) { + if (!(this instanceof ChainOptions)) + return new ChainOptions(options); + + this.network = Network.primary; + this.logger = Logger.global; + + this.location = null; + this.db = 'memory'; + this.maxFiles = 64; + this.cacheSize = 32 << 20; + this.compression = true; + this.bufferKeys = !util.isBrowser; + + this.spv = false; + this.witness = false; + this.prune = false; + this.indexTX = false; + this.indexAddress = false; + this.forceWitness = false; + + this.coinCache = 0; + this.entryCache = (2016 + 1) * 2 + 100; + this.orphanLimit = 20 << 20; + this.useCheckpoints = false; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {ChainOptions} + */ + +ChainOptions.prototype.fromOptions = function fromOptions(options) { + if (options.network != null) + this.network = Network.get(options.network); + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.location != null) { + assert(typeof options.location === 'string'); + this.location = options.location; + } + + if (options.db != null) { + assert(typeof options.db === 'string'); + this.db = options.db; + } + + if (options.maxFiles != null) { + assert(util.isNumber(options.maxFiles)); + this.maxFiles = options.maxFiles; + } + + if (options.cacheSize != null) { + assert(util.isNumber(options.cacheSize)); + this.cacheSize = options.cacheSize; + } + + if (options.compression != null) { + assert(typeof options.compression === 'boolean'); + this.compression = options.compression; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + + if (options.prune != null) { + assert(typeof options.prune === 'boolean'); + this.prune = options.prune; + } + + if (options.indexTX != null) { + assert(typeof options.indexTX === 'boolean'); + this.indexTX = options.indexTX; + } + + if (options.indexAddress != null) { + assert(typeof options.indexAddress === 'boolean'); + this.indexAddress = options.indexAddress; + } + + if (options.forceWitness != null) { + assert(typeof options.forceWitness === 'boolean'); + this.forceWitness = options.forceWitness; + } + + if (options.coinCache != null) { + assert(util.isNumber(options.coinCache)); + this.coinCache = options.coinCache; + } + + if (options.entryCache != null) { + assert(util.isNumber(options.entryCache)); + this.entryCache = options.entryCache; + } + + if (options.orphanLimit != null) { + assert(util.isNumber(options.orphanLimit)); + this.orphanLimit = options.orphanLimit; + } + + if (options.useCheckpoints != null) { + assert(typeof options.useCheckpoints === 'boolean'); + this.useCheckpoints = options.useCheckpoints; + } + + return this; +}; + +/** + * Instantiate chain options from object. + * @param {Object} options + * @returns {ChainOptions} + */ + +ChainOptions.fromOptions = function fromOptions(options) { + return new ChainOptions().fromOptions(options); +}; + /** * Represents the deployment state of the chain. * @constructor diff --git a/lib/blockchain/chaindb.js b/lib/blockchain/chaindb.js index bde770c2..bcaee47b 100644 --- a/lib/blockchain/chaindb.js +++ b/lib/blockchain/chaindb.js @@ -35,7 +35,7 @@ var DUMMY = new Buffer([0]); * The database backend for the {@link Chain} object. * @exports ChainDB * @constructor - * @param {Object} options + * @param {Chain} chain * @param {Boolean?} options.prune - Whether to prune the chain. * @param {Boolean?} options.spv - SPV-mode, will not save block * data, only entries. @@ -54,41 +54,19 @@ function ChainDB(chain) { AsyncObject.call(this); this.chain = chain; - this.logger = chain.logger; - this.network = chain.network; - this.options = new ChainOptions(chain.options); + this.options = chain.options; + this.network = this.options.network; + this.logger = this.options.logger; - this.db = LDB({ - location: chain.options.location, - db: chain.options.db, - maxFiles: chain.options.maxFiles, - cacheSize: chain.options.cacheSize || (32 << 20), - compression: true, - bufferKeys: !util.isBrowser - }); - - this.stateCache = new StateCache(chain.network); + this.db = LDB(this.options); + this.stateCache = new StateCache(this.network); this.state = new ChainState(); this.pending = null; this.current = null; - // We want at least 1 retarget interval cached - // for retargetting, but we need at least two - // cached for optimal versionbits state checks. - // We add a padding of 100 for forked chains, - // reorgs, chain locator creation and the bip34 - // check. - this.cacheWindow = (this.network.pow.retargetInterval + 1) * 2 + 100; - - // We want to keep the last 5 blocks of unspents in memory. - this.coinWindow = chain.options.coinCache || 0; - - this.coinCache = new LRU.Nil(); - this.cacheHash = new LRU(this.cacheWindow); - this.cacheHeight = new LRU(this.cacheWindow); - - if (this.coinWindow) - this.coinCache = new LRU(this.coinWindow, getSize); + this.coinCache = new LRU(this.options.coinCache, getSize); + this.cacheHash = new LRU(this.options.entryCache); + this.cacheHeight = new LRU(this.options.entryCache); } util.inherits(ChainDB, AsyncObject); @@ -118,7 +96,7 @@ ChainDB.prototype._open = co(function* open() { if (state) { // Verify options have not changed. - yield this.verifyOptions(); + yield this.verifyFlags(); // Verify deployment params have not changed. yield this.verifyDeployments(); @@ -133,7 +111,7 @@ ChainDB.prototype._open = co(function* open() { } else { // Database is fresh. // Write initial state. - yield this.saveOptions(); + yield this.saveFlags(); yield this.saveDeployments(); yield this.saveGenesis(); @@ -527,17 +505,17 @@ ChainDB.prototype.saveGenesis = co(function* saveGenesis() { }); /** - * Retrieve the tip entry from the tip record. - * @returns {Promise} - Returns {@link ChainOptions}. + * Retrieve the database flags. + * @returns {Promise} - Returns {@link ChainFlags}. */ -ChainDB.prototype.getOptions = co(function* getOptions() { +ChainDB.prototype.getFlags = co(function* getFlags() { var data = yield this.db.get(layout.O); if (!data) return; - return ChainOptions.fromRaw(data); + return ChainFlags.fromRaw(data); }); /** @@ -545,15 +523,15 @@ ChainDB.prototype.getOptions = co(function* getOptions() { * @returns {Promise} */ -ChainDB.prototype.verifyOptions = co(function* verifyOptions() { - var options = yield this.getOptions(); +ChainDB.prototype.verifyFlags = co(function* verifyFlags() { + var flags = yield this.getFlags(); - assert(options, 'No options found.'); + assert(flags, 'No flags found.'); - this.options.verify(options); + flags.verify(this.options); if (this.options.forceWitness) - yield this.saveOptions(); + yield this.saveFlags(); }); /** @@ -1801,8 +1779,9 @@ ChainDB.prototype.pruneBlock = co(function* pruneBlock(entry) { * @returns {Promise} */ -ChainDB.prototype.saveOptions = function saveOptions() { - return this.db.put(layout.O, this.options.toRaw()); +ChainDB.prototype.saveFlags = function saveFlags() { + var flags = ChainFlags.fromOptions(this.options); + return this.db.put(layout.O, flags.toRaw()); }; /** @@ -1914,9 +1893,9 @@ ChainDB.prototype.unindexTX = function unindexTX(tx, view) { * @constructor */ -function ChainOptions(options) { - if (!(this instanceof ChainOptions)) - return new ChainOptions(options); +function ChainFlags(options) { + if (!(this instanceof ChainFlags)) + return new ChainFlags(options); this.network = Network.primary; this.spv = false; @@ -1925,13 +1904,11 @@ function ChainOptions(options) { this.indexTX = false; this.indexAddress = false; - this.forceWitness = false; - if (options) this.fromOptions(options); } -ChainOptions.prototype.fromOptions = function fromOptions(options) { +ChainFlags.prototype.fromOptions = function fromOptions(options) { this.network = Network.get(options.network); if (options.spv != null) { @@ -1959,56 +1936,51 @@ ChainOptions.prototype.fromOptions = function fromOptions(options) { this.indexAddress = options.indexAddress; } - if (options.forceWitness != null) { - assert(typeof options.forceWitness === 'boolean'); - this.forceWitness = options.forceWitness; - } - return this; }; -ChainOptions.fromOptions = function fromOptions(data) { - return new ChainOptions().fromOptions(data); +ChainFlags.fromOptions = function fromOptions(data) { + return new ChainFlags().fromOptions(data); }; -ChainOptions.prototype.verify = function verify(options) { - if (this.network !== options.network) +ChainFlags.prototype.verify = function verify(options) { + if (options.network !== this.network) throw new Error('Network mismatch for chain.'); - if (this.spv && !options.spv) + if (options.spv && !this.spv) throw new Error('Cannot retroactively enable SPV.'); - if (!this.spv && options.spv) + if (!options.spv && this.spv) throw new Error('Cannot retroactively disable SPV.'); - if (!this.forceWitness) { - if (this.witness && !options.witness) + if (!options.forceWitness) { + if (options.witness && !this.witness) throw new Error('Cannot retroactively enable witness.'); - if (!this.witness && options.witness) + if (!options.witness && this.witness) throw new Error('Cannot retroactively disable witness.'); } - if (this.prune && !options.prune) + if (options.prune && !this.prune) throw new Error('Cannot retroactively prune.'); - if (!this.prune && options.prune) + if (!options.prune && this.prune) throw new Error('Cannot retroactively unprune.'); - if (this.indexTX && !options.indexTX) + if (options.indexTX && !this.indexTX) throw new Error('Cannot retroactively enable TX indexing.'); - if (!this.indexTX && options.indexTX) + if (!options.indexTX && this.indexTX) throw new Error('Cannot retroactively disable TX indexing.'); - if (this.indexAddress && !options.indexAddress) + if (options.indexAddress && !this.indexAddress) throw new Error('Cannot retroactively enable address indexing.'); - if (!this.indexAddress && options.indexAddress) + if (!options.indexAddress && this.indexAddress) throw new Error('Cannot retroactively disable address indexing.'); }; -ChainOptions.prototype.toRaw = function toRaw() { +ChainFlags.prototype.toRaw = function toRaw() { var bw = new StaticWriter(12); var flags = 0; @@ -2034,7 +2006,7 @@ ChainOptions.prototype.toRaw = function toRaw() { return bw.render(); }; -ChainOptions.prototype.fromRaw = function fromRaw(data) { +ChainFlags.prototype.fromRaw = function fromRaw(data) { var br = new BufferReader(data); var flags; @@ -2051,8 +2023,8 @@ ChainOptions.prototype.fromRaw = function fromRaw(data) { return this; }; -ChainOptions.fromRaw = function fromRaw(data) { - return new ChainOptions().fromRaw(data); +ChainFlags.fromRaw = function fromRaw(data) { + return new ChainFlags().fromRaw(data); }; /** diff --git a/lib/http/base.js b/lib/http/base.js index 26d2a0a8..275a8d40 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -25,24 +25,15 @@ function HTTPBase(options) { if (!(this instanceof HTTPBase)) return new HTTPBase(options); - if (!options) - options = {}; - AsyncObject.call(this); - this.options = options; - this.io = null; + this.options = new HTTPBaseOptions(options); + this.server = null; + this.io = null; this.routes = new Routes(); this.stack = []; - this.keyLimit = 100; - this.bodyLimit = 20 << 20; - - this.server = options.key - ? require('https').createServer(options) - : require('http').createServer(); - this._init(); } @@ -55,6 +46,10 @@ util.inherits(HTTPBase, AsyncObject); HTTPBase.prototype._init = function _init() { var self = this; + var backend = this.options.getBackend(); + var options = this.options.toHTTP(); + + this.server = backend.createServer(options); this._initRouter(); this._initIO(); @@ -116,7 +111,7 @@ HTTPBase.prototype._initRouter = function _initRouter() { HTTPBase.prototype.handleRequest = co(function* handleRequest(req, res) { var i, routes, route, params; - initRequest(req, res, this.keyLimit); + initRequest(req, res, this.options.keyLimit); this.emit('request', req, res); @@ -170,7 +165,7 @@ HTTPBase.prototype.parseBody = co(function* parseBody(req) { body = JSON.parse(data); break; case 'form': - body = parsePairs(data, this.keyLimit); + body = parsePairs(data, this.options.keyLimit); break; default: break; @@ -254,7 +249,7 @@ HTTPBase.prototype._readBody = function _readBody(req, enc, resolve, reject) { total += data.length; hasData = true; - if (total > self.bodyLimit) { + if (total > self.options.bodyLimit) { reject(new Error('Request body overflow.')); return; } @@ -322,7 +317,6 @@ HTTPBase.prototype._initIO = function _initIO() { */ HTTPBase.prototype._open = function open() { - assert(typeof this.options.port === 'number', 'Port required.'); return this.listen(this.options.port, this.options.host); }; @@ -445,6 +439,130 @@ HTTPBase.prototype.listen = function listen(port, host) { }); }; +/** + * HTTP Base Options + * @constructor + * @param {Object} options + */ + +function HTTPBaseOptions(options) { + if (!(this instanceof HTTPBaseOptions)) + return new HTTPBaseOptions(options); + + this.host = '127.0.0.1'; + this.port = 8080; + this.sockets = false; + + this.ssl = false; + this.key = null; + this.cert = null; + this.ca = null; + + this.keyLimit = 100; + this.bodyLimit = 20 << 20; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {HTTPBaseOptions} + */ + +HTTPBaseOptions.prototype.fromOptions = function fromOptions(options) { + assert(options); + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(typeof options.port === 'number', 'Port must be a number.'); + assert(options.port > 0 && options.port <= 0xffff); + this.port = options.port; + } + + if (options.sockets != null) { + assert(typeof options.sockets === 'boolean'); + this.sockets = options.sockets; + } + + if (options.key != null) { + assert(typeof options.key === 'string' || Buffer.isBuffer(options.key)); + this.key = options.key; + this.ssl = true; + } + + if (options.cert != null) { + assert(typeof options.cert === 'string' || Buffer.isBuffer(options.cert)); + this.cert = options.cert; + } + + if (options.ca != null) { + assert(Array.isArray(options.ca)); + this.ca = options.ca; + } + + if (options.keyLimit != null) { + assert(typeof options.keyLimit === 'number'); + this.keyLimit = options.keyLimit; + } + + if (options.bodyLimit != null) { + assert(typeof options.bodyLimit === 'number'); + this.bodyLimit = options.bodyLimit; + } + + if (options.ssl != null) { + assert(typeof options.ssl === 'boolean'); + assert(this.key, 'SSL specified with no provided key.'); + this.ssl = options.ssl; + } + + return this; +}; + +/** + * Instantiate http server options from object. + * @param {Object} options + * @returns {HTTPBaseOptions} + */ + +HTTPBaseOptions.fromOptions = function fromOptions(options) { + return new HTTPBaseOptions().fromOptions(options); +}; + +/** + * Get HTTP server backend. + * @private + * @returns {Object} + */ + +HTTPBaseOptions.prototype.getBackend = function getBackend() { + return this.ssl ? require('https') : require('http'); +}; + +/** + * Get HTTP server options. + * @private + * @returns {Object} + */ + +HTTPBaseOptions.prototype.toHTTP = function toHTTP() { + if (!this.ssl) + return undefined; + + return { + key: this.key, + cert: this.cert, + ca: this.ca + }; +}; + /** * Route * @constructor diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 194dfddc..b21471cf 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -46,6 +46,8 @@ function RPC(node) { EventEmitter.call(this); + assert(node, 'RPC requires a Node.'); + this.node = node; this.network = node.network; this.chain = node.chain; @@ -682,10 +684,10 @@ RPC.prototype.getblockchaininfo = co(function* getblockchaininfo(args) { mediantime: yield this.chain.tip.getMedianTimeAsync(), verificationprogress: this.chain.getProgress(), chainwork: this.chain.tip.chainwork.toString('hex', 64), - pruned: this.chain.db.options.prune, + pruned: this.chain.options.prune, softforks: this._getSoftforks(), bip9_softforks: yield this._getBIP9Softforks(), - pruneheight: this.chain.db.options.prune + pruneheight: this.chain.options.prune ? Math.max(0, this.chain.height - this.chain.db.keepBlocks) : null }; @@ -754,10 +756,10 @@ RPC.prototype.getblock = co(function* getblock(args) { block = yield this.chain.db.getBlock(entry.hash); if (!block) { - if (this.chain.db.options.spv) + if (this.chain.options.spv) throw new RPCError('Block not available (spv mode)'); - if (this.chain.db.prune) + if (this.chain.options.prune) throw new RPCError('Block not available (pruned data)'); throw new RPCError('Can\'t read block from disk'); @@ -1000,8 +1002,8 @@ RPC.prototype.getmempoolinfo = co(function* getmempoolinfo(args) { size: this.mempool.totalTX, bytes: this.mempool.getSize(), usage: this.mempool.getSize(), - maxmempool: this.mempool.maxSize, - mempoolminfee: Amount.btc(this.mempool.minRelay, true) + maxmempool: this.mempool.options.maxSize, + mempoolminfee: Amount.btc(this.mempool.options.minRelay, true) }; }); @@ -1172,10 +1174,10 @@ RPC.prototype.gettxout = co(function* gettxout(args) { if (args.help || args.length < 2 || args.length > 3) throw new RPCError('gettxout "txid" n ( includemempool )'); - if (this.chain.db.options.spv) + if (this.chain.options.spv) throw new RPCError('Cannot get coins in SPV mode.'); - if (this.chain.db.options.prune) + if (this.chain.options.prune) throw new RPCError('Cannot get coins when pruned.'); hash = toHash(args[0]); @@ -1214,10 +1216,10 @@ RPC.prototype.gettxoutproof = co(function* gettxoutproof(args) { if (args.help || (args.length !== 1 && args.length !== 2)) throw new RPCError('gettxoutproof ["txid",...] ( blockhash )'); - if (this.chain.db.options.spv) + if (this.chain.options.spv) throw new RPCError('Cannot get coins in SPV mode.'); - if (this.chain.db.options.prune) + if (this.chain.options.prune) throw new RPCError('Cannot get coins when pruned.'); txids = toArray(args[0]); @@ -1302,7 +1304,7 @@ RPC.prototype.gettxoutsetinfo = co(function* gettxoutsetinfo(args) { if (args.help || args.length !== 0) throw new RPCError('gettxoutsetinfo'); - if (this.chain.db.options.spv) + if (this.chain.options.spv) throw new RPCError('Chainstate not available (SPV mode).'); return { @@ -1320,10 +1322,10 @@ RPC.prototype.verifychain = co(function* verifychain(args) { if (args.help || args.length > 2) throw new RPCError('verifychain ( checklevel numblocks )'); - if (this.chain.db.options.spv) + if (this.chain.options.spv) throw new RPCError('Cannot verify chain in SPV mode.'); - if (this.chain.db.options.prune) + if (this.chain.options.prune) throw new RPCError('Cannot verify chain when pruned.'); return null; @@ -3199,7 +3201,7 @@ RPC.prototype.importprivkey = co(function* importprivkey(args) { if (args.length > 2) rescan = toBool(args[2]); - if (rescan && this.chain.db.options.prune) + if (rescan && this.chain.options.prune) throw new RPCError('Cannot rescan when pruned.'); key = KeyRing.fromSecret(secret); @@ -3229,7 +3231,7 @@ RPC.prototype.importwallet = co(function* importwallet(args) { if (args.length > 1) rescan = toBool(args[1]); - if (rescan && this.chain.db.options.prune) + if (rescan && this.chain.options.prune) throw new RPCError('Cannot rescan when pruned.'); data = yield readFile(file, 'utf8'); @@ -3289,7 +3291,7 @@ RPC.prototype.importaddress = co(function* importaddress(args) { if (args.length > 3) p2sh = toBool(args[3]); - if (rescan && this.chain.db.options.prune) + if (rescan && this.chain.options.prune) throw new RPCError('Cannot rescan when pruned.'); addr = Address.fromBase58(addr); @@ -3320,7 +3322,7 @@ RPC.prototype.importpubkey = co(function* importpubkey(args) { if (args.length > 2) rescan = toBool(args[2]); - if (rescan && this.chain.db.options.prune) + if (rescan && this.chain.options.prune) throw new RPCError('Cannot rescan when pruned.'); pubkey = new Buffer(pubkey, 'hex'); diff --git a/lib/http/server.js b/lib/http/server.js index 933f093f..a024deed 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -25,6 +25,7 @@ var Outpoint = require('../primitives/outpoint'); var HD = require('../hd/hd'); var Script = require('../script/script'); var crypto = require('../crypto/crypto'); +var Network = require('../protocol/network'); var pkg = require('../../package.json'); var cob = co.cob; var RPC; @@ -43,17 +44,14 @@ function HTTPServer(options) { if (!(this instanceof HTTPServer)) return new HTTPServer(options); - if (!options) - options = {}; - EventEmitter.call(this); - this.options = options; - this.node = options.node; + this.options = new HTTPOptions(options); - assert(this.node, 'HTTP requires a Node.'); + this.network = this.options.network; + this.logger = this.options.logger; + this.node = this.options.node; - this.network = this.node.network; this.chain = this.node.chain; this.mempool = this.node.mempool; this.pool = this.node.pool; @@ -61,33 +59,10 @@ function HTTPServer(options) { this.miner = this.node.miner; this.wallet = this.node.wallet; this.walletdb = this.node.walletdb; - this.logger = options.logger || this.node.logger; - this.loaded = false; - this.apiKey = options.apiKey; - this.apiHash = null; - this.serviceKey = options.serviceKey; - this.serviceHash = null; + + this.server = new HTTPBase(this.options); this.rpc = null; - if (!this.apiKey) - this.apiKey = base58.encode(crypto.randomBytes(20)); - - if (!this.serviceKey) - this.serviceKey = this.apiKey; - - assert(typeof this.apiKey === 'string', 'API key must be a string.'); - assert(this.apiKey.length <= 200, 'API key must be under 200 bytes.'); - - assert(typeof this.serviceKey === 'string', 'API key must be a string.'); - assert(this.serviceKey.length <= 200, 'API key must be under 200 bytes.'); - - this.apiHash = hash256(this.apiKey); - this.serviceHash = hash256(this.serviceKey); - - options.sockets = true; - - this.server = new HTTPBase(options); - this._init(); } @@ -129,7 +104,7 @@ HTTPServer.prototype._init = function _init() { } res.setHeader('X-Bcoin-Version', pkg.version); - res.setHeader('X-Bcoin-Agent', this.pool.userAgent); + res.setHeader('X-Bcoin-Agent', this.pool.options.agent); res.setHeader('X-Bcoin-Network', this.network.type); res.setHeader('X-Bcoin-Height', this.chain.height + ''); res.setHeader('X-Bcoin-Tip', this.chain.tip.rhash()); @@ -167,7 +142,7 @@ HTTPServer.prototype._init = function _init() { hash = hash256(req.password); // Regular API key gives access to everything. - if (crypto.ccmp(hash, this.apiHash)) { + if (crypto.ccmp(hash, this.options.apiHash)) { req.admin = true; return; } @@ -175,7 +150,7 @@ HTTPServer.prototype._init = function _init() { // If they're hitting the wallet services, // they can use the less powerful API key. if (isWalletPath(req)) { - if (crypto.ccmp(hash, this.serviceHash)) + if (crypto.ccmp(hash, this.options.serviceHash)) return; } @@ -643,7 +618,7 @@ HTTPServer.prototype._init = function _init() { pool: { host: this.pool.address.host, port: this.pool.address.port, - agent: this.pool.userAgent, + agent: this.pool.options.agent, services: this.pool.address.services.toString(2), outbound: this.pool.peers.outbound, inbound: this.pool.peers.inbound @@ -653,7 +628,7 @@ HTTPServer.prototype._init = function _init() { size: size }, time: { - uptime: Math.floor(util.uptime()), + uptime: this.node.uptime(), system: util.now(), adjusted: this.network.now(), offset: this.network.time.offset @@ -1329,8 +1304,8 @@ HTTPServer.prototype._initIO = function _initIO() { if (!self.options.noAuth) { hash = hash256(key); - api = crypto.ccmp(hash, self.apiHash); - service = crypto.ccmp(hash, self.serviceHash); + api = crypto.ccmp(hash, self.options.apiHash); + service = crypto.ccmp(hash, self.options.serviceHash); if (!api && !service) throw { error: 'Bad key.' }; @@ -1347,7 +1322,7 @@ HTTPServer.prototype._initIO = function _initIO() { socket.emit('version', { version: pkg.version, - agent: self.pool.userAgent, + agent: self.pool.options.agent, network: self.network.type }); }); @@ -1616,11 +1591,11 @@ HTTPServer.prototype.open = co(function* open() { return; } - this.logger.info('HTTP API key: %s', this.apiKey); - this.logger.info('HTTP Service API key: %s', this.serviceKey); + this.logger.info('HTTP API key: %s', this.options.apiKey); + this.logger.info('HTTP Service API key: %s', this.options.serviceKey); - this.apiKey = null; - this.serviceKey = null; + this.options.apiKey = null; + this.options.serviceKey = null; }); /** @@ -1697,6 +1672,127 @@ HTTPServer.prototype.listen = function listen(port, host) { return this.server.listen(port, host); }; +/** + * HTTPOptions + * @constructor + * @param {Object} options + */ + +function HTTPOptions(options) { + if (!(this instanceof HTTPOptions)) + return new HTTPOptions(options); + + this.network = Network.primary; + this.logger = null; + this.node = null; + this.apiKey = base58.encode(crypto.randomBytes(20)); + this.apiHash = hash256(this.apiKey); + this.serviceKey = this.apiKey; + this.serviceHash = this.apiHash; + this.noAuth = false; + this.walletAuth = false; + this.sockets = true; + this.host = '127.0.0.1'; + this.port = this.network.rpcPort; + this.key = null; + this.cert = null; + this.ca = null; + + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {HTTPOptions} + */ + +HTTPOptions.prototype.fromOptions = function fromOptions(options) { + assert(options); + assert(options.node && typeof options.node === 'object', + 'HTTP Server requires a Node.'); + + this.node = options.node; + this.network = options.node.network; + this.logger = options.node.logger; + + this.port = this.network.rpcPort; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.apiKey != null) { + assert(typeof options.apiKey === 'string', + 'API key must be a string.'); + assert(options.apiKey.length <= 200, + 'API key must be under 200 bytes.'); + this.apiKey = options.apiKey; + this.apiHash = hash256(this.apiKey); + this.serviceKey = this.apiKey; + this.serviceHash = this.apiHash; + } + + if (options.serviceKey != null) { + assert(typeof options.serviceKey === 'string', + 'API key must be a string.'); + assert(options.serviceKey.length <= 200, + 'API key must be under 200 bytes.'); + this.serviceKey = options.serviceKey; + this.serviceHash = hash256(this.serviceKey); + } + + if (options.noAuth != null) { + assert(typeof options.noAuth === 'boolean'); + this.noAuth = options.noAuth; + } + + if (options.walletAuth != null) { + assert(typeof options.walletAuth === 'boolean'); + this.walletAuth = options.walletAuth; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(typeof options.port === 'number', 'Port must be a number.'); + assert(options.port > 0 && options.port <= 0xffff); + this.port = options.port; + } + + if (options.key != null) { + assert(typeof options.key === 'string' || Buffer.isBuffer(options.key)); + this.key = options.key; + } + + if (options.cert != null) { + assert(typeof options.cert === 'string' || Buffer.isBuffer(options.cert)); + this.cert = options.cert; + } + + if (options.ca != null) { + assert(Array.isArray(options.ca)); + this.ca = options.ca; + } + + return this; +}; + +/** + * Instantiate http options from object. + * @param {Object} options + * @returns {HTTPOptions} + */ + +HTTPOptions.fromOptions = function fromOptions(options) { + return new HTTPOptions().fromOptions(options); +}; + /** * ClientSocket * @constructor diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index 7c2f17e8..38f30930 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -26,6 +26,7 @@ var TXMeta = require('../primitives/txmeta'); var MempoolEntry = require('./mempoolentry'); var CoinView = require('../coins/coinview'); var Coins = require('../coins/coins'); +var Network = require('../protocol/network'); var VerifyError = errors.VerifyError; var VerifyResult = errors.VerifyResult; @@ -66,128 +67,33 @@ function Mempool(options) { AsyncObject.call(this); - if (!options) - options = {}; + this.options = new MempoolOptions(options); - this.options = options; - this.chain = options.chain; - this.fees = options.fees; + this.network = this.options.network; + this.logger = this.options.logger; + this.chain = this.options.chain; + this.fees = this.options.fees; - assert(this.chain, 'Mempool requires a blockchain.'); - - this.network = this.chain.network; - this.logger = options.logger || this.chain.logger; this.locker = new Lock(true); this.size = 0; this.totalOrphans = 0; this.totalTX = 0; + this.freeCount = 0; + this.lastTime = 0; this.waiting = {}; this.orphans = {}; this.map = {}; this.spents = {}; - this.coinIndex = new CoinIndex(this); - this.txIndex = new TXIndex(this); - this.rejects = new Bloom.Rolling(120000, 0.000001); - this.freeCount = 0; - this.lastTime = 0; - - this.limitFree = true; - this.limitFreeRelay = 15; - this.relayPriority = true; - this.requireStandard = this.network.requireStandard; - this.rejectAbsurdFees = true; - this.prematureWitness = false; - this.paranoidChecks = false; - this.replaceByFee = false; - - this.maxSize = policy.MEMPOOL_MAX_SIZE; - this.maxOrphans = policy.MEMPOOL_MAX_ORPHANS; - this.maxAncestors = policy.MEMPOOL_MAX_ANCESTORS; - this.expiryTime = policy.MEMPOOL_EXPIRY_TIME; - this.minRelay = this.network.minRelay; - - this._initOptions(options); + this.coinIndex = new CoinIndex(this); + this.txIndex = new TXIndex(this); } util.inherits(Mempool, AsyncObject); -/** - * Initialize options. - * @param {Object} options - * @private - */ - -Mempool.prototype._initOptions = function _initOptions(options) { - if (options.limitFree != null) { - assert(typeof options.limitFree === 'boolean'); - this.limitFree = options.limitFree; - } - - if (options.limitFreeRelay != null) { - assert(util.isNumber(options.limitFreeRelay)); - this.limitFreeRelay = options.limitFreeRelay; - } - - if (options.relayPriority != null) { - assert(typeof options.relayPriority === 'boolean'); - this.relayPriority = options.relayPriority; - } - - if (options.requireStandard != null) { - assert(typeof options.requireStandard === 'boolean'); - this.requireStandard = options.requireStandard; - } - - if (options.rejectAbsurdFees != null) { - assert(typeof options.rejectAbsurdFees === 'boolean'); - this.rejectAbsurdFees = options.rejectAbsurdFees; - } - - if (options.prematureWitness != null) { - assert(typeof options.prematureWitness === 'boolean'); - this.prematureWitness = options.prematureWitness; - } - - if (options.paranoidChecks != null) { - assert(typeof options.paranoidChecks === 'boolean'); - this.paranoidChecks = options.paranoidChecks; - } - - if (options.replaceByFee != null) { - assert(typeof options.replaceByFee === 'boolean'); - this.replaceByFee = options.replaceByFee; - } - - if (options.maxSize != null) { - assert(util.isNumber(options.maxSize)); - this.maxSize = options.maxSize; - } - - if (options.maxOrphans != null) { - assert(util.isNumber(options.maxOrphans)); - this.maxOrphans = options.maxOrphans; - } - - if (options.maxAncestors != null) { - assert(util.isNumber(options.maxAncestors)); - this.maxAncestors = options.maxAncestors; - } - - if (options.expiryTime != null) { - assert(util.isNumber(options.expiryTime)); - this.expiryTime = options.expiryTime; - } - - if (options.minRelay != null) { - assert(util.isNumber(options.minRelay)); - this.minRelay = options.minRelay; - } -}; - /** * Open the chain, wait for the database to load. * @alias Mempool#open @@ -195,7 +101,7 @@ Mempool.prototype._initOptions = function _initOptions(options) { */ Mempool.prototype._open = co(function* open() { - var size = (this.maxSize / 1024).toFixed(2); + var size = (this.options.maxSize / 1024).toFixed(2); yield this.chain.open(); this.logger.info('Mempool loaded (maxsize=%dkb).', size); }); @@ -385,11 +291,11 @@ Mempool.prototype.limitSize = function limitSize(entryHash) { var trimmed = false; var i, hashes, hash, end, entry; - if (this.getSize() <= this.maxSize) + if (this.getSize() <= this.options.maxSize) return trimmed; hashes = this.getSnapshot(); - end = util.now() - this.expiryTime; + end = util.now() - this.options.expiryTime; for (i = 0; i < hashes.length; i++) { hash = hashes[i]; @@ -406,7 +312,7 @@ Mempool.prototype.limitSize = function limitSize(entryHash) { this.removeEntry(entry, true); - if (this.getSize() <= this.maxSize) + if (this.getSize() <= this.options.maxSize) return trimmed; } @@ -424,7 +330,7 @@ Mempool.prototype.limitSize = function limitSize(entryHash) { this.removeEntry(entry, true); - if (this.getSize() <= this.maxSize) + if (this.getSize() <= this.options.maxSize) return trimmed; } @@ -439,7 +345,7 @@ Mempool.prototype.limitOrphans = function limitOrphans() { var orphans = Object.keys(this.orphans); var i, hash; - while (this.totalOrphans > this.maxOrphans) { + while (this.totalOrphans > this.options.maxOrphans) { i = crypto.randomRange(0, orphans.length); hash = orphans[i]; orphans.splice(i, 1); @@ -757,7 +663,7 @@ Mempool.prototype._addTX = co(function* _addTX(tx) { } // Do not allow CSV until it's activated. - if (this.requireStandard) { + if (this.options.requireStandard) { if (!this.chain.state.hasCSV() && tx.version >= 2) { throw new VerifyError(tx, 'nonstandard', @@ -767,7 +673,7 @@ Mempool.prototype._addTX = co(function* _addTX(tx) { } // Do not allow segwit until it's activated. - if (!this.chain.state.hasWitness() && !this.prematureWitness) { + if (!this.chain.state.hasWitness() && !this.options.prematureWitness) { if (tx.hasWitness()) { throw new VerifyError(tx, 'nonstandard', @@ -777,14 +683,14 @@ Mempool.prototype._addTX = co(function* _addTX(tx) { } // Non-contextual standardness checks. - if (this.requireStandard) { + if (this.options.requireStandard) { if (!tx.isStandard(ret)) { throw new VerifyError(tx, 'nonstandard', ret.reason, ret.score); } - if (!this.replaceByFee) { + if (!this.options.replaceByFee) { if (tx.isRBF()) { throw new VerifyError(tx, 'nonstandard', @@ -884,7 +790,7 @@ Mempool.prototype.verify = co(function* verify(entry, view) { } // Check input an witness standardness. - if (this.requireStandard) { + if (this.options.requireStandard) { if (!tx.hasStandardInputs(view)) { throw new VerifyError(tx, 'nonstandard', @@ -912,9 +818,9 @@ Mempool.prototype.verify = co(function* verify(entry, view) { } // Make sure this guy gave a decent fee. - minFee = tx.getMinFee(entry.size, this.minRelay); + minFee = tx.getMinFee(entry.size, this.options.minRelay); - if (this.relayPriority && entry.fee < minFee) { + if (this.options.relayPriority && entry.fee < minFee) { if (!entry.isFree(height)) { throw new VerifyError(tx, 'insufficientfee', @@ -925,7 +831,7 @@ Mempool.prototype.verify = co(function* verify(entry, view) { // Continuously rate-limit free (really, very-low-fee) // transactions. This mitigates 'penny-flooding'. - if (this.limitFree && entry.fee < minFee) { + if (this.options.limitFree && entry.fee < minFee) { now = util.now(); // Use an exponentially decaying ~10-minute window. @@ -934,7 +840,7 @@ Mempool.prototype.verify = co(function* verify(entry, view) { // The limitFreeRelay unit is thousand-bytes-per-minute // At default rate it would take over a month to fill 1GB. - if (this.freeCount > this.limitFreeRelay * 10 * 1000) { + if (this.freeCount > this.options.limitFreeRelay * 10 * 1000) { throw new VerifyError(tx, 'insufficientfee', 'rate limited free transaction', @@ -945,11 +851,11 @@ Mempool.prototype.verify = co(function* verify(entry, view) { } // Important safety feature. - if (this.rejectAbsurdFees && entry.fee > minFee * 10000) + if (this.options.rejectAbsurdFees && entry.fee > minFee * 10000) throw new VerifyError(tx, 'highfee', 'absurdly-high-fee', 0); // Why do we have this here? Nested transactions are cool. - if (this.countAncestors(tx) > this.maxAncestors) { + if (this.countAncestors(tx) > this.options.maxAncestors) { throw new VerifyError(tx, 'nonstandard', 'too-long-mempool-chain', @@ -992,7 +898,7 @@ Mempool.prototype.verify = co(function* verify(entry, view) { } // Paranoid checks. - if (this.paranoidChecks) { + if (this.options.paranoidChecks) { flags = Script.flags.MANDATORY_VERIFY_FLAGS; result = yield this.verifyResult(tx, view, flags); assert(result, 'BUG: Verify failed for mandatory but not standard.'); @@ -1143,12 +1049,12 @@ Mempool.prototype._countAncestors = function countAncestors(tx, count, set) { set[hash] = true; count += 1; - if (count > this.maxAncestors) + if (count > this.options.maxAncestors) break; count = this._countAncestors(prev, count, set); - if (count > this.maxAncestors) + if (count > this.options.maxAncestors) break; } @@ -1941,6 +1847,146 @@ Mempool.prototype.getSize = function getSize() { return this.size; }; +/** + * MempoolOptions + * @constructor + * @param {Object} + */ + +function MempoolOptions(options) { + if (!(this instanceof MempoolOptions)) + return new MempoolOptions(options); + + this.network = Network.primary; + this.chain = null; + this.logger = null; + this.fees = null; + + this.limitFree = true; + this.limitFreeRelay = 15; + this.relayPriority = true; + this.requireStandard = this.network.requireStandard; + this.rejectAbsurdFees = true; + this.prematureWitness = false; + this.paranoidChecks = false; + this.replaceByFee = false; + + this.maxSize = policy.MEMPOOL_MAX_SIZE; + this.maxOrphans = policy.MEMPOOL_MAX_ORPHANS; + this.maxAncestors = policy.MEMPOOL_MAX_ANCESTORS; + this.expiryTime = policy.MEMPOOL_EXPIRY_TIME; + this.minRelay = this.network.minRelay; + + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {MempoolOptions} + */ + +MempoolOptions.prototype.fromOptions = function fromOptions(options) { + assert(options, 'Mempool requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'Mempool requires a blockchain.'); + + this.chain = options.chain; + this.network = options.chain.network; + this.logger = options.chain.logger; + + this.requireStandard = this.network.requireStandard; + this.minRelay = this.network.minRelay; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.fees != null) { + assert(typeof options.fees === 'object'); + this.fees = options.fees; + } + + if (options.limitFree != null) { + assert(typeof options.limitFree === 'boolean'); + this.limitFree = options.limitFree; + } + + if (options.limitFreeRelay != null) { + assert(util.isNumber(options.limitFreeRelay)); + this.limitFreeRelay = options.limitFreeRelay; + } + + if (options.relayPriority != null) { + assert(typeof options.relayPriority === 'boolean'); + this.relayPriority = options.relayPriority; + } + + if (options.requireStandard != null) { + assert(typeof options.requireStandard === 'boolean'); + this.requireStandard = options.requireStandard; + } + + if (options.rejectAbsurdFees != null) { + assert(typeof options.rejectAbsurdFees === 'boolean'); + this.rejectAbsurdFees = options.rejectAbsurdFees; + } + + if (options.prematureWitness != null) { + assert(typeof options.prematureWitness === 'boolean'); + this.prematureWitness = options.prematureWitness; + } + + if (options.paranoidChecks != null) { + assert(typeof options.paranoidChecks === 'boolean'); + this.paranoidChecks = options.paranoidChecks; + } + + if (options.replaceByFee != null) { + assert(typeof options.replaceByFee === 'boolean'); + this.replaceByFee = options.replaceByFee; + } + + if (options.maxSize != null) { + assert(util.isNumber(options.maxSize)); + this.maxSize = options.maxSize; + } + + if (options.maxOrphans != null) { + assert(util.isNumber(options.maxOrphans)); + this.maxOrphans = options.maxOrphans; + } + + if (options.maxAncestors != null) { + assert(util.isNumber(options.maxAncestors)); + this.maxAncestors = options.maxAncestors; + } + + if (options.expiryTime != null) { + assert(util.isNumber(options.expiryTime)); + this.expiryTime = options.expiryTime; + } + + if (options.minRelay != null) { + assert(util.isNumber(options.minRelay)); + this.minRelay = options.minRelay; + } + + return this; +}; + +/** + * Instantiate mempool options from object. + * @param {Object} options + * @returns {MempoolOptions} + */ + +MempoolOptions.fromOptions = function fromOptions(options) { + return new MempoolOptions().fromOptions(options); +}; + /** * TX Address Index */ diff --git a/lib/mining/mine.js b/lib/mining/mine.js index 7c9ef031..ac0b0357 100644 --- a/lib/mining/mine.js +++ b/lib/mining/mine.js @@ -4,8 +4,8 @@ * https://github.com/bcoin-org/bcoin */ -var crypto = require('../crypto/crypto'); var assert = require('assert'); +var crypto = require('../crypto/crypto'); /** * Hash until the nonce overflows. diff --git a/lib/mining/miner.js b/lib/mining/miner.js index 367f573a..8d1fb8f8 100644 --- a/lib/mining/miner.js +++ b/lib/mining/miner.js @@ -13,9 +13,10 @@ var co = require('../utils/co'); var AsyncObject = require('../utils/async'); var Address = require('../primitives/address'); var MinerBlock = require('./minerblock'); -var BlockEntry = MinerBlock.BlockEntry; +var Network = require('../protocol/network'); var consensus = require('../protocol/consensus'); var policy = require('../protocol/policy'); +var BlockEntry = MinerBlock.BlockEntry; /** * A bitcoin miner (supports mining witness blocks). @@ -36,92 +37,24 @@ function Miner(options) { AsyncObject.call(this); - assert(options, 'Miner requires options.'); - assert(options.chain, 'Miner requires a blockchain.'); + this.options = new MinerOptions(options); - this.chain = options.chain; - this.mempool = options.mempool; - this.network = this.chain.network; - this.logger = options.logger || this.chain.logger; + this.network = this.options.network; + this.logger = this.options.logger; + this.chain = this.options.chain; + this.mempool = this.options.mempool; + this.addresses = this.options.addresses; this.running = false; this.stopping = false; this.attempt = null; this.since = 0; - this.version = -1; - this.addresses = []; - this.coinbaseFlags = new Buffer('mined by bcoin', 'ascii'); - - this.minWeight = policy.MIN_BLOCK_WEIGHT; - this.maxWeight = policy.MAX_BLOCK_WEIGHT; - this.priorityWeight = policy.PRIORITY_BLOCK_WEIGHT; - this.minPriority = policy.MIN_BLOCK_PRIORITY; - this.maxSigops = consensus.MAX_BLOCK_SIGOPS_COST; - - this._initOptions(options); this._init(); } util.inherits(Miner, AsyncObject); -/** - * Initialize the miner options. - * @private - */ - -Miner.prototype._initOptions = function _initOptions(options) { - var i, flags; - - if (options.version != null) { - assert(util.isNumber(options.version)); - this.version = options.version; - } - - if (options.address) - this.addAddress(options.address); - - if (options.addresses) { - assert(Array.isArray(options.addresses)); - for (i = 0; i < options.addresses.length; i++) - this.addAddress(options.addresses[i]); - } - - if (options.coinbaseFlags) { - flags = options.coinbaseFlags; - if (typeof flags === 'string') - flags = new Buffer(flags, 'utf8'); - assert(Buffer.isBuffer(flags)); - this.coinbaseFlags = flags; - } - - if (options.minWeight != null) { - assert(util.isNumber(options.minWeight)); - this.minWeight = options.minWeight; - } - - if (options.maxWeight != null) { - assert(util.isNumber(options.maxWeight)); - this.maxWeight = options.maxWeight; - } - - if (options.maxSigops != null) { - assert(util.isNumber(options.maxSigops)); - assert(options.maxSigops <= consensus.MAX_BLOCK_SIGOPS_COST); - this.maxSigops = options.maxSigops; - } - - if (options.priorityWeight != null) { - assert(util.isNumber(options.priorityWeight)); - this.priorityWeight = options.priorityWeight; - } - - if (options.minPriority != null) { - assert(util.isNumber(options.minPriority)); - this.minPriority = options.minPriority; - } -}; - /** * Initialize the miner. * @private @@ -168,7 +101,7 @@ Miner.prototype._open = co(function* open() { yield this.mempool.open(); this.logger.info('Miner loaded (flags=%s).', - this.coinbaseFlags.toString('utf8')); + this.options.coinbaseFlags.toString('utf8')); }); /** @@ -306,7 +239,7 @@ Miner.prototype._onStop = function _onStop() { */ Miner.prototype.createBlock = co(function* createBlock(tip, address) { - var version = this.version; + var version = this.options.version; var ts, locktime, target, attempt; if (!tip) @@ -335,7 +268,7 @@ Miner.prototype.createBlock = co(function* createBlock(tip, address) { locktime: locktime, flags: this.chain.state.flags, address: address, - coinbaseFlags: this.coinbaseFlags, + coinbaseFlags: this.options.coinbaseFlags, witness: this.chain.state.hasWitness(), network: this.network }); @@ -457,16 +390,17 @@ Miner.prototype.build = function build(attempt) { weight += tx.getWeight(); - if (weight > this.maxWeight) + if (weight > this.options.maxWeight) continue; sigops += item.sigops; - if (sigops > this.maxSigops) + if (sigops > this.options.maxSigops) continue; if (priority) { - if (weight > this.priorityWeight || item.priority < this.minPriority) { + if (weight > this.options.priorityWeight + || item.priority < this.options.minPriority) { // Todo: Compare descendant rate with // cumulative fees and cumulative vsize. queue.cmp = cmpRate; @@ -475,7 +409,7 @@ Miner.prototype.build = function build(attempt) { continue; } } else { - if (item.free && weight >= this.minWeight) + if (item.free && weight >= this.options.minWeight) continue; } @@ -504,6 +438,123 @@ Miner.prototype.build = function build(attempt) { assert(block.getWeight() <= attempt.weight); }; +/** + * MinerOptions + * @constructor + * @param {Object} + */ + +function MinerOptions(options) { + if (!(this instanceof MinerOptions)) + return new MinerOptions(options); + + this.network = Network.primary; + this.logger = null; + this.chain = null; + this.mempool = null; + + this.version = -1; + this.addresses = []; + this.coinbaseFlags = new Buffer('mined by bcoin', 'ascii'); + + this.minWeight = policy.MIN_BLOCK_WEIGHT; + this.maxWeight = policy.MAX_BLOCK_WEIGHT; + this.priorityWeight = policy.PRIORITY_BLOCK_WEIGHT; + this.minPriority = policy.MIN_BLOCK_PRIORITY; + this.maxSigops = consensus.MAX_BLOCK_SIGOPS_COST; + + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {MinerOptions} + */ + +MinerOptions.prototype.fromOptions = function fromOptions(options) { + var i, flags; + + assert(options, 'Miner requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'Miner requires a blockchain.'); + + this.chain = options.chain; + this.network = options.chain.network; + this.logger = options.chain.logger; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.mempool != null) { + assert(typeof options.mempool === 'object'); + this.mempool = options.mempool; + } + + if (options.version != null) { + assert(util.isNumber(options.version)); + this.version = options.version; + } + + if (options.address) + this.addresses.push(new Address(options.address)); + + if (options.addresses) { + assert(Array.isArray(options.addresses)); + for (i = 0; i < options.addresses.length; i++) + this.addresses.push(new Address(options.addresses[i])); + } + + if (options.coinbaseFlags) { + flags = options.coinbaseFlags; + if (typeof flags === 'string') + flags = new Buffer(flags, 'utf8'); + assert(Buffer.isBuffer(flags)); + this.coinbaseFlags = flags; + } + + if (options.minWeight != null) { + assert(util.isNumber(options.minWeight)); + this.minWeight = options.minWeight; + } + + if (options.maxWeight != null) { + assert(util.isNumber(options.maxWeight)); + this.maxWeight = options.maxWeight; + } + + if (options.maxSigops != null) { + assert(util.isNumber(options.maxSigops)); + assert(options.maxSigops <= consensus.MAX_BLOCK_SIGOPS_COST); + this.maxSigops = options.maxSigops; + } + + if (options.priorityWeight != null) { + assert(util.isNumber(options.priorityWeight)); + this.priorityWeight = options.priorityWeight; + } + + if (options.minPriority != null) { + assert(util.isNumber(options.minPriority)); + this.minPriority = options.minPriority; + } + + return this; +}; + +/** + * Instantiate miner options from object. + * @param {Object} options + * @returns {MinerOptions} + */ + +MinerOptions.fromOptions = function fromOptions(options) { + return new MinerOptions().fromOptions(options); +}; + /** * Queue * @constructor diff --git a/lib/mining/minerblock.js b/lib/mining/minerblock.js index 494fb542..670b8861 100644 --- a/lib/mining/minerblock.js +++ b/lib/mining/minerblock.js @@ -8,12 +8,12 @@ 'use strict'; var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; var BN = require('bn.js'); var util = require('../utils/util'); var co = require('../utils/co'); var StaticWriter = require('../utils/staticwriter'); var Network = require('../protocol/network'); -var EventEmitter = require('events').EventEmitter; var TX = require('../primitives/tx'); var Block = require('../primitives/block'); var Input = require('../primitives/input'); @@ -48,6 +48,7 @@ function MinerBlock(options) { EventEmitter.call(this); + this.network = Network.get(options.network); this.tip = options.tip; this.version = options.version; this.height = options.tip.height + 1; @@ -61,7 +62,6 @@ function MinerBlock(options) { this.coinbaseFlags = options.coinbaseFlags; this.witness = options.witness; this.address = options.address; - this.network = Network.get(options.network); this.reward = consensus.getReward(this.height, this.network.halvingInterval); this.destroyed = false; diff --git a/lib/net/bip150.js b/lib/net/bip150.js index 934a5ebe..5933d63a 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -308,14 +308,31 @@ BIP150.address = function address(key) { * @constructor */ -function AuthDB() { +function AuthDB(options) { if (!(this instanceof AuthDB)) - return new AuthDB(); + return new AuthDB(options); this.known = {}; this.authorized = []; + + this._init(options); } +AuthDB.prototype._init = function _init(options) { + if (!options) + return; + + if (options.knownPeers != null) { + assert(typeof options.knownPeers === 'object'); + this.setKnown(options.knownPeers); + } + + if (options.authPeers != null) { + assert(Array.isArray(options.authPeers)); + this.setAuthorized(options.authPeers); + } +}; + AuthDB.prototype.addKnown = function addKnown(host, key) { assert(typeof host === 'string'); assert(Buffer.isBuffer(key) && key.length === 33, diff --git a/lib/net/hostlist.js b/lib/net/hostlist.js index 6cdcedea..96e10c10 100644 --- a/lib/net/hostlist.js +++ b/lib/net/hostlist.js @@ -15,6 +15,9 @@ var List = require('../utils/list'); var murmur3 = require('../utils/murmur3'); var StaticWriter = require('../utils/staticwriter'); var Map = require('../utils/map'); +var common = require('./common'); +var dns = require('./dns'); +var Network = require('../protocol/network'); /** * Host List @@ -26,19 +29,12 @@ function HostList(options) { if (!(this instanceof HostList)) return new HostList(options); - assert(options, 'Options are required.'); - assert(options.address); - assert(options.network); - assert(options.logger); - assert(typeof options.resolve === 'function'); - assert(options.banTime >= 0); - - this.address = options.address; - this.network = options.network; - this.logger = options.logger; - this.proxyServer = options.proxyServer; - this.resolve = options.resolve; - this.banTime = options.banTime; + this.network = Network.primary; + this.logger = null; + this.address = new NetAddress(); + this.proxyServer = null; + this.resolve = dns.resolve; + this.banTime = common.BAN_TIME; this.seeds = []; this.banned = {}; @@ -60,23 +56,68 @@ function HostList(options) { this.maxFailures = 10; this.maxRefs = 8; - this._init(); + this._init(options); } +/** + * Initialize options. + * @private + */ + +HostList.prototype._initOptions = function initOptions(options) { + if (options.network != null) + this.network = Network.get(options.network); + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.address != null) { + assert(options.address instanceof NetAddress); + this.address = options.address; + } + + if (options.proxyServer != null) { + assert(typeof options.proxyServer === 'string'); + this.proxyServer = options.proxyServer; + } + + if (options.resolve != null) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.banTime != null) { + assert(options.banTime >= 0); + this.banTime = options.banTime; + } + + if (options.seeds) + assert(Array.isArray(options.seeds)); +}; + /** * Initialize list. * @private */ -HostList.prototype._init = function init() { +HostList.prototype._init = function init(options) { var i; + this._initOptions(options); + for (i = 0; i < this.maxBuckets; i++) this.fresh.push(new Map()); for (i = 0; i < this.maxBuckets; i++) this.used.push(new List()); + if (options.seeds) { + this.setSeeds(options.seeds); + return; + } + this.setSeeds(this.network.seeds); }; @@ -661,12 +702,14 @@ HostList.prototype.populate = co(function* populate(seed) { return; } - this.logger.info('Resolving hosts from seed: %s.', seed.host); + if (this.logger) + this.logger.info('Resolving hosts from seed: %s.', seed.host); try { hosts = yield this.resolve(seed.host, this.proxyServer); } catch (e) { - this.logger.error(e); + if (this.logger) + this.logger.error(e); return; } diff --git a/lib/net/peer.js b/lib/net/peer.js index 009f5bf4..d8e2dfce 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -146,7 +146,7 @@ function Peer(pool) { this.hostname, this.outbound, this.pool.authdb, - this.pool.identityKey); + this.options.identityKey); this.bip151.bip150 = this.bip150; } } @@ -353,12 +353,12 @@ Peer.prototype.accept = function accept(socket) { */ Peer.prototype.connect = function connect(addr) { - var proxy = this.pool.proxyServer; + var proxy = this.options.proxyServer; var socket; assert(!this.socket); - socket = this.pool.createSocket(addr.port, addr.host, proxy); + socket = this.options.createSocket(addr.port, addr.host, proxy); this.address = addr; this.ts = util.now(); @@ -889,13 +889,13 @@ Peer.prototype.sendHeaders = function sendHeaders(items) { Peer.prototype.sendVersion = function sendVersion() { var packet = new packets.VersionPacket(); - packet.version = this.pool.protoVersion; - packet.services = this.pool.address.services; + packet.version = this.options.version; + packet.services = this.options.services; packet.ts = this.network.now(); packet.recv = this.address; packet.from = this.pool.address; - packet.nonce = this.pool.localNonce; - packet.agent = this.pool.userAgent; + packet.nonce = this.pool.nonce; + packet.agent = this.options.agent; packet.height = this.chain.height; packet.noRelay = this.options.noRelay; this.send(packet); @@ -1698,7 +1698,7 @@ Peer.prototype.handleGetUTXOs = co(function* handleGetUTXOs(packet) { if (this.options.selfish) return; - if (this.chain.db.options.spv) + if (this.chain.options.spv) return; if (packet.prevout.length > 15) @@ -1770,10 +1770,10 @@ Peer.prototype.handleGetHeaders = co(function* handleGetHeaders(packet) { if (this.options.selfish) return; - if (this.chain.db.options.spv) + if (this.chain.options.spv) return; - if (this.chain.db.options.prune) + if (this.chain.options.prune) return; if (packet.locator.length > 0) { @@ -1818,10 +1818,10 @@ Peer.prototype.handleGetBlocks = co(function* handleGetBlocks(packet) { if (this.options.selfish) return; - if (this.chain.db.options.spv) + if (this.chain.options.spv) return; - if (this.chain.db.options.prune) + if (this.chain.options.prune) return; hash = yield this.chain.findLocator(packet.locator); @@ -1863,7 +1863,7 @@ Peer.prototype.handleVersion = co(function* handleVersion(packet) { this.haveWitness = packet.hasWitness(); if (!this.network.selfConnect) { - if (util.equal(packet.nonce, this.pool.localNonce)) + if (util.equal(packet.nonce, this.pool.nonce)) throw new Error('We connected to ourself. Oops.'); } @@ -1990,10 +1990,10 @@ Peer.prototype.getItem = co(function* getItem(item) { return this.mempool.getTX(item.hash); } - if (this.chain.db.options.spv) + if (this.chain.options.spv) return; - if (this.chain.db.options.prune) + if (this.chain.options.prune) return; return yield this.chain.db.getBlock(item.hash); @@ -2016,14 +2016,14 @@ Peer.prototype.sendBlock = co(function* sendBlock(item, witness) { } if (this.options.selfish - || this.chain.db.options.spv - || this.chain.db.options.prune) { + || this.chain.options.spv + || this.chain.options.prune) { return false; } // If we have the same serialization, we // can write the raw binary to the socket. - if (witness === this.chain.db.options.witness) { + if (witness === this.chain.options.witness) { block = yield this.chain.db.getRawBlock(item.hash); if (!block) @@ -2713,10 +2713,10 @@ Peer.prototype.handleGetBlockTxn = co(function* handleGetBlockTxn(packet) { var req = packet.request; var res, item, block, height; - if (this.chain.db.options.spv) + if (this.chain.options.spv) return; - if (this.chain.db.options.prune) + if (this.chain.options.prune) return; if (this.options.selfish) @@ -2928,7 +2928,7 @@ Peer.prototype.sendCompact = function sendCompact() { Peer.prototype.increaseBan = function increaseBan(score) { this.banScore += score; - if (this.banScore >= this.pool.banScore) { + if (this.banScore >= this.options.banScore) { this.logger.debug('Ban threshold exceeded (%s).', this.hostname); this.ban(); return true; diff --git a/lib/net/pool.js b/lib/net/pool.js index 89317711..88ff3721 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -56,8 +56,6 @@ var VerifyResult = errors.VerifyResult; * headers, hashes, utxos, or transactions to peers. * @param {Boolean?} options.broadcast - Whether to automatically broadcast * transactions accepted to our mempool. - * @param {Boolean?} options.witness - Request witness blocks and transactions. - * Only deal with witness peers. * @param {Boolean} options.noDiscovery - Automatically discover new * peers. * @param {String[]} options.seeds @@ -94,198 +92,46 @@ function Pool(options) { AsyncObject.call(this); - assert(options && options.chain, 'Pool requires a blockchain.'); + this.options = new PoolOptions(options); - this.options = options; - this.chain = options.chain; - this.logger = options.logger || this.chain.logger; - this.mempool = options.mempool; - this.network = this.chain.network; + this.network = this.options.network; + this.logger = this.options.logger; + this.chain = this.options.chain; + this.mempool = this.options.mempool; + this.server = this.options.createServer(); + this.locker = new Lock(); - this.server = null; - this.maxOutbound = 8; - this.maxInbound = 8; this.connected = false; this.syncing = false; - this.createSocket = tcp.createSocket; - this.createServer = tcp.createServer; - this.resolve = dns.resolve; - this.locker = new Lock(); - this.authdb = null; - this.identityKey = null; - this.proxyServer = null; - this.banTime = common.BAN_TIME; - this.banScore = common.BAN_SCORE; - this.feeRate = -1; - - // Required services. - this.reqServices = common.REQUIRED_SERVICES; - - this.address = new NetAddress(); - this.address.ts = this.network.now(); - this.address.services = common.LOCAL_SERVICES; - this.address.setPort(this.network.port); - - this.hosts = new HostList(this); - this.peers = new PeerList(this); - - this.localNonce = util.nonce(); - - this.protoVersion = common.PROTOCOL_VERSION; - this.userAgent = common.USER_AGENT; - + this.feeRate = this.options.feeRate; + this.nonce = util.nonce(); this.spvFilter = null; this.txFilter = null; - - // Requested objects. this.requestMap = new Map(); this.queueMap = new Map(); - - // Currently broadcasted objects. this.invMap = new Map(); - this.invTimeout = 60000; - this.scheduled = false; this.pendingWatch = null; this.pendingRefill = null; - this._initOptions(); - this._init(); -}; + this.peers = new PeerList(); + this.authdb = new BIP150.AuthDB(this.options); + this.address = new NetAddress(this.options); + this.address.ts = this.network.now(); + this.hosts = new HostList(this.options); + this.hosts.address = this.address; -util.inherits(Pool, AsyncObject); - -/** - * Initialize options. - * @private - */ - -Pool.prototype._initOptions = function _initOptions() { - if (this.options.noRelay == null) - this.options.noRelay = !!this.options.spv; - - if (this.options.headers == null) - this.options.headers = this.options.spv; - - if (!this.options.witness) { - this.address.services &= ~common.services.WITNESS; - this.reqServices &= ~common.services.WITNESS; - } - - if (this.options.host != null) { - assert(typeof this.options.host === 'string'); - this.address.setHost(this.options.host); - } - - if (this.options.port != null) { - assert(typeof this.options.port === 'number'); - this.address.setPort(this.options.port); - } - - if (this.options.maxOutbound != null) { - assert(typeof this.options.maxOutbound === 'number'); - this.maxOutbound = this.options.maxOutbound; - } - - if (this.options.maxInbound != null) { - assert(typeof this.options.maxInbound === 'number'); - this.maxInbound = this.options.maxInbound; - } - - if (this.options.createSocket) { - assert(typeof this.options.createSocket === 'function'); - this.createSocket = this.options.createSocket; - } - - if (this.options.createServer) { - assert(typeof this.options.createServer === 'function'); - this.createServer = this.options.createServer; - } - - if (this.options.resolve) { - assert(typeof this.options.resolve === 'function'); - this.resolve = this.options.resolve; - } - - if (this.options.proxyServer) { - assert(typeof this.options.proxyServer === 'string'); - this.proxyServer = this.options.proxyServer; - } - - if (this.options.selfish) { - assert(typeof this.options.selfish === 'boolean'); - this.address.services &= ~common.services.NETWORK; - } - - if (this.options.spv) { - assert(typeof this.options.spv === 'boolean'); - this.address.services &= ~common.services.NETWORK; - } - - if (this.options.protoVersion) { - assert(typeof this.options.protoVersion === 'number'); - this.protoVersion = this.options.protoVersion; - } - - if (this.options.userAgent) { - assert(typeof this.options.userAgent === 'string'); - assert(this.options.userAgent.length < 256); - this.userAgent = this.options.userAgent; - } - - if (this.options.bip150) { - assert(typeof this.options.bip151 === 'boolean'); - - this.authdb = new BIP150.AuthDB(); - - if (this.options.authPeers) - this.authdb.setAuthorized(this.options.authPeers); - - if (this.options.knownPeers) - this.authdb.setKnown(this.options.knownPeers); - - this.identityKey = this.options.identityKey || ec.generatePrivateKey(); - - assert(Buffer.isBuffer(this.identityKey), 'Identity key must be a buffer.'); - assert(ec.privateKeyVerify(this.identityKey), - 'Invalid identity key.'); - } - - if (this.options.banScore != null) { - assert(typeof this.options.banScore === 'number'); - this.banScore = this.options.banScore; - } - - if (this.options.banTime != null) { - assert(typeof this.options.banTime === 'number'); - this.banTime = this.options.banTime; - } - - if (this.options.feeRate != null) { - assert(typeof this.options.feeRate === 'number'); - this.feeRate = this.options.feeRate; - } - - if (this.options.seeds) - this.hosts.setSeeds(this.options.seeds); - - if (this.options.preferredSeed) - this.hosts.setSeeds([this.options.preferredSeed]); - - if (this.options.spv) { + if (this.options.spv) this.spvFilter = Bloom.fromRate(10000, 0.001, Bloom.flags.ALL); - this.reqServices |= common.services.BLOOM; - } if (!this.options.mempool) this.txFilter = new Bloom.Rolling(50000, 0.000001); - if (this.options.invTimeout != null) { - assert(typeof this.options.invTimeout === 'number'); - this.invTimeout = this.options.invTimeout; - } + this._init(); }; +util.inherits(Pool, AsyncObject); + /** * Initialize the pool. * @private @@ -294,7 +140,22 @@ Pool.prototype._initOptions = function _initOptions() { Pool.prototype._init = function _init() { var self = this; - this._initServer(); + this.server.on('error', function(err) { + self.emit('error', err); + }); + + this.server.on('connection', function(socket) { + self.handleSocket(socket); + self.emit('connection', socket); + }); + + this.server.on('listening', function() { + var data = self.server.address(); + self.logger.info( + 'Pool server listening on %s (port=%d).', + data.address, data.port); + self.emit('listening', data); + }); this.chain.on('block', function(block, entry) { self.emit('block', block, entry); @@ -351,39 +212,6 @@ Pool.prototype._init = function _init() { } }; -/** - * Initialize server. - * @private - */ - -Pool.prototype._initServer = function _initServer() { - var self = this; - - assert(!this.server); - - if (!this.createServer) - return; - - this.server = this.createServer(); - - this.server.on('error', function(err) { - self.emit('error', err); - }); - - this.server.on('connection', function(socket) { - self.handleSocket(socket); - self.emit('connection', socket); - }); - - this.server.on('listening', function() { - var data = self.server.address(); - self.logger.info( - 'Pool server listening on %s (port=%d).', - data.address, data.port); - self.emit('listening', data); - }); -}; - /** * Open the pool, wait for the chain to load. * @alias Pool#open @@ -398,7 +226,7 @@ Pool.prototype._open = co(function* _open() { else yield this.chain.open(); - this.logger.info('Pool loaded (maxpeers=%d).', this.maxOutbound); + this.logger.info('Pool loaded (maxpeers=%d).', this.options.maxOutbound); if (this.identityKey) { key = ec.publicKeyCreate(this.identityKey, true); @@ -533,9 +361,6 @@ Pool.prototype._disconnect = co(function* disconnect() { */ Pool.prototype.listen = co(function* listen() { - if (!this.createServer) - return; - assert(this.server); assert(!this.connected, 'Already listening.'); @@ -565,9 +390,6 @@ Pool.prototype._listen = function _listen() { */ Pool.prototype.unlisten = co(function* unlisten() { - if (!this.createServer) - return; - assert(this.server); assert(this.connected, 'Not listening.'); @@ -607,7 +429,7 @@ Pool.prototype.handleSocket = function handleSocket(socket) { host = IP.normalize(socket.remoteAddress); - if (this.peers.inbound >= this.maxInbound) { + if (this.peers.inbound >= this.options.maxInbound) { this.logger.debug('Ignoring peer: too many inbound (%s).', host); socket.destroy(); return; @@ -966,9 +788,13 @@ Pool.prototype.handleOpen = function handleOpen(peer) { Pool.prototype.handleClose = co(function* handleClose(peer, connected) { var outbound = peer.outbound; + var loader = peer.isLoader(); this.removePeer(peer); + if (loader) + this.logger.info('Removed loader peer (%s).', peer.hostname); + if (!this.loaded) return; @@ -1007,6 +833,7 @@ Pool.prototype.handleVersion = function handleVersion(peer, packet) { */ Pool.prototype.handleAddr = function handleAddr(peer, addrs) { + var services = this.options.requiredServices; var i, addr; if (this.options.noDiscovery) @@ -1018,7 +845,7 @@ Pool.prototype.handleAddr = function handleAddr(peer, addrs) { if (!addr.isRoutable()) continue; - if (!addr.hasServices(this.reqServices)) + if (!addr.hasServices(services)) continue; if (this.hosts.add(addr, peer.address)) @@ -1489,6 +1316,7 @@ Pool.prototype.addInbound = function addInbound(socket) { */ Pool.prototype.getHost = function getHost(unique) { + var services = this.options.requiredServices; var now = this.network.now(); var i, entry, addr; @@ -1508,7 +1336,7 @@ Pool.prototype.getHost = function getHost(unique) { if (!addr.isValid()) continue; - if (!addr.hasServices(this.reqServices)) + if (!addr.hasServices(services)) continue; if (i < 30 && now - entry.lastAttempt < 600) @@ -1536,7 +1364,7 @@ Pool.prototype.addOutbound = function addOutbound() { if (!this.loaded) return; - if (this.peers.outbound >= this.maxOutbound) + if (this.peers.outbound >= this.options.maxOutbound) return; // Hang back if we don't have a loader peer yet. @@ -1561,7 +1389,7 @@ Pool.prototype.addOutbound = function addOutbound() { */ Pool.prototype.fillOutbound = function fillOutbound() { - var need = this.maxOutbound - this.peers.outbound; + var need = this.options.maxOutbound - this.peers.outbound; var i; if (!this.peers.load) @@ -1572,7 +1400,7 @@ Pool.prototype.fillOutbound = function fillOutbound() { this.logger.debug('Refilling peers (%d/%d).', this.peers.outbound, - this.maxOutbound); + this.options.maxOutbound); for (i = 0; i < need; i++) this.addOutbound(); @@ -2085,14 +1913,287 @@ Pool.prototype.getIP2 = co(function* getIP2() { return IP.normalize(ip); }); +/** + * PoolOptions + * @constructor + */ + +function PoolOptions(options) { + if (!(this instanceof PoolOptions)) + return new PoolOptions(options); + + this.network = Network.primary; + this.logger = null; + this.chain = null; + this.mempool = null; + + this.witness = false; + this.spv = false; + this.listen = false; + this.headers = false; + this.compact = false; + this.noRelay = false; + this.host = '0.0.0.0'; + this.port = this.network.port; + this.maxOutbound = 8; + this.maxInbound = 8; + this.createSocket = tcp.createSocket; + this.createServer = tcp.createServer; + this.resolve = dns.resolve; + this.proxyServer = null; + this.selfish = false; + this.version = common.PROTOCOL_VERSION; + this.agent = common.USER_AGENT; + this.bip151 = false; + this.bip150 = false; + this.authPeers = []; + this.knownPeers = {}; + this.identityKey = ec.generatePrivateKey(); + this.banScore = common.BAN_SCORE; + this.banTime = common.BAN_TIME; + this.feeRate = -1; + this.noDiscovery = false; + this.seeds = this.network.seeds; + this.preferredSeed = null; + this.invTimeout = 60000; + this.services = common.LOCAL_SERVICES; + this.requiredServices = common.REQUIRED_SERVICES; + + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {PoolOptions} + */ + +PoolOptions.prototype.fromOptions = function fromOptions(options) { + assert(options, 'Pool requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'Pool options require a blockchain.'); + + this.chain = options.chain; + this.network = options.chain.network; + this.logger = options.chain.logger; + + this.port = this.network.port; + this.seeds = this.network.seeds; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.mempool != null) { + assert(typeof options.mempool === 'object'); + this.mempool = options.mempool; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + assert(options.witness === this.chain.options.witness); + this.witness = options.witness; + } else { + this.witness = this.chain.options.witness; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + assert(options.spv === this.chain.options.spv); + this.spv = options.spv; + } else { + this.spv = this.chain.options.spv; + } + + if (options.listen != null) { + assert(typeof options.listen === 'boolean'); + this.listen = options.listen; + } + + if (options.headers != null) { + assert(typeof options.headers === 'boolean'); + this.headers = options.headers; + } else { + this.headers = this.spv === true; + } + + if (options.compact != null) { + assert(typeof options.compact === 'boolean'); + this.compact = options.compact; + } + + if (options.noRelay != null) { + assert(typeof options.noRelay === 'boolean'); + this.noRelay = options.noRelay; + } else { + this.noRelay = this.spv === true; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + this.host = options.host; + } + + if (options.port != null) { + assert(typeof options.port === 'number'); + this.port = options.port; + } + + if (options.maxOutbound != null) { + assert(typeof options.maxOutbound === 'number'); + this.maxOutbound = options.maxOutbound; + } + + if (options.maxInbound != null) { + assert(typeof options.maxInbound === 'number'); + this.maxInbound = options.maxInbound; + } + + if (options.createSocket) { + assert(typeof options.createSocket === 'function'); + this.createSocket = options.createSocket; + } + + if (options.createServer) { + assert(typeof options.createServer === 'function'); + this.createServer = options.createServer; + } + + if (options.resolve) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.proxyServer) { + assert(typeof options.proxyServer === 'string'); + this.proxyServer = options.proxyServer; + } + + if (options.selfish) { + assert(typeof options.selfish === 'boolean'); + this.selfish = options.selfish; + } + + if (options.version) { + assert(typeof options.version === 'number'); + this.version = options.version; + } + + if (options.agent) { + assert(typeof options.agent === 'string'); + assert(options.agent.length <= 255); + this.agent = options.agent; + } + + if (options.bip151 != null) { + assert(typeof options.bip151 === 'boolean'); + this.bip151 = options.bip151; + } + + if (options.bip150 != null) { + assert(typeof options.bip150 === 'boolean'); + assert(this.bip151, 'Cannot enable bip150 without bip151.'); + + if (options.knownPeers) { + assert(typeof options.knownPeers === 'object'); + assert(!Array.isArray(options.knownPeers)); + this.knownPeers = options.knownPeers; + } + + if (options.authPeers) { + assert(Array.isArray(options.authPeers)); + this.authPeers = options.authPeers; + } + + if (options.identityKey) { + assert(Buffer.isBuffer(options.identityKey), + 'Identity key must be a buffer.'); + assert(ec.privateKeyVerify(options.identityKey), + 'Invalid identity key.'); + this.identityKey = options.identityKey; + } + } + + if (options.banScore != null) { + assert(typeof this.options.banScore === 'number'); + this.banScore = this.options.banScore; + } + + if (options.banTime != null) { + assert(typeof this.options.banTime === 'number'); + this.banTime = this.options.banTime; + } + + if (options.feeRate != null) { + assert(typeof this.options.feeRate === 'number'); + this.feeRate = this.options.feeRate; + } + + if (options.noDiscovery != null) { + assert(typeof options.noDiscovery === 'boolean'); + this.noDiscovery = options.noDiscovery; + } + + if (options.seeds) { + assert(Array.isArray(options.seeds)); + this.seeds = options.seeds; + } + + if (options.preferredSeed) { + assert(typeof options.preferredSeed === 'string'); + this.seeds = [options.preferredSeed]; + } + + if (options.invTimeout != null) { + assert(typeof options.invTimeout === 'number'); + this.invTimeout = options.invTimeout; + } + + if (!this.witness) { + this.services &= ~common.services.WITNESS; + this.requiredServices &= ~common.services.WITNESS; + } + + if (this.spv) { + this.requiredServices |= common.services.BLOOM; + this.services &= ~common.services.NETWORK; + } + + if (this.selfish) + this.services &= ~common.services.NETWORK; + + if (options.services != null) { + assert(util.isUInt32(options.services)); + this.services = options.services; + } + + if (options.requiredServices != null) { + assert(util.isUInt32(options.requiredServices)); + this.requiredServices = options.requiredServices; + } + + return this; +}; + +/** + * Instantiate options from object. + * @param {Object} options + * @returns {PoolOptions} + */ + +PoolOptions.fromOptions = function fromOptions(options) { + return new PoolOptions().fromOptions(options); +}; + /** * Peer List * @constructor * @param {Object} options */ -function PeerList(options) { - this.logger = options.logger; +function PeerList() { this.map = {}; this.list = new List(); this.load = null; @@ -2155,10 +2256,8 @@ PeerList.prototype.remove = function remove(peer) { assert(this.map[peer.hostname]); delete this.map[peer.hostname]; - if (peer.isLoader()) { - this.logger.info('Removed loader peer (%s).', peer.hostname); + if (peer.isLoader()) this.load = null; - } if (peer.outbound) this.outbound--; @@ -2293,7 +2392,7 @@ BroadcastItem.prototype.refresh = function refresh() { this.timeout = setTimeout(function() { self.emit('timeout'); self.reject(new Error('Timed out.')); - }, this.pool.invTimeout); + }, this.pool.options.invTimeout); }; /** diff --git a/lib/net/tcp-browser.js b/lib/net/tcp-browser.js index 26da71fe..2fa1e3a8 100644 --- a/lib/net/tcp-browser.js +++ b/lib/net/tcp-browser.js @@ -7,10 +7,27 @@ 'use strict'; var ProxySocket = require('./proxysocket'); +var EventEmitter = require('events').EventEmitter; var tcp = exports; tcp.createSocket = function createSocket(port, host, proxy) { return ProxySocket.connect(proxy, port, host); }; -tcp.createServer = null; +tcp.createServer = function createServer() { + var server = new EventEmitter(); + server.listen = function listen(port, host, callback) { + callback(); + server.emit('listening'); + }; + server.close = function close(callback) { + callback(); + }; + server.address = function address() { + return { + address: '127.0.0.1', + port: 0 + }; + }; + return server; +}; diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 67a4649c..87f1a87b 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -65,17 +65,15 @@ function FullNode(options) { logger: this.logger, db: this.options.db, location: this.location('chain'), - preload: false, - spv: false, + maxFiles: this.options.maxFiles, + cacheSize: this.options.cacheSize, witness: this.options.witness, forceWitness: this.options.forceWitness, prune: this.options.prune, useCheckpoints: this.options.useCheckpoints, coinCache: this.options.coinCache, indexTX: this.options.indexTX, - indexAddress: this.options.indexAddress, - maxFiles: this.options.maxFiles, - cacheSize: this.options.cacheSize + indexAddress: this.options.indexAddress }); // Fee estimation. @@ -104,7 +102,6 @@ function FullNode(options) { logger: this.logger, chain: this.chain, mempool: this.mempool, - witness: this.options.witness, selfish: this.options.selfish, headers: this.options.headers, compact: this.options.compact, @@ -119,8 +116,7 @@ function FullNode(options) { preferredSeed: this.options.preferredSeed, noDiscovery: this.options.noDiscovery, port: this.options.port, - listen: this.options.listen, - spv: false + listen: this.options.listen }); // Miner needs access to the chain and mempool. @@ -141,13 +137,12 @@ function FullNode(options) { client: this.client, db: this.options.db, location: this.location('walletdb'), - witness: false, - useCheckpoints: this.options.useCheckpoints, maxFiles: this.options.walletMaxFiles, cacheSize: this.options.walletCacheSize, + witness: false, + useCheckpoints: this.options.useCheckpoints, startHeight: this.options.startHeight, wipeNoReally: this.options.wipeNoReally, - resolution: false, verify: false }); @@ -159,8 +154,8 @@ function FullNode(options) { node: this, key: this.options.sslKey, cert: this.options.sslCert, - port: this.options.httpPort || this.network.rpcPort, - host: this.options.httpHost || '0.0.0.0', + port: this.options.httpPort, + host: this.options.httpHost, apiKey: this.options.apiKey, serviceKey: this.options.serviceKey, walletAuth: this.options.walletAuth, diff --git a/lib/node/logger.js b/lib/node/logger.js index 6a67cac6..cb95550b 100644 --- a/lib/node/logger.js +++ b/lib/node/logger.js @@ -23,54 +23,105 @@ function Logger(options) { if (!(this instanceof Logger)) return new Logger(options); - if (!options) - options = {}; - - if (typeof options === 'string') - options = { level: options }; - - this.level = Logger.levels.warning; - this.colors = options.colors !== false; - this.console = options.console !== false; - this.file = options.file; - this.stream = options.stream; + this.level = Logger.levels.WARNING; + this.colors = Logger.HAS_TTY; + this.console = true; + this.file = null; + this.stream = null; this.closed = false; - assert(!this.file || typeof this.file === 'string', 'Bad file.'); - assert(!this.stream || typeof this.stream.write === 'function', 'Bad stream.'); - - if (!process.stdout || !process.stdout.isTTY) - this.colors = false; - - if (options.level != null) - this.setLevel(options.level); + this._init(options); } +/** + * Whether stdout is a tty FD. + * @const {Boolean} + */ + +Logger.HAS_TTY = !!(process.stdout && process.stdout.isTTY); + /** * Available log levels. * @enum {Number} */ Logger.levels = { - none: 0, - error: 1, - warning: 2, - info: 3, - debug: 4, - spam: 5 + NONE: 0, + ERROR: 1, + WARNING: 2, + INFO: 3, + DEBUG: 4, + SPAM: 5 }; +/** + * Available log levels. + * @enum {Number} + */ + +Logger.levelsByVal = [ + 'none', + 'error', + 'warning', + 'info', + 'debug', + 'spam' +]; + /** * Default CSI colors. * @enum {String} */ -Logger.colors = { - error: '1;31', - warning: '1;33', - info: '94', - debug: '90', - spam: '90' +Logger.colors = [ + '0', + '1;31', + '1;33', + '94', + '90', + '90' +]; + +/** + * Initialize the logger. + * @private + * @param {Object} options + */ + +Logger.prototype._init = function _init(options) { + if (!options) + return; + + if (typeof options === 'string') { + this.setLevel(options); + return; + } + + if (options.level != null) { + assert(typeof options.level === 'string'); + this.setLevel(options.level); + } + + if (options.colors != null && Logger.HAS_TTY) { + assert(typeof options.colors === 'boolean'); + this.colors = options.colors; + } + + if (options.console != null) { + assert(typeof options.console === 'boolean'); + this.console = options.console; + } + + if (options.file != null) { + assert(typeof options.file === 'string', 'Bad file.'); + this.file = options.file; + } + + if (options.stream != null) { + assert(typeof options.stream === 'object', 'Bad stream.'); + assert(typeof options.stream.write === 'function', 'Bad stream.'); + this.stream = options.stream; + } }; /** @@ -105,8 +156,8 @@ Logger.prototype.close = function close() { * @param {String} level */ -Logger.prototype.setLevel = function setLevel(level) { - level = Logger.levels[level]; +Logger.prototype.setLevel = function setLevel(name) { + var level = Logger.levels[name.toUpperCase()]; assert(level != null, 'Invalid log level.'); this.level = level; }; @@ -120,7 +171,7 @@ Logger.prototype.setLevel = function setLevel(level) { Logger.prototype.error = function error(err) { var i, args; - if (this.level < Logger.levels.error) + if (this.level < Logger.levels.ERROR) return; if (err instanceof Error) @@ -131,7 +182,7 @@ Logger.prototype.error = function error(err) { for (i = 0; i < args.length; i++) args[i] = arguments[i]; - this.log('error', args); + this.log(Logger.levels.ERROR, args); }; /** @@ -143,7 +194,7 @@ Logger.prototype.error = function error(err) { Logger.prototype.warning = function warning() { var i, args; - if (this.level < Logger.levels.warning) + if (this.level < Logger.levels.WARNING) return; args = new Array(arguments.length); @@ -151,7 +202,7 @@ Logger.prototype.warning = function warning() { for (i = 0; i < args.length; i++) args[i] = arguments[i]; - this.log('warning', args); + this.log(Logger.levels.WARNING, args); }; /** @@ -163,7 +214,7 @@ Logger.prototype.warning = function warning() { Logger.prototype.info = function info() { var i, args; - if (this.level < Logger.levels.info) + if (this.level < Logger.levels.INFO) return; args = new Array(arguments.length); @@ -171,7 +222,7 @@ Logger.prototype.info = function info() { for (i = 0; i < args.length; i++) args[i] = arguments[i]; - this.log('info', args); + this.log(Logger.levels.INFO, args); }; /** @@ -183,7 +234,7 @@ Logger.prototype.info = function info() { Logger.prototype.debug = function debug() { var i, args; - if (this.level < Logger.levels.debug) + if (this.level < Logger.levels.DEBUG) return; args = new Array(arguments.length); @@ -191,7 +242,7 @@ Logger.prototype.debug = function debug() { for (i = 0; i < args.length; i++) args[i] = arguments[i]; - this.log('debug', args); + this.log(Logger.levels.DEBUG, args); }; /** @@ -203,7 +254,7 @@ Logger.prototype.debug = function debug() { Logger.prototype.spam = function spam() { var i, args; - if (this.level < Logger.levels.spam) + if (this.level < Logger.levels.SPAM) return; args = new Array(arguments.length); @@ -211,7 +262,7 @@ Logger.prototype.spam = function spam() { for (i = 0; i < args.length; i++) args[i] = arguments[i]; - this.log('spam', args); + this.log(Logger.levels.SPAM, args); }; /** @@ -225,7 +276,8 @@ Logger.prototype.log = function log(level, args) { if (this.closed) return; - assert(Logger.levels[level] != null, 'Invalid log level.'); + if (this.level < level) + return; this.writeConsole(level, args); this.writeStream(level, args); @@ -238,12 +290,15 @@ Logger.prototype.log = function log(level, args) { */ Logger.prototype.writeConsole = function writeConsole(level, args) { + var name = Logger.levelsByVal[level]; var prefix, msg, color; + assert(name, 'Invalid log level.'); + if (!this.console) return; - prefix = '[' + level + '] '; + prefix = '[' + name + '] '; if (util.isBrowser) { msg = typeof args[0] !== 'object' @@ -252,7 +307,7 @@ Logger.prototype.writeConsole = function writeConsole(level, args) { msg = prefix + msg; - return level === 'error' + return level === Logger.levels.ERROR ? console.error(msg) : console.log(msg); } @@ -264,7 +319,7 @@ Logger.prototype.writeConsole = function writeConsole(level, args) { msg = prefix + util.format(args, this.colors); - return level === 'error' + return level === Logger.levels.ERROR ? process.stderr.write(msg + '\n') : process.stdout.write(msg + '\n'); }; @@ -276,8 +331,11 @@ Logger.prototype.writeConsole = function writeConsole(level, args) { */ Logger.prototype.writeStream = function writeStream(level, args) { + var name = Logger.levelsByVal[level]; var prefix, msg; + assert(name, 'Invalid log level.'); + if (this.closed) return; @@ -294,7 +352,7 @@ Logger.prototype.writeStream = function writeStream(level, args) { this.stream.on('error', function() {}); } - prefix = '[' + level + '] '; + prefix = '[' + name + '] '; msg = prefix + util.format(args, false); msg = '(' + util.date() + '): ' + msg + '\n'; @@ -322,9 +380,9 @@ Logger.prototype._error = function error(err) { msg = (err.message + '').replace(/^ *Error: */, ''); - this.log('error', [msg]); + this.log(Logger.levels.ERROR, [msg]); - if (this.level >= Logger.levels.debug) { + if (this.level >= Logger.levels.DEBUG) { if (this.stream) this.stream.write(err.stack + '\n'); } diff --git a/lib/node/node.js b/lib/node/node.js index 91dd34fc..933ec3c7 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -53,6 +53,8 @@ function Node(options) { // Local client for walletdb this.client = new NodeClient(this); + this.startTime = -1; + this._bound = []; this.__init(); @@ -79,7 +81,13 @@ Node.prototype.__init = function __init() { this.on('preopen', function() { self._onOpen(); }); + + this.on('open', function() { + self.startTime = util.now(); + }); + this.on('close', function() { + self.startTime = -1; self._onClose(); }); }; @@ -241,6 +249,18 @@ Node.prototype.location = function location(name) { return path; }; +/** + * Get node uptime in seconds. + * @returns {Number} + */ + +Node.prototype.uptime = function uptime() { + if (this.startTime === -1) + return 0; + + return util.now() - this.startTime; +}; + /** * Open and ensure primary wallet. * @returns {Promise} diff --git a/lib/node/nodeclient.js b/lib/node/nodeclient.js index 4a4e0598..8e634ca0 100644 --- a/lib/node/nodeclient.js +++ b/lib/node/nodeclient.js @@ -24,7 +24,6 @@ function NodeClient(node) { this.node = node; this.network = node.network; - this.onError = node._error.bind(node); this.filter = null; this.listen = false; @@ -124,7 +123,7 @@ NodeClient.prototype.getEntry = co(function* getEntry(hash) { */ NodeClient.prototype.send = function send(tx) { - this.node.sendTX(tx).catch(this.onError); + this.node.sendTX(tx).catch(util.nop); return Promise.resolve(); }; diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 3a09a31a..e29b3588 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -50,11 +50,11 @@ function SPVNode(options) { logger: this.logger, db: this.options.db, location: this.location('spvchain'), + maxFiles: this.options.maxFiles, + cacheSize: this.options.cacheSize, witness: this.options.witness, forceWitness: this.options.forceWitness, useCheckpoints: this.options.useCheckpoints, - maxFiles: this.options.maxFiles, - cacheSize: this.options.cacheSize, spv: true }); @@ -74,8 +74,7 @@ function SPVNode(options) { noDiscovery: this.options.noDiscovery, headers: this.options.headers, selfish: true, - listen: false, - spv: true + listen: false }); this.walletdb = new WalletDB({ @@ -84,12 +83,12 @@ function SPVNode(options) { client: this.client, db: this.options.db, location: this.location('walletdb'), - witness: false, maxFiles: this.options.walletMaxFiles, cacheSize: this.options.walletCacheSize, + witness: false, + useCheckpoints: this.options.useCheckpoints, startHeight: this.options.startHeight, wipeNoReally: this.options.wipeNoReally, - resolution: true, verify: true, spv: true }); @@ -101,8 +100,8 @@ function SPVNode(options) { node: this, key: this.options.sslKey, cert: this.options.sslCert, - port: this.options.httpPort || this.network.rpcPort, - host: this.options.httpHost || '0.0.0.0', + port: this.options.httpPort, + host: this.options.httpHost, apiKey: this.options.apiKey, serviceKey: this.options.serviceKey, walletAuth: this.options.walletAuth, diff --git a/lib/utils/lru.js b/lib/utils/lru.js index a7bffd30..15bc99e9 100644 --- a/lib/utils/lru.js +++ b/lib/utils/lru.js @@ -21,18 +21,19 @@ function LRU(capacity, getSize) { if (!(this instanceof LRU)) return new LRU(capacity, getSize); - assert(typeof capacity === 'number', 'Max size must be a number.'); - assert(!getSize || typeof getSize === 'function', 'Bad size callback.'); - - this.capacity = capacity; - this.getSize = getSize; - this.map = Object.create(null); this.size = 0; this.items = 0; this.head = null; this.tail = null; this.pending = null; + + assert(typeof capacity === 'number', 'Capacity must be a number.'); + assert(capacity >= 0, 'Capacity cannot be negative.'); + assert(!getSize || typeof getSize === 'function', 'Bad size callback.'); + + this.capacity = capacity; + this.getSize = getSize; } /** @@ -116,6 +117,9 @@ LRU.prototype.reset = function reset() { LRU.prototype.set = function set(key, value) { var item; + if (this.capacity === 0) + return; + key = key + ''; item = this.map[key]; @@ -151,6 +155,9 @@ LRU.prototype.set = function set(key, value) { LRU.prototype.get = function get(key) { var item; + if (this.capacity === 0) + return; + key = key + ''; item = this.map[key]; @@ -171,6 +178,8 @@ LRU.prototype.get = function get(key) { */ LRU.prototype.has = function get(key) { + if (this.capacity === 0) + return false; return this.map[key] != null; }; @@ -183,6 +192,9 @@ LRU.prototype.has = function get(key) { LRU.prototype.remove = function remove(key) { var item; + if (this.capacity === 0) + return; + key = key + ''; item = this.map[key]; @@ -387,6 +399,10 @@ LRU.prototype.commit = function commit() { LRU.prototype.push = function push(key, value) { assert(this.pending); + + if (this.capacity === 0) + return; + this.pending.set(key, value); }; @@ -397,6 +413,10 @@ LRU.prototype.push = function push(key, value) { LRU.prototype.unpush = function unpush(key) { assert(this.pending); + + if (this.capacity === 0) + return; + this.pending.remove(key); }; @@ -418,6 +438,7 @@ function LRUItem(key, value) { /** * LRU Batch * @constructor + * @param {LRU} lru */ function LRUBatch(lru) { @@ -425,18 +446,37 @@ function LRUBatch(lru) { this.ops = []; } +/** + * Push an item onto the batch. + * @param {String} key + * @param {Object} value + */ + LRUBatch.prototype.set = function set(key, value) { this.ops.push(new LRUOp(false, key, value)); }; +/** + * Push a removal onto the batch. + * @param {String} key + */ + LRUBatch.prototype.remove = function remove(key) { - this.ops.push(new LRUOp(true, key)); + this.ops.push(new LRUOp(true, key, null)); }; +/** + * Clear the batch. + */ + LRUBatch.prototype.clear = function clear() { this.ops.length = 0; }; +/** + * Commit the batch. + */ + LRUBatch.prototype.commit = function commit() { var i, op; @@ -455,6 +495,10 @@ LRUBatch.prototype.commit = function commit() { /** * LRU Op * @constructor + * @private + * @param {Boolean} remove + * @param {String} key + * @param {Object} value */ function LRUOp(remove, key, value) { @@ -463,40 +507,8 @@ function LRUOp(remove, key, value) { this.value = value; } -/** - * A null cache. Every method is a NOP. - * @constructor - * @param {Number} size - */ - -function NullCache(size) { - this.capacity = 0; - this.size = 0; - this.items = 0; -} - -NullCache.prototype.set = function set(key, value) {}; -NullCache.prototype.remove = function remove(key) {}; -NullCache.prototype.get = function get(key) {}; -NullCache.prototype.has = function has(key) { return false; }; -NullCache.prototype.reset = function reset() {}; -NullCache.prototype.keys = function keys(key) { return []; }; -NullCache.prototype.values = function values(key) { return []; }; -NullCache.prototype.toArray = function toArray(key) { return []; }; -NullCache.prototype.batch = function batch() { return new LRUBatch(this); }; -NullCache.prototype.start = function start() {}; -NullCache.prototype.clear = function clear() {}; -NullCache.prototype.drop = function drop() {}; -NullCache.prototype.commit = function commit() {}; -NullCache.prototype.push = function push(key, value) {}; -NullCache.prototype.unpush = function unpush(key) {}; - /* * Expose */ -exports = LRU; -exports.LRU = LRU; -exports.Nil = NullCache; - -module.exports = exports; +module.exports = LRU; diff --git a/lib/utils/util.js b/lib/utils/util.js index 8fd51a00..96cfaeb4 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -125,17 +125,6 @@ util.isBase58 = function isBase58(obj) { return typeof obj === 'string' && /^[1-9a-zA-Z]+$/.test(obj); }; -/** - * Return uptime (shim for browser). - * @returns {Number} - */ - -util.uptime = function uptime() { - if (!process.uptime) - return 0; - return process.uptime(); -}; - /** * Return hrtime (shim for browser). * @param {Array} time diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 0a517a75..ae889f3e 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -54,27 +54,21 @@ function WalletDB(options) { if (!(this instanceof WalletDB)) return new WalletDB(options); - if (!options) - options = {}; - AsyncObject.call(this); - this.options = options; - this.network = Network.get(options.network); - this.logger = options.logger || Logger.global; - this.spv = options.spv || false; - this.client = options.client; - this.onError = this._onError.bind(this); + this.options = new WalletOptions(options); + + this.network = this.options.network; + this.logger = this.options.logger; + this.client = this.options.client; + this.db = LDB(this.options); this.state = new ChainState(); + this.wallets = Object.create(null); this.depth = 0; - this.wallets = {}; - this.keepBlocks = this.network.block.keepBlocks; this.rescanning = false; this.bound = false; - // We need one read lock for `get` and `create`. - // It will hold locks specific to wallet ids. this.readLock = new Lock.Mapped(); this.writeLock = new Lock(); this.txLock = new Lock(); @@ -82,22 +76,7 @@ function WalletDB(options) { this.widCache = new LRU(10000); this.pathMapCache = new LRU(100000); - // Try to optimize for up to 1m addresses. - // We use a regular bloom filter here - // because we never want members to - // lose membership, even if quality - // degrades. - // Memory used: 1.7mb - this.filter = Bloom.fromRate(1000000, 0.001, this.spv ? 1 : -1); - - this.db = LDB({ - location: this.options.location, - db: this.options.db, - maxFiles: this.options.maxFiles, - cacheSize: this.options.cacheSize, - compression: true, - bufferKeys: !util.isBrowser - }); + this.filter = Bloom.fromRate(1000000, 0.001, this.options.spv ? 1 : -1); } util.inherits(WalletDB, AsyncObject); @@ -154,16 +133,6 @@ WalletDB.prototype._close = co(function* close() { yield this.db.close(); }); -/** - * Emit an error. - * @private - * @returns {Promise} - */ - -WalletDB.prototype._onError = function onError(err) { - this.emit('error', err); -}; - /** * Load the walletdb. * @returns {Promise} @@ -202,25 +171,41 @@ WalletDB.prototype.bind = function bind() { self.emit('error', err); }); - this.client.on('block connect', function(entry, txs) { - self.addBlock(entry, txs).catch(self.onError); - }); + this.client.on('block connect', co(function* (entry, txs) { + try { + yield self.addBlock(entry, txs); + } catch (e) { + self.emit('error', e); + } + })); - this.client.on('block disconnect', function(entry) { - self.removeBlock(entry).catch(self.onError); - }); + this.client.on('block disconnect', co(function* (entry) { + try { + yield self.removeBlock(entry); + } catch (e) { + self.emit('error', e); + } + })); this.client.on('block rescan', cob(function* (entry, txs) { yield self.rescanBlock(entry, txs); })); - this.client.on('tx', function(tx) { - self.addTX(tx).catch(self.onError); - }); + this.client.on('tx', co(function* (tx) { + try { + yield self.addTX(tx); + } catch (e) { + self.emit('error', e); + } + })); - this.client.on('chain reset', function(tip) { - self.resetChain(tip).catch(self.onError); - }); + this.client.on('chain reset', co(function* (tip) { + try { + yield self.resetChain(tip); + } catch (e) { + self.emit('error', e); + } + })); }; /** @@ -1621,8 +1606,8 @@ WalletDB.prototype.syncState = co(function* syncState(tip) { height = state.height; blocks = height - tip.height; - if (blocks > this.keepBlocks) - blocks = this.keepBlocks; + if (blocks > this.options.keepBlocks) + blocks = this.options.keepBlocks; for (i = 0; i < blocks; i++) { batch.del(layout.h(height)); @@ -1632,7 +1617,7 @@ WalletDB.prototype.syncState = co(function* syncState(tip) { // Prune old hashes. assert(tip.height === state.height + 1, 'Bad chain sync.'); - height = tip.height - this.keepBlocks; + height = tip.height - this.options.keepBlocks; if (height >= 0) batch.del(layout.h(height)); @@ -2153,6 +2138,125 @@ WalletDB.prototype._resetChain = co(function* resetChain(entry) { yield this.scan(); }); +/** + * WalletOptions + * @constructor + * @param {Object} options + */ + +function WalletOptions(options) { + if (!(this instanceof WalletOptions)) + return new WalletOptions(options); + + this.network = Network.primary; + this.logger = Logger.global; + this.client = null; + + this.location = null; + this.db = 'memory'; + this.maxFiles = 64; + this.cacheSize = 16 << 20; + this.compression = true; + this.bufferKeys = !util.isBrowser; + + this.spv = false; + this.witness = false; + this.useCheckpoints = false; + this.startHeight = 0; + this.keepBlocks = this.network.block.keepBlocks; + this.wipeNoReally = false; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from object. + * @private + * @param {Object} options + * @returns {WalletOptions} + */ + +WalletOptions.prototype.fromOptions = function fromOptions(options) { + if (options.network != null) + this.network = Network.get(options.network); + + this.keepBlocks = this.network.block.keepBlocks; + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.client != null) { + assert(typeof options.client === 'object'); + this.client = options.client; + } + + if (options.location != null) { + assert(typeof options.location === 'string'); + this.location = options.location; + } + + if (options.db != null) { + assert(typeof options.db === 'string'); + this.db = options.db; + } + + if (options.maxFiles != null) { + assert(util.isNumber(options.maxFiles)); + this.maxFiles = options.maxFiles; + } + + if (options.cacheSize != null) { + assert(util.isNumber(options.cacheSize)); + this.cacheSize = options.cacheSize; + } + + if (options.compression != null) { + assert(typeof options.compression === 'boolean'); + this.compression = options.compression; + } + + if (options.spv != null) { + assert(typeof options.spv === 'boolean'); + this.spv = options.spv; + } + + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + + if (options.useCheckpoints != null) { + assert(typeof options.useCheckpoints === 'boolean'); + this.useCheckpoints = options.useCheckpoints; + } + + if (options.startHeight != null) { + assert(typeof options.startHeight === 'number'); + assert(options.startHeight >= 0); + this.startHeight = options.startHeight; + } + + if (options.wipeNoReally != null) { + assert(typeof options.wipeNoReally === 'boolean'); + this.wipeNoReally = options.wipeNoReally; + } + + return this; +}; + +/** + * Instantiate chain options from object. + * @param {Object} options + * @returns {WalletOptions} + */ + +WalletOptions.fromOptions = function fromOptions(options) { + return new WalletOptions().fromOptions(options); +}; + /* * Helpers */ diff --git a/lib/workers/workerpool.js b/lib/workers/workerpool.js index 88a9072b..073a841b 100644 --- a/lib/workers/workerpool.js +++ b/lib/workers/workerpool.js @@ -9,6 +9,8 @@ var assert = require('assert'); var EventEmitter = require('events').EventEmitter; +var os = require('os'); +var cp = require('child_process'); var util = require('../utils/util'); var co = require('../utils/co'); var global = util.global; @@ -17,8 +19,6 @@ var jobs = require('./jobs'); var Parser = require('./parser'); var Framer = require('./framer'); var packets = require('./packets'); -var os = require('os'); -var cp = require('child_process'); /** * A worker pool. @@ -39,14 +39,13 @@ function WorkerPool(options) { EventEmitter.call(this); - if (!options) - options = {}; - - this.size = Math.max(1, options.size || WorkerPool.CORES); - this.timeout = options.timeout || 60000; + this.size = WorkerPool.CORES; + this.timeout = 60000; this.children = []; this.nonce = 0; this.enabled = true; + + this.set(options); } util.inherits(WorkerPool, EventEmitter); @@ -88,7 +87,13 @@ WorkerPool.cleanup = function cleanup() { WorkerPool.children.pop().destroy(); }; -WorkerPool._exitBound = false; +/** + * Whether exit events have been bound globally. + * @private + * @type {Boolean} + */ + +WorkerPool.bound = false; /** * Bind to process events in @@ -96,14 +101,14 @@ WorkerPool._exitBound = false; * @private */ -WorkerPool._bindExit = function _bindExit() { +WorkerPool.bindExit = function bindExit() { if (util.isBrowser) return; - if (WorkerPool._exitBound) + if (WorkerPool.bound) return; - WorkerPool._exitBound = true; + WorkerPool.bound = true; function onSignal() { WorkerPool.cleanup(); @@ -143,6 +148,33 @@ WorkerPool._bindExit = function _bindExit() { }); }; +/** + * Set worker pool options. + * @param {Object} options + */ + +WorkerPool.prototype.set = function set(options) { + if (!options) + return; + + if (options.enabled != null) { + assert(typeof options.enabled === 'boolean'); + this.enabled = options.enabled; + } + + if (options.size != null) { + assert(util.isNumber(options.size)); + assert(options.size > 0); + this.size = options.size; + } + + if (options.timeout != null) { + assert(util.isNumber(options.timeout)); + assert(options.timeout > 0); + this.timeout = options.timeout; + } +}; + /** * Spawn a new worker. * @param {Number} id - Worker ID. @@ -541,7 +573,7 @@ Worker.prototype._bind = function _bind() { WorkerPool.children.push(this); - WorkerPool._bindExit(); + WorkerPool.bindExit(); }; /** @@ -828,31 +860,24 @@ function getCores() { if (os.unsupported) return 2; - return os.cpus().length; + return Math.max(1, os.cpus().length); } /* - * Default + * Default Pool */ exports.pool = new WorkerPool(); exports.pool.enabled = false; exports.set = function set(options) { - if (typeof options.useWorkers === 'boolean') - this.pool.enabled = options.useWorkers; - - if (util.isNumber(options.maxWorkers)) - this.pool.size = options.maxWorkers; - - if (util.isNumber(options.workerTimeout)) - this.pool.timeout = options.workerTimeout; + this.pool.set(options); }; exports.set({ useWorkers: +process.env.BCOIN_USE_WORKERS === 1, - maxWorkers: +process.env.BCOIN_MAX_WORKERS, - workerTimeout: +process.env.BCOIN_WORKER_TIMEOUT + maxWorkers: +process.env.BCOIN_MAX_WORKERS || null, + workerTimeout: +process.env.BCOIN_WORKER_TIMEOUT || null }); /* diff --git a/test/http-test.js b/test/http-test.js index bec6361b..c3e7a7db 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -45,7 +45,7 @@ describe('HTTP', function() { var info = yield wallet.client.getInfo(); assert.equal(info.network, node.network.type); assert.equal(info.version, USER_VERSION); - assert.equal(info.pool.agent, node.pool.userAgent); + assert.equal(info.pool.agent, node.pool.options.agent); assert.equal(typeof info.chain, 'object'); assert.equal(info.chain.height, 0); }));