diff --git a/bin/node b/bin/node index 970dece1..c1ee7b91 100755 --- a/bin/node +++ b/bin/node @@ -6,59 +6,34 @@ var bcoin = require('../'); var utils = bcoin.utils; var assert = utils.assert; +var options = bcoin.config({ + config: true, + arg: true, + env: true, + logLevel: 'debug', + logFile: true, + db: 'leveldb', + listen: true +}); + +bcoin.set(options); + +var node = new bcoin.fullnode(options); + +node.on('error', function(err) { + ; +}); + process.on('uncaughtException', function(err) { node.logger.debug(err.stack); node.logger.error(err); process.exit(1); }); -var fast = process.argv.indexOf('--fast') !== -1; - -var node = new bcoin.fullnode({ - logLevel: 'debug', - logFile: true, - db: 'leveldb', - prune: process.argv.indexOf('--prune') !== -1, - compact: process.argv.indexOf('--compact') !== -1, - useCheckpoints: fast || process.argv.indexOf('--checkpoints') !== -1, - coinCache: fast || process.argv.indexOf('--coin-cache') !== -1, - selfish: process.argv.indexOf('--selfish') !== -1, - headers: fast || process.argv.indexOf('--headers') !== -1, - parallel: process.argv.indexOf('--parallel') !== -1, - bip151: process.argv.indexOf('--bip151') !== -1 -}); - -node.on('error', function(err) { - ; -}); - node.open(function(err) { if (err) throw err; - node.listen(function(err) { - if (err) - throw err; - - if (process.argv.indexOf('--mine') === -1) { - node.startSync(); - return; - } - - if (node.network.type === 'regtest') { - node.miner.start(); - node.startSync(); - return; - } - - node.pool.connect(); - node.startSync(); - - if (node.chain.isFull()) { - node.miner.start(); - return; - } - - node.chain.once('full', node.miner.start.bind(node.miner)); - }); + node.pool.connect(); + node.startSync(); }); diff --git a/bin/spvnode b/bin/spvnode index b02d4687..0c4aed85 100755 --- a/bin/spvnode +++ b/bin/spvnode @@ -6,15 +6,19 @@ var bcoin = require('../'); var utils = bcoin.utils; var assert = utils.assert; -var node = bcoin.spvnode({ +var options = bcoin.config({ + config: true, + arg: true, + env: true, logLevel: 'debug', logFile: true, - db: 'leveldb', - useCheckpoints: process.argv.indexOf('--checkpoints') !== -1, - headers: process.argv.indexOf('--headers') !== -1, - bip151: process.argv.indexOf('--bip151') !== -1 + db: 'leveldb' }); +bcoin.set(options); + +var node = bcoin.spvnode(options); + node.on('error', function(err) { ; }); diff --git a/etc/sample.conf b/etc/sample.conf new file mode 100644 index 00000000..b0ee916c --- /dev/null +++ b/etc/sample.conf @@ -0,0 +1,56 @@ +# Default bcoin config file (~/.bcoin/bcoin.conf) + +# Options +network: main +use-workers: true +# max-workers: 4 +# worker-timeout: 5000 +# sigcache-size: 50000 + +# Logger +log-level: debug +log-console: true +log-file: true + +# Node +# prefix: ~/.bcoin +db: leveldb +# fast: false + +# Chain +# witness: true +# prune: false +use-checkpoints: true +coin-cache: true + +# Mempool +# limit-free: true +# limit-free-relay: 15 +# reject-insane-fees: true +# replace-by-fee: false + +# Pool +# selfish: false +headers: true +compact: true +bip151: true +# proxy-server: localhost +# preferred-seed: +# ignore-discovery: false +# port: 8333 +listen: true + +# Miner +# payout-address: +# coinbase-flags: Mined by bcoin +# parallel: false + +# HTTP +# ssl-cert: /path/to/cert +# ssl-key: /path/to/key +# http-port: 8332 +# http-host: 0.0.0.0 +# api-key: 74b4147957813b62cc8987f2b711ddb31f8cb46dcbf71502033da66053c8780a +# wallet-auth: true +# rpc-user: admin +# rpc-password: bcoin diff --git a/lib/bcoin/config.js b/lib/bcoin/config.js new file mode 100644 index 00000000..081eb5e1 --- /dev/null +++ b/lib/bcoin/config.js @@ -0,0 +1,423 @@ +/*! + * config.js - bcoin configuration + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var bcoin = require('./env'); +var utils = require('./utils'); +var assert = utils.assert; +var fs; + +if (!utils.isBrowser) + fs = require('f' + 's'); + +/** + * @exports config + */ + +function config(options) { + return config.parse(options); +} + +/** + * Option name aliases. + * @const {Object} + */ + +config.alias = { + conf: {}, + env: { + 'seed': 'preferredseed' + }, + arg: { + 'seed': 'preferredseed' + } +}; + +/** + * Parse options and potentially env, args, and config. + * @param {Object} options + * @returns {Object} + */ + +config.parse = function parse(options) { + var env = {}; + var arg = {}; + var text = {}; + var data = {}; + var prefix, cfg; + + if (!options) + options = {}; + + if (options.env) + env = config.parseEnv(); + + if (options.arg) + arg = config.parseArg(); + + merge(data, options); + merge(data, env); + merge(data, arg); + + if (data.config) { + if (typeof data.config === 'string') { + text = config.parseFile(data.config); + } else { + prefix = config.getPrefix(data); + text = config.parseFile(prefix + '/bcoin.conf'); + } + } + + merge(text, data); + + return text; +}; + +/** + * Grab prefix from env, args, and options. + * @param {Object} env + * @param {Object} arg + * @param {Object} options + */ + +config.getPrefix = function getPrefix(data) { + var prefix = data.prefix; + var network; + + if (!prefix) + prefix = utils.HOME + '/.bcoin'; + + network = bcoin.network.get(data.network).type; + + prefix = utils.normalize(prefix); + + if (network !== 'main') + prefix += '/' + network; + + return prefix; +}; + +/** + * Enforce types on parsed data. + * @param {Object} data + */ + +config.parseData = function parseData(data) { + var options = {}; + + // Config + options.config = path(data.config); + + // Options + options.network = str(data.network); + options.useWorkers = bool(data.useworkers); + options.maxWorkers = num(data.maxworkers); + options.workerTimeout = num(data.workertimeout); + options.sigcacheSize = num(data.sigcachesize); + + // Logger + options.logLevel = str(data.loglevel); + options.logConsole = bool(data.logconsole); + options.logFile = boolstr(data.logfile); + + // Node + options.prefix = path(data.prefix); + options.db = str(data.db); + options.fast = bool(data.fast); + + // Chain + options.witness = bool(data.witness); + options.prune = bool(data.prune); + options.useCheckpoints = bool(data.usecheckpoints); + options.coinCache = bool(data.coincache); + + // Mempool + options.limitFree = bool(data.limitfree); + options.limitFreeRelay = bool(data.limitfreerelay); + options.requireStandard = bool(data.requirestandard); + options.rejectInsaneFees = bool(data.rejectinsanefees); + options.replaceByFee = bool(data.replacebyfee); + + // Pool + options.selfish = bool(data.selfish); + options.headers = bool(data.headers); + options.compact = bool(data.compact); + options.bip151 = bool(data.bip151); + options.proxyServer = str(data.proxyserver); + options.preferredSeed = str(data.preferredseed); + options.maxPeers = num(data.maxpeers); + options.maxLeeches = num(data.maxleeches); + options.ignoreDiscovery = bool(data.ignorediscovery); + options.port = num(data.port); + options.listen = bool(data.listen); + + // Miner + options.payoutAddress = str(data.payoutaddress); + options.coinbaseFlags = str(data.coinbaseflags); + options.parallel = bool(data.parallel); + + // HTTP + options.sslCert = file(data.sslcert); + options.sslKey = file(data.sslkey); + options.httpPort = num(data.httpport); + options.httpHost = str(data.httphost); + options.apiKey = str(data.apikey); + options.walletAuth = bool(data.walletauth); + options.rpcUser = str(data.rpcuser); + options.rpcPassword = str(data.rpcpassword); + + return options; +}; + +/** + * Parse config file. + * @param {String} file + * @returns {Object} + */ + +config.parseFile = function parseFile(file) { + return config.parseText(readFile(file)); +}; + +/** + * Parse config text. + * @param {String} text + * @returns {Object} + */ + +config.parseText = function parseText(text) { + var data = {}; + var i, parts, line, pair, key, value, eq, col, alias; + + assert(typeof text === 'string', 'Config must be text.'); + + text = text.trim(); + parts = text.split(/\n+/); + + for (i = 0; i < parts.length; i++) { + line = parts[i].trim(); + + if (line.length === 0) + continue; + + if (/^\s*#/.test(line)) + continue; + + eq = line.indexOf('='); + col = line.indexOf(':'); + + if (col !== -1 && (col < eq || eq === -1)) + eq = col; + + if (eq === -1) { + key = line.trim(); + value = null; + } else { + key = line.slice(0, eq).trim(); + value = line.slice(eq + 1).trim(); + } + + key = key.replace(/\-/g, '').toLowerCase(); + alias = config.alias.conf[key]; + + if (alias) + key = alias; + + if (value.length === 0) + value = null; + + data[key] = value; + } + + return config.parseData(data); +}; + +/** + * Parse arguments. + * @param {Array?} argv + * @returns {Object} + */ + +config.parseArg = function parseArg(argv) { + var data = {}; + var arg, key, value, alias; + + if (!argv) { + if (utils.isBrowser) + return data; + argv = process.argv; + } + + argv = argv.slice(); + + while (argv.length) { + arg = argv.shift(); + + if (arg.indexOf('--') === 0) { + // e.g. --opt + arg = arg.split('='); + key = arg[0]; + if (arg.length > 1) { + // e.g. --opt=val + value = arg.slice(1).join('=').trim(); + } else { + value = 'true'; + } + key = key.replace(/\-/g, ''); + data[key] = value; + continue; + } + + if (arg[0] === '-') { + // e.g. -abc + arg = arg.substring(1).split(''); + + for (i = 0; i < arg.length; i++) { + key = arg[i].trim(); + alias = config.alias.arg[key]; + if (alias) + key = alias; + data[key] = 'true'; + } + + continue; + } + + // e.g. foo + if (key) + data[key] = arg; + } + + return config.parseData(data); +}; + +/** + * Parse environment variables. + * @param {Object} env + * @returns {Object} + */ + +config.parseEnv = function parseEnv(env) { + var data = {}; + var i, keys, key, value, alias; + + if (!env) + env = process.env; + + keys = Object.keys(env); + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + + if (key.indexOf('BCOIN_') !== 0) + continue; + + value = env[key]; + + key = key.substring(6); + key = key.replace(/_/g, '').toLowerCase(); + alias = config.alias.env[key]; + + if (alias) + key = alias; + + data[key] = value; + } + + return config.parseData(data); +}; + +/* + * Helpers + */ + +function str(value) { + if (!value) + return null; + return value; +} + +function path(value) { + if (!value) + return null; + return utils.normalize(value.replace(/^~/, utils.HOME)); +} + +function bool(value) { + if (!value) + return null; + return value === 'true' || value === '1'; +} + +function num(value) { + if (value == null) + return null; + + value = parseInt(value, 10); + + if (!isFinite(value)) + return null; + + return value; +} + +function boolstr(value) { + if (!value) + return null; + + if (value === 'true' || value === '1') + return true; + + if (value === 'false' || value === '0') + return false; + + return path(value); +} + +function file(value) { + if (!fs) + return null; + + if (!value) + return null; + + try { + return fs.readFileSync(path(value)); + } catch (e) { + return null; + } +} + +function readFile(file) { + if (!fs) + return ''; + + try { + return fs.readFileSync(file, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') + return ''; + throw e; + } +} + +function merge(a, b) { + var keys = Object.keys(b); + var i, key; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + if (b[key] != null) + a[key] = b[key]; + } +} + +/* + * Expose + */ + +module.exports = config; diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index 2d978f61..ccc305f0 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -126,6 +126,7 @@ function Environment() { this.lowlevelup = require('./lowlevelup'); this.uri = require('./uri'); this.logger = require('./logger'); + this.config = require('./config'); this.protocol = require('./protocol'); this.packets = this.protocol.packets; @@ -224,7 +225,7 @@ Environment.prototype.set = function set(options) { } if (utils.isNumber(options.sigcacheSize)) - this.sigcache.size = options.sigcacheSize; + this.sigcache.resize(options.sigcacheSize); return this; }; diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 52af83cb..ee148072 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -61,7 +61,7 @@ function Fullnode(options) { location: this.location('chain'), preload: false, spv: false, - witness: this.network.witness, + witness: this.options.witness, prune: this.options.prune, useCheckpoints: this.options.useCheckpoints, coinCache: this.options.coinCache @@ -94,7 +94,7 @@ function Fullnode(options) { logger: this.logger, chain: this.chain, mempool: this.mempool, - witness: this.network.witness, + witness: this.options.witness, selfish: this.options.selfish, headers: this.options.headers, compact: this.options.compact, @@ -103,6 +103,9 @@ function Fullnode(options) { maxLeeches: this.options.maxLeeches, proxyServer: this.options.proxyServer, preferredSeed: this.options.preferredSeed, + ignoreDiscovery: this.options.ignoreDiscovery, + pool: this.options.port, + listen: this.options.listen, spv: false }); @@ -125,7 +128,7 @@ function Fullnode(options) { fees: this.fees, db: this.db, location: this.location('walletdb'), - witness: this.network.witness, + witness: this.options.witness, useCheckpoints: this.options.useCheckpoints, verify: false }); diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index f8ab7f9b..24630629 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -169,33 +169,6 @@ Node.prototype._parseOptions = function _parseOptions(options) { options.network = bcoin.network.get(options.network); - if (process.env.BCOIN_PREFIX != null) - options.prefix = process.env.BCOIN_PREFIX; - - if (process.env.BCOIN_DB != null) - options.db = process.env.BCOIN_DB; - - if (process.env.BCOIN_LOGLEVEL != null) - options.logLevel = process.env.BCOIN_LOGLEVEL; - - if (process.env.BCOIN_LOGFILE != null) { - if (process.env.BCOIN_LOGFILE === '0' - || process.env.BCOIN_LOGFILE === '1') { - options.logFile = +process.env.BCOIN_LOGFILE === 1; - } else { - options.logFile = process.env.BCOIN_LOGFILE; - } - } - - if (process.env.BCOIN_SEED != null) - options.preferredSeed = process.env.BCOIN_SEED; - - if (process.env.BCOIN_RPCUSER != null) - options.rpcUser = process.env.BCOIN_RPCUSER; - - if (process.env.BCOIN_RPCPASSWORD != null) - options.rpcPassword = process.env.BCOIN_RPCPASSWORD; - if (!options.prefix) options.prefix = utils.HOME + '/.bcoin'; @@ -215,6 +188,15 @@ Node.prototype._parseOptions = function _parseOptions(options) { ? utils.normalize(options.logFile) : null; + if (options.fast) { + options.headers = true; + options.useCheckpoints = true; + options.coinCache = true; + } + + if (options.witness == null) + options.witness = options.network.witness; + return options; }; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 4f561314..b7cee958 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -48,7 +48,7 @@ var InvItem = bcoin.packets.InvItem; * transactions accepted to our mempool. * @param {Boolean?} options.witness - Request witness blocks and transactions. * Only deal with witness peers. - * @param {Boolean} [options.discoverPeers=true] Automatically discover new + * @param {Boolean} options.ignoreDiscovery - Automatically discover new * peers. * @param {String[]} options.seeds * @param {Function?} options.createSocket - Custom function to create a socket. @@ -127,11 +127,15 @@ function Pool(options) { if (!this.options.witness) this.services &= ~constants.services.WITNESS; + this.port = this.options.port != null + ? this.options.port + : this.network.port; + this.address = new NetworkAddress({ ts: utils.now() - (process.uptime() | 0), services: this.services, host: '0.0.0.0', - port: this.network.port + port: this.port }); this.server = null; @@ -307,10 +311,22 @@ Pool.prototype._open = function open(callback) { self.logger.info('External IP found: %s.', ip); } - if (self.mempool) - self.mempool.open(callback); - else - self.chain.open(callback); + function open(callback) { + if (self.mempool) + self.mempool.open(callback); + else + self.chain.open(callback); + } + + open(function(err) { + if (err) + return callback(err); + + if (!self.options.listen) + return callback(); + + self.listen(callback); + }); }); }; @@ -465,7 +481,7 @@ Pool.prototype.listen = function listen(callback) { data.address, data.port); }); - this.server.listen(this.network.port, '0.0.0.0', callback); + this.server.listen(this.port, '0.0.0.0', callback); }; /** @@ -1090,7 +1106,7 @@ Pool.prototype._createPeer = function _createPeer(options) { peer.on('addr', function(hosts) { var i, host; - if (self.options.discoverPeers === false) + if (self.options.ignoreDiscovery) return; for (i = 0; i < hosts.length; i++) { diff --git a/lib/bcoin/sigcache.js b/lib/bcoin/sigcache.js index 6d0bd062..3d12729e 100644 --- a/lib/bcoin/sigcache.js +++ b/lib/bcoin/sigcache.js @@ -34,6 +34,20 @@ function SigCache(size) { this.valid = {}; } +/** + * Resize the sigcache. + * @param {Number} size + */ + +SigCache.prototype.resize = function resize(size) { + assert(utils.isNumber(size)); + assert(size >= 0); + + this.size = size; + this.keys.length = 0; + this.valid = {}; +}; + /** * Add item to the sigcache. * Potentially evict a random member. diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js index 7510b4f6..cf327220 100644 --- a/lib/bcoin/spvnode.js +++ b/lib/bcoin/spvnode.js @@ -46,7 +46,7 @@ function SPVNode(options) { profiler: this.profiler, db: this.db, location: this.location('spvchain'), - witness: this.network.witness, + witness: this.options.witness, useCheckpoints: this.options.useCheckpoints, spv: true }); @@ -55,12 +55,12 @@ function SPVNode(options) { network: this.network, logger: this.logger, chain: this.chain, - witness: this.network.witness, + witness: this.options.witness, proxyServer: this.options.proxyServer, preferredSeed: this.options.preferredSeed, maxPeers: this.options.maxPeers, + ignoreDiscovery: this.options.ignoreDiscovery, selfish: true, - listen: false, spv: true }); @@ -69,7 +69,7 @@ function SPVNode(options) { logger: this.logger, db: this.db, location: this.location('walletdb'), - witness: this.network.witness, + witness: this.options.witness, verify: true });