diff --git a/index.js b/index.js index 49938a63..7129c70e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = require('./lib/node'); +module.exports = require('./lib'); module.exports.daemon = require('./lib/daemon'); module.exports.Node = require('./lib/node'); module.exports.Block = require('./lib/block'); @@ -22,6 +22,3 @@ module.exports.scaffold.defaultConfig = require('./lib/scaffold/default-config') module.exports.cli = {}; module.exports.cli.main = require('./cli/main'); - -module.exports.deps = {}; -module.exports.deps.chainlib = require('chainlib'); diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 4bfbecb5..0832e89f 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -5,9 +5,9 @@ // functionality by including the wallet in the build. // To run the tests: $ mocha -R spec integration/regtest-node.js -var chainlib = require('chainlib'); +var index = require('..'); var async = require('async'); -var log = chainlib.log; +var log = index.log; log.debug = function() {}; if (process.env.BITCORENODE_ENV !== 'test') { diff --git a/integration/regtest.js b/integration/regtest.js index 468d5517..62d66803 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -5,8 +5,8 @@ // functionality by including the wallet in the build. // To run the tests: $ mocha -R spec integration/regtest.js -var chainlib = require('chainlib'); -var log = chainlib.log; +var index = require('..'); +var log = index.log; if (process.env.BITCORENODE_ENV !== 'test') { log.info('Please set the environment variable BITCORENODE_ENV=test and make sure bindings are compiled for testing'); diff --git a/lib/block.js b/lib/block.js index 4d36af54..7a5b39a6 100644 --- a/lib/block.js +++ b/lib/block.js @@ -1,31 +1,60 @@ 'use strict'; -var util = require('util'); -var chainlib = require('chainlib'); -var BaseBlock = chainlib.Block; var bitcore = require('bitcore'); var BufferReader = bitcore.encoding.BufferReader; -var BN = bitcore.crypto.BN; +var BufferWriter = bitcore.encoding.BufferWriter; +var Hash = bitcore.crypto.Hash; + +//TODO: use bitcore.Block function Block(obj) { - if (!obj) { - obj = {}; + /* jshint maxstatements: 25 */ + if (!(this instanceof Block)) { + return new Block(obj); } - BaseBlock.call(this, obj); + this.version = obj.version || 1; + this.prevHash = obj.prevHash; + + if (!obj.hasOwnProperty('prevHash')) { + throw new TypeError('"prevHash" is expected'); + } + if (!obj.timestamp) { + throw new TypeError('"timestamp" is expected'); + } + this.timestamp = obj.timestamp; + if (typeof this.timestamp === 'string') { + this.timestamp = new Date(obj.timestamp); + } + + this.merkleRoot = obj.merkleRoot; + + if (obj.data) { + if (!Buffer.isBuffer(obj.data)) { + throw new TypeError('"data" is expected to be a buffer'); + } + this.data = obj.data; + } else { + this.data = new Buffer(0); + } + + var hashProperty = { + configurable: false, + enumerable: true, + get: function() { + return this.getHash(); + }, + set: function() {} + }; + + Object.defineProperty(this, 'hash', hashProperty); this.bits = obj.bits; this.nonce = obj.nonce || 0; + return this; } -util.inherits(Block, BaseBlock); - -Block.prototype.validate = function(chain, callback) { - // bitcoind does all validation - setImmediate(callback); -}; - Block.fromBuffer = function(buffer) { var br = new BufferReader(buffer); return Block.fromBufferReader(br); @@ -48,17 +77,15 @@ Block.fromBufferReader = function(br) { return new Block(obj); }; -Block.prototype.toObject = Block.prototype.toJSON = function() { - return { - hash: this.hash, - version: this.version, - prevHash: this.prevHash, - merkleRoot: this.merkleRoot, - timestamp: this.timestamp.toISOString(), - bits: this.bits, - nonce: this.nonce, - data: this.data.toString('hex') - }; +Block.prototype.validate = function(chain, callback) { + // bitcoind does all validation + setImmediate(callback); +}; + +Block.prototype.headerToBuffer = function() { + var bw = new BufferWriter(); + this.headerToBufferWriter(bw); + return bw.concat(); }; Block.prototype.headerToBufferWriter = function(bw) { @@ -104,4 +131,38 @@ Block.prototype.headerToBufferWriter = function(bw) { }; +Block.prototype.toObject = Block.prototype.toJSON = function() { + return { + hash: this.hash, + version: this.version, + prevHash: this.prevHash, + merkleRoot: this.merkleRoot, + timestamp: this.timestamp.toISOString(), + bits: this.bits, + nonce: this.nonce, + data: this.data.toString('hex') + }; +}; + +Block.prototype.toBufferWriter = function(bw) { + // header + this.headerToBufferWriter(bw); + + // transaction data + bw.write(this.data); + return bw; +}; + +Block.prototype.toBuffer = function() { + var bw = new BufferWriter(); + this.toBufferWriter(bw); + return bw.concat(); +}; + +Block.prototype.getHash = function() { + var hashBuffer = BufferReader(Hash.sha256sha256(this.headerToBuffer())).readReverse(); + var hash = hashBuffer.toString('hex'); + return hash; +}; + module.exports = Block; diff --git a/lib/chain.js b/lib/chain.js index e6c17665..1daf29b6 100644 --- a/lib/chain.js +++ b/lib/chain.js @@ -1,19 +1,16 @@ 'use strict'; var util = require('util'); +var EventEmitter = require('events').EventEmitter; var bitcore = require('bitcore'); -var chainlib = require('chainlib'); -var BaseChain = chainlib.Chain; var BN = bitcore.crypto.BN; +var $ = bitcore.util.preconditions; var Block = require('./block'); +var index = require('./index'); +var log = index.log; +var utils = require('./utils'); -Chain.DEFAULTS = { - MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'), - TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks - TARGET_SPACING: 10 * 60 * 1000, // ten minutes - MAX_BITS: 0x1d00ffff, - MIN_BITS: 0x03000000 -}; +var MAX_STACK_DEPTH = 1000; /** * Will instantiate a new Chain instance @@ -26,38 +23,123 @@ Chain.DEFAULTS = { * @extends BaseChain * @constructor */ -function Chain(options) { - /* jshint maxstatements: 20 */ - /* jshint maxcomplexity: 12 */ +function Chain(opts) { + /* jshint maxstatements: 30 */ if (!(this instanceof Chain)) { - return new Chain(options); + return new Chain(opts); } - if (!options) { - options = {}; + + var self = this; + if(!opts) { + opts = {}; } - BaseChain.call(this, options); - this.minBits = options.minBits || Chain.DEFAULTS.MIN_BITS; - this.maxBits = options.maxBits || Chain.DEFAULTS.MAX_BITS; + this.db = opts.db; + this.p2p = opts.p2p; - this.maxHashes = options.maxHashes || Chain.DEFAULTS.MAX_HASHES; + this.genesis = opts.genesis; + this.genesisOptions = opts.genesisOptions; + this.genesisWeight = new BN(0); + this.tip = null; + this.overrideTip = opts.overrideTip; + this.cache = { + hashes: {}, // dictionary of hash -> prevHash + chainHashes: {} + }; + this.lastSavedMetadata = null; + this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance + this.blockQueue = []; + this.processingBlockQueue = false; + this.builder = opts.builder || false; + this.ready = false; - this.targetTimespan = options.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN; - this.targetSpacing = options.targetSpacing || Chain.DEFAULTS.TARGET_SPACING; + this.on('initialized', function() { + self.initialized = true; + }); - this.node = options.node; + this.on('initialized', this._onInitialized.bind(this)); + + this.on('ready', function() { + log.debug('Chain is ready'); + self.ready = true; + self.startBuilder(); + }); + + this.minBits = opts.minBits || Chain.DEFAULTS.MIN_BITS; + this.maxBits = opts.maxBits || Chain.DEFAULTS.MAX_BITS; + + this.maxHashes = opts.maxHashes || Chain.DEFAULTS.MAX_HASHES; + + this.targetTimespan = opts.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN; + this.targetSpacing = opts.targetSpacing || Chain.DEFAULTS.TARGET_SPACING; + + this.node = opts.node; return this; } -util.inherits(Chain, BaseChain); +util.inherits(Chain, EventEmitter); + +Chain.DEFAULTS = { + MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'), + TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks + TARGET_SPACING: 10 * 60 * 1000, // ten minutes + MAX_BITS: 0x1d00ffff, + MIN_BITS: 0x03000000 +}; + +Chain.prototype._onInitialized = function() { + this.emit('ready'); +}; Chain.prototype.start = function(callback) { this.genesis = Block.fromBuffer(this.node.bitcoind.genesisBuffer); - this.on('initialized', callback); + this.once('initialized', callback); this.initialize(); }; +Chain.prototype.initialize = function() { + var self = this; + + // Does our database already have a tip? + self.db.getMetadata(function getMetadataCallback(err, metadata) { + if(err) { + return self.emit('error', err); + } else if(!metadata || !metadata.tip) { + self.tip = self.genesis; + self.tip.__height = 0; + self.tip.__weight = self.genesisWeight; + self.db.putBlock(self.genesis, function putBlockCallback(err) { + if(err) { + return self.emit('error', err); + } + self.db._onChainAddBlock(self.genesis, function(err) { + if(err) { + return self.emit('error', err); + } + + self.emit('addblock', self.genesis); + self.saveMetadata(); + self.emit('initialized'); + }); + }); + } else { + metadata.tip = metadata.tip; + self.db.getBlock(metadata.tip, function getBlockCallback(err, tip) { + if(err) { + return self.emit('error', err); + } + + self.tip = tip; + self.tip.__height = metadata.tipHeight; + self.tip.__weight = new BN(metadata.tipWeight, 'hex'); + self.cache = metadata.cache; + self.emit('initialized'); + }); + } + }); +}; + Chain.prototype.stop = function(callback) { setImmediate(callback); }; @@ -78,23 +160,6 @@ Chain.prototype.startBuilder = function() { // Unused in bitcoind.js }; -Chain.prototype.buildGenesisBlock = function buildGenesisBlock(options) { - if (!options) { - options = {}; - } - var genesis = new Block({ - prevHash: null, - height: 0, - timestamp: options.timestamp || new Date(), - nonce: options.nonce || 0, - bits: options.bits || this.maxBits - }); - var data = this.db.buildGenesisData(); - genesis.merkleRoot = data.merkleRoot; - genesis.data = data.buffer; - return genesis; -}; - Chain.prototype.getWeight = function getWeight(blockHash, callback) { var self = this; var blockIndex = self.db.bitcoind.getBlockIndex(blockHash); @@ -108,4 +173,90 @@ Chain.prototype.getWeight = function getWeight(blockHash, callback) { }); }; +/** + * Will get an array of hashes all the way to the genesis block for + * the chain based on "block hash" as the tip. + * + * @param {String} block hash - a block hash + * @param {Function} callback - A function that accepts: Error and Array of hashes + */ +Chain.prototype.getHashes = function getHashes(tipHash, callback) { + var self = this; + + $.checkArgument(utils.isHash(tipHash)); + + var hashes = []; + var depth = 0; + + getHashAndContinue(null, tipHash); + + function getHashAndContinue(err, hash) { + if (err) { + return callback(err); + } + + depth++; + + hashes.unshift(hash); + + if (hash === self.genesis.hash) { + // Stop at the genesis block + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else if(self.cache.chainHashes[hash]) { + hashes.shift(); + hashes = self.cache.chainHashes[hash].concat(hashes); + delete self.cache.chainHashes[hash]; + self.cache.chainHashes[tipHash] = hashes; + callback(null, hashes); + } else { + // Continue with the previous hash + // check cache first + var prevHash = self.cache.hashes[hash]; + if(prevHash) { + // Don't let the stack get too deep. Otherwise we will crash. + if(depth >= MAX_STACK_DEPTH) { + depth = 0; + return setImmediate(function() { + getHashAndContinue(null, prevHash); + }); + } else { + return getHashAndContinue(null, prevHash); + } + } else { + // do a db call if we don't have it + self.db.getPrevHash(hash, function(err, prevHash) { + if(err) { + return callback(err); + } + + return getHashAndContinue(null, prevHash); + }); + } + } + } + +}; + +Chain.prototype.saveMetadata = function saveMetadata(callback) { + var self = this; + + callback = callback || function() {}; + + if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) { + return callback(); + } + + var metadata = { + tip: self.tip ? self.tip.hash : null, + tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0, + tipWeight: self.tip && self.tip.__weight ? self.tip.__weight.toString(16) : '0', + cache: self.cache + }; + + self.lastSavedMetadata = new Date(); + + self.db.putMetadata(metadata, callback); +}; + module.exports = Chain; diff --git a/lib/daemon.js b/lib/daemon.js index 24508922..297d02ac 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -1,10 +1,10 @@ 'use strict'; +var util = require('util'); var EventEmitter = require('events').EventEmitter; var bitcoind = require('bindings')('bitcoind.node'); -var chainlib = require('chainlib'); -var log = chainlib.log; -var util = require('util'); +var index = require('./'); +var log = index.log; var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; diff --git a/lib/db.js b/lib/db.js index cfab74f3..723ebf13 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,24 +1,51 @@ 'use strict'; +var EventEmitter = require('events').EventEmitter; var util = require('util'); -var chainlib = require('chainlib'); -var BaseDB = chainlib.DB; -var Transaction = require('./transaction'); var async = require('async'); +var levelup = require('levelup'); +var leveldown = require('leveldown'); var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; +var BufferReader = bitcore.encoding.BufferReader; var BufferWriter = bitcore.encoding.BufferWriter; -var errors = require('./errors'); -var log = chainlib.log; +var index = require('./'); +var errors = index.errors; +var log = index.log; +var Transaction = require('./transaction'); var BaseModule = require('./module'); var AddressModule = require('./modules/address'); function DB(options) { + /* jshint maxstatements: 30 */ + /* jshint maxcomplexity: 20 */ + + if (!(this instanceof DB)) { + return new DB(options); + } if(!options) { options = {}; } - BaseDB.call(this, options); + this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8; + + var levelupStore = leveldown; + + if(options.store) { + levelupStore = options.store; + } else if(!options.path) { + throw new Error('Please include database path in options'); + } + + this.store = levelup(options.path, { db: levelupStore }); + this.chain = options.chain; + this.Block = options.Block || require('./block'); + this.txPrefix = options.txPrefix || DB.PREFIXES.TX; + this.prevHashPrefix = options.prevHashPrefix || DB.PREFIXES.PREV_HASH; + this.blockPrefix = options.blockPrefix || DB.PREFIXES.BLOCK; + this.dataPrefix = options.dataPrefix || DB.PREFIXES.DATA; + this.weightPrefix = options.weightPrefix || DB.PREFIXES.WEIGHT; + this.Transaction = Transaction; this.coinbaseAddress = options.coinbaseAddress; this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8; @@ -40,7 +67,19 @@ function DB(options) { }; } -util.inherits(DB, BaseDB); +DB.PREFIXES = { + TX: 'tx', + PREV_HASH: 'ph', + BLOCK: 'blk', + DATA: 'data', + WEIGHT: 'wt' +}; + +util.inherits(DB, EventEmitter); + +DB.prototype.initialize = function() { + this.emit('ready'); +}; DB.prototype.start = function(callback) { // Add all db option modules @@ -151,48 +190,83 @@ DB.prototype._updateWeight = function(hash, weight, callback) { setImmediate(callback); }; -DB.prototype.buildGenesisData = function() { - var coinbaseTx = this.buildCoinbaseTransaction(); - var bw = new BufferWriter(); - bw.writeVarintNum(1); - bw.write(coinbaseTx.toBuffer()); - var merkleRoot = this.getMerkleRoot([coinbaseTx]); - var buffer = bw.concat(); - return { - merkleRoot: merkleRoot, - buffer: buffer - }; +/** + * Saves metadata to the database + * @param {Object} metadata - The metadata + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.putMetadata = function(metadata, callback) { + this.store.put('metadata', JSON.stringify(metadata), {}, callback); }; -DB.prototype.buildCoinbaseTransaction = function(transactions, data) { - if(!this.coinbaseAddress) { - throw new Error('coinbaseAddress required to build coinbase'); - } +/** + * Retrieves metadata from the database + * @param {Function} callback - A function that accepts: Error and Object + */ +DB.prototype.getMetadata = function(callback) { + var self = this; - if(!data) { - data = bitcore.crypto.Random.getRandomBuffer(40); - } + self.store.get('metadata', {}, function(err, data) { + if(err instanceof levelup.errors.NotFoundError) { + return callback(null, {}); + } else if(err) { + return callback(err); + } - var fees = 0; + var metadata; + try { + metadata = JSON.parse(data); + } catch(e) { + return callback(new Error('Could not parse metadata')); + } - if(transactions && transactions.length) { - fees = this.getInputTotal(transactions) - this.getOutputTotal(transactions, true); - } - - var coinbaseTx = new this.Transaction(); - coinbaseTx.to(this.coinbaseAddress, this.coinbaseAmount + fees); - - var script = bitcore.Script.buildDataOut(data); - - var input = new bitcore.Transaction.Input({ - prevTxId: '0000000000000000000000000000000000000000000000000000000000000000', - outputIndex: 0xffffffff, - sequenceNumber: 4294967295, - script: script + callback(null, metadata); }); +}; - coinbaseTx.inputs = [input]; - return coinbaseTx; +/** + * Closes the underlying store database + * @param {Function} callback - A function that accepts: Error + */ +DB.prototype.close = function(callback) { + this.store.close(callback); +}; + +DB.prototype.addTransactionsToBlock = function addTransactionsToBlock(block, transactions) { + var txs = this.getTransactionsFromBlock(block); + txs = txs.concat(transactions); + var txsBuffer = this.Transaction.manyToBuffer(txs); + var bw = new BufferWriter(); + bw.writeVarintNum(txs.length); + bw.write(txsBuffer); + block.merkleRoot = this.getMerkleRoot(txs); + block.data = bw.concat(); + block.__transactions = txs; +}; + +DB.prototype.getTransactionsFromBlock = function getTransactionsFromBlock(block) { + var self = this; + if (block.data.length === 0) { + return []; + } else if(block.__transactions) { + return block.__transactions; + } + + var br = new BufferReader(block.data); + var count = br.readVarintNum(); + var transactions = []; + for (var i = 0; i < count; i++) { + var tx; + if (self.Transaction.prototype.fromBufferReader) { + tx = self.Transaction().fromBufferReader(br); + } else { + tx = self.Transaction.fromBufferReader(br); + } + transactions.push(tx); + } + + block.__transactions = transactions; + return transactions; }; DB.prototype.getOutputTotal = function(transactions, excludeCoinbase) { @@ -226,8 +300,6 @@ DB.prototype.getInputTotal = function(transactions) { DB.prototype._onChainAddBlock = function(block, callback) { log.debug('DB handling new chain block'); - // Remove block from mempool - this.mempool.removeBlock(block.hash); this.blockHandler(block, true, callback); }; diff --git a/lib/errors.js b/lib/errors.js index 35e1002d..0a1b6a42 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,11 +1,25 @@ 'use strict'; var createError = require('errno').create; -var chainlib = require('chainlib'); -var errors = chainlib.errors; +var BitcoreNodeError = createError('BitcoreNodeError'); +var NoOutputs = createError('NoOutputs', BitcoreNodeError); +var NoOutput = createError('NoOutput', BitcoreNodeError); -errors.Transaction = createError('Transaction', errors.Error); -errors.Transaction.NotFound = createError('NotFound', errors.Transaction); +var Wallet = createError('WalletError', BitcoreNodeError); +Wallet.InsufficientFunds = createError('InsufficientFunds', Wallet); -module.exports = errors; +var Consensus = createError('Consensus', BitcoreNodeError); +Consensus.BlockExists = createError('BlockExists', Consensus); + +var Transaction = createError('Transaction', BitcoreNodeError); +Transaction.NotFound = createError('NotFound', Transaction); + +module.exports = { + Error: BitcoreNodeError, + NoOutputs: NoOutputs, + NoOutput: NoOutput, + Wallet: Wallet, + Consensus: Consensus, + Transaction: Transaction +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..4b02c98b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,3 @@ +var Logger = require('./logger'); +module.exports.errors = require('./errors'); +module.exports.log = new Logger(); diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 00000000..ea997b5a --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,62 @@ +'use strict'; + +var colors = require('colors/safe'); + +/** + * Wraps console.log with some special magic + * @constructor + */ +function Logger(namespace) { + this.namespace = namespace || 'bitcore-node'; +} + +/** + * Prints an info message + * #info + */ +Logger.prototype.info = function() { + this._log.apply(this, ['blue', 'info'].concat(Array.prototype.slice.call(arguments))); +}; + +/** + * Prints an error message + * #error + */ +Logger.prototype.error = function() { + this._log.apply(this, ['red', 'error'].concat(Array.prototype.slice.call(arguments))); +}; + +/** + * Prints an debug message + * #debug + */ +Logger.prototype.debug = function() { + this._log.apply(this, ['magenta', 'debug'].concat(Array.prototype.slice.call(arguments))); +}; + +/** + * Prints an warn message + * #warn + */ +Logger.prototype.warn = function() { + this._log.apply(this, ['yellow', 'warn'].concat(Array.prototype.slice.call(arguments))); +}; + +/** + * Proxies console.log with color and arg parsing magic + * #_log + */ +Logger.prototype._log = function(color, type) { + if (process.env.NODE_ENV === 'test') { + return; + } + + var args = Array.prototype.slice.call(arguments); + args = args.slice(1); + var name = colors.bold('{' + this.namespace + '}'); + var type = colors[color].italic(args.shift() + ':'); + args[0] = name + ' ' + type + ' ' + args[0]; + console.log.apply(console, args); +}; + +module.exports = Logger; diff --git a/lib/modules/address.js b/lib/modules/address.js index c61e6786..73a3899c 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -3,10 +3,10 @@ var BaseModule = require('../module'); var inherits = require('util').inherits; var async = require('async'); -var chainlib = require('chainlib'); -var log = chainlib.log; -var levelup = chainlib.deps.levelup; -var errors = chainlib.errors; +var index = require('../'); +var log = index.log; +var levelup = require('levelup'); +var errors = index.errors; var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; diff --git a/lib/node.js b/lib/node.js index 45f21b7d..f44dc616 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1,28 +1,39 @@ 'use strict'; -var async = require('async'); -var Chain = require('./chain'); -var Block = require('./block'); -var DB = require('./db'); -var chainlib = require('chainlib'); var fs = require('fs'); -var BaseNode = chainlib.Node; var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var async = require('async'); var mkdirp = require('mkdirp'); -var log = chainlib.log; var bitcore = require('bitcore'); var Networks = bitcore.Networks; var _ = bitcore.deps._; var $ = bitcore.util.preconditions; +var Chain = require('./chain'); +var Block = require('./block'); +var DB = require('./db'); +var index = require('./'); +var log = index.log; var daemon = require('./daemon'); var Bus = require('./bus'); function Node(config) { - BaseNode.call(this, config); + if(!(this instanceof Node)) { + return new Node(config); + } + + this.db = null; + this.chain = null; + this.p2p = null; + this.network = null; + + this._loadConfiguration(config); + this._initialize(); + this.testnet = config.testnet; } -util.inherits(Node, BaseNode); +util.inherits(Node, EventEmitter); Node.prototype.openBus = function() { return new Bus({db: this.db}); @@ -47,10 +58,12 @@ Node.prototype.getAllPublishEvents = function() { }; Node.prototype._loadConfiguration = function(config) { - var self = this; this._loadBitcoinConf(config); this._loadBitcoind(config); - Node.super_.prototype._loadConfiguration.call(self, config); + this._loadNetwork(config); + this._loadDB(config); + this._loadAPI(); + this._loadConsensus(config); }; Node.DEFAULT_DAEMON_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n'; @@ -360,6 +373,21 @@ Node.prototype._loadConsensus = function(config) { this.chain = new Chain(options); }; +Node.prototype._loadAPI = function() { + var self = this; + + var methodData = self.db.getAPIMethods(); + methodData.forEach(function(data) { + var name = data[0]; + var instance = data[1]; + var method = data[2]; + + self[name] = function() { + return method.apply(instance, arguments); + }; + }); +}; + Node.prototype._initialize = function() { var self = this; diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index c2cac866..3d927043 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -3,10 +3,10 @@ var path = require('path'); var socketio = require('socket.io'); var BitcoreNode = require('../node'); -var chainlib = require('chainlib'); +var index = require('../'); var bitcore = require('bitcore'); var _ = bitcore.deps._; -var log = chainlib.log; +var log = index.log; log.debug = function() {}; var count = 0; diff --git a/lib/transaction.js b/lib/transaction.js index 39b9537b..39a7e35f 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -1,13 +1,10 @@ 'use strict'; var async = require('async'); +var levelup = require('levelup'); var bitcore = require('bitcore'); var Transaction = bitcore.Transaction; -var chainlib = require('chainlib'); -var BaseTransaction = chainlib.Transaction; -var BaseDatabase = chainlib.DB; -var levelup = chainlib.deps.levelup; -var _ = bitcore.deps._; +var BufferWriter = bitcore.encoding.BufferWriter; Transaction.prototype.populateInputs = function(db, poolTransactions, callback) { var self = this; @@ -62,7 +59,12 @@ Transaction.prototype._checkSpent = function(db, input, poolTransactions, callba }; Transaction.manyToBuffer = function(transactions) { - return BaseTransaction.manyToBuffer(transactions); + var bw = new BufferWriter(); + var count = transactions.length; + for(var i = 0; i < count; i++) { + transactions[i].toBufferWriter(bw); + } + return bw.concat(); }; module.exports = Transaction; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..72cc58c1 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,24 @@ +'use strict'; + +var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1 + +var utils = {}; +utils.isHash = function isHash(value) { + return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value); +}; + +utils.isSafeNatural = function isSafeNatural(value) { + return typeof value === 'number' && + isFinite(value) && + Math.floor(value) === value && + value >= 0 && + value <= MAX_SAFE_INTEGER; +}; + +utils.startAtZero = function startAtZero(obj, key) { + if (!obj.hasOwnProperty(key)) { + obj[key] = 0; + } +}; + +module.exports = utils; diff --git a/package.json b/package.json index 3201eb05..389c0bf0 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,14 @@ "bitcoind" ], "dependencies": { - "async": "1.3.0", + "async": "^1.3.0", "bindings": "^1.2.1", "bitcore": "^0.13.0", - "chainlib": "^0.2.0", + "colors": "^1.1.2", "commander": "^2.8.1", - "errno": "^0.1.2", + "errno": "^0.1.4", + "leveldown": "^1.4.1", + "levelup": "^1.2.1", "liftoff": "^2.1.0", "memdown": "^1.0.0", "mkdirp": "0.5.0", diff --git a/test/chain.unit.js b/test/chain.unit.js index 615c7ee6..d20b91da 100644 --- a/test/chain.unit.js +++ b/test/chain.unit.js @@ -3,21 +3,21 @@ var chai = require('chai'); var should = chai.should(); var sinon = require('sinon'); -var async = require('async'); -var proxyquire = require('proxyquire'); var memdown = require('memdown'); -var bitcoindjs = require('../'); -var DB = bitcoindjs.DB; -var Chain = bitcoindjs.Chain; -var Block = bitcoindjs.Block; +var index = require('../'); +var DB = index.DB; +var Chain = index.Chain; +var Block = index.Block; +var bitcore = require('bitcore'); +var BN = bitcore.crypto.BN; var chainData = require('./data/testnet-blocks.json'); describe('Bitcoin Chain', function() { describe('@constructor', function() { - + it('can create a new instance with and without `new`', function() { var chain = new Chain(); chain = Chain(); @@ -39,6 +39,125 @@ describe('Bitcoin Chain', function() { }); }); + describe('#initialize', function() { + + it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) { + var db = {}; + db.getMetadata = sinon.stub().callsArgWith(0, null, {}); + db.putBlock = sinon.stub().callsArg(1); + db.putMetadata = sinon.stub().callsArg(1); + db.getTransactionsFromBlock = sinon.stub(); + db._onChainAddBlock = sinon.stub().callsArg(1); + db.mempool = { + on: sinon.spy() + }; + var chain = new Chain({db: db, genesis: {hash: 'genesis'}}); + + chain.on('ready', function() { + should.exist(chain.tip); + db.putBlock.callCount.should.equal(1); + chain.tip.hash.should.equal('genesis'); + Number(chain.tip.__weight.toString(10)).should.equal(0); + done(); + }); + chain.on('error', function(err) { + should.not.exist(err); + done(); + }); + + chain.initialize(); + }); + + it('should initialize the chain with the metadata from the database if it exists', function(done) { + var db = {}; + db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2}); + db.putBlock = sinon.stub().callsArg(1); + db.putMetadata = sinon.stub().callsArg(1); + db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'}); + db.getTransactionsFromBlock = sinon.stub(); + db.mempool = { + on: sinon.spy() + }; + var chain = new Chain({db: db, genesis: {hash: 'genesis'}}); + chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10); + chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50)); + chain.on('ready', function() { + should.exist(chain.tip); + db.putBlock.callCount.should.equal(0); + chain.tip.hash.should.equal('block2'); + done(); + }); + chain.on('error', function(err) { + should.not.exist(err); + done(); + }); + chain.initialize(); + }); + + it('emit error from getMetadata', function(done) { + var db = { + getMetadata: function(cb) { + cb(new Error('getMetadataError')); + } + }; + db.getTransactionsFromBlock = sinon.stub(); + db.mempool = { + on: sinon.spy() + }; + var chain = new Chain({db: db, genesis: {hash: 'genesis'}}); + chain.on('error', function(error) { + should.exist(error); + error.message.should.equal('getMetadataError'); + done(); + }); + chain.initialize(); + }); + + it('emit error from putBlock', function(done) { + var db = { + getMetadata: function(cb) { + cb(null, null); + }, + putBlock: function(block, cb) { + cb(new Error('putBlockError')); + } + }; + db.getTransactionsFromBlock = sinon.stub(); + db.mempool = { + on: sinon.spy() + }; + var chain = new Chain({db: db, genesis: {hash: 'genesis'}}); + chain.on('error', function(error) { + should.exist(error); + error.message.should.equal('putBlockError'); + done(); + }); + chain.initialize(); + }); + + it('emit error from getBlock', function(done) { + var db = { + getMetadata: function(cb) { + cb(null, {tip: 'tip'}); + }, + getBlock: function(tip, cb) { + cb(new Error('getBlockError')); + } + }; + db.getTransactionsFromBlock = sinon.stub(); + db.mempool = { + on: sinon.spy() + }; + var chain = new Chain({db: db, genesis: {hash: 'genesis'}}); + chain.on('error', function(error) { + should.exist(error); + error.message.should.equal('getBlockError'); + done(); + }); + chain.initialize(); + }); + }); + describe('#stop', function() { it('should call the callback', function(done) { var chain = new Chain(); @@ -71,44 +190,6 @@ describe('Bitcoin Chain', function() { }); }); - describe('#buildGenesisBlock', function() { - it('can handle no options', function() { - var db = { - buildGenesisData: sinon.stub().returns({}) - }; - var chain = new Chain({db: db}); - var block = chain.buildGenesisBlock(); - should.exist(block); - block.should.be.instanceof(Block); - db.buildGenesisData.calledOnce.should.equal(true); - }); - - it('set timestamp, nonce, bits, merkleRoot and data of the genesis', function() { - var db = { - buildGenesisData: sinon.stub().returns({ - merkleRoot: 'merkleRoot', - buffer: new Buffer('abcdef', 'hex') - }) - }; - var chain = new Chain({db: db}); - var timestamp = '2015-03-20T14:46:01.118Z'; - var block = chain.buildGenesisBlock({ - timestamp: timestamp, - nonce: 1, - bits: 520617984 - }); - should.exist(block); - block.should.be.instanceof(Block); - block.timestamp.toISOString().should.equal(timestamp); - block.nonce.should.equal(1); - block.bits.should.equal(520617984); - block.merkleRoot.should.equal('merkleRoot'); - block.data.should.deep.equal(new Buffer('abcdef', 'hex')); - db.buildGenesisData.calledOnce.should.equal(true); - }); - - }); - describe('#getWeight', function() { var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9'; var chain = new Chain(); @@ -136,4 +217,53 @@ describe('Bitcoin Chain', function() { }); }); }); + + describe('#getHashes', function() { + + it('should get an array of chain hashes', function(done) { + + var blocks = {}; + var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex')); + var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex')); + var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex')); + blocks[genesisBlock.hash] = genesisBlock; + blocks[block1.hash] = block1; + blocks[block2.hash] = block2; + + var db = new DB({store: memdown}); + db.getPrevHash = function(blockHash, cb) { + cb(null, blocks[blockHash].prevHash); + }; + + var chain = new Chain({ + db: db, + genesis: genesisBlock + }); + + chain.tip = block2; + + chain.on('ready', function() { + + // remove one of the cached hashes to force db call + delete chain.cache.hashes[block1.hash]; + + // the test + chain.getHashes(block2.hash, function(err, hashes) { + should.not.exist(err); + should.exist(hashes); + hashes.length.should.equal(3); + done(); + }); + }); + + chain.on('error', function(err) { + should.not.exist(err); + done(); + }); + + chain.initialize(); + }); + }); + + }); diff --git a/test/db.unit.js b/test/db.unit.js index 372ec016..21a8ec1c 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -227,90 +227,6 @@ describe('Bitcoin DB', function() { }); }); }); - describe('#buildGenesisData', function() { - it('build genisis data', function() { - var db = new DB({path: 'path', store: memdown}); - db.buildCoinbaseTransaction = sinon.stub().returns({ - toBuffer: sinon.stub().returns(new Buffer('abcdef', 'hex')) - }); - db.getMerkleRoot = sinon.stub().returns('merkleRoot'); - var data = db.buildGenesisData(); - data.buffer.should.deep.equal(new Buffer('01abcdef', 'hex')); - data.merkleRoot.should.equal('merkleRoot'); - }); - }); - - describe('#buildCoinbaseTransaction', function() { - it('should correctly build a coinbase transaction with no fees', function() { - var db = new DB({path: 'path', store: memdown}); - db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; - db.coinbaseAmount = coinbaseAmount; - var coinbaseTx = db.buildCoinbaseTransaction(); - coinbaseTx.inputs.length.should.equal(1); - var input = coinbaseTx.inputs[0]; - var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000'; - input.prevTxId.toString('hex').should.equal(expectedTxId); - should.exist(input.outputIndex); - should.exist(input.sequenceNumber); - should.exist(input._script); // coinbase input script returns null - coinbaseTx.outputs.length.should.equal(1); - var output = coinbaseTx.outputs[0]; - output.satoshis.should.equal(coinbaseAmount); - }); - - it('should correctly build a coinbase transaction with fees', function() { - var db = new DB({path: 'path', store: memdown}); - db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; - db.coinbaseAmount = coinbaseAmount; - var transactions = [ - { - _getInputAmount: sinon.stub().returns(5000), - _getOutputAmount: sinon.stub().returns(4000), - isCoinbase: sinon.stub().returns(false) - }, - { - _getInputAmount: sinon.stub().returns(8000), - _getOutputAmount: sinon.stub().returns(7000), - isCoinbase: sinon.stub().returns(false) - } - ]; - var coinbaseTx = db.buildCoinbaseTransaction(transactions); - coinbaseTx.inputs.length.should.equal(1); - var input = coinbaseTx.inputs[0]; - var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000'; - input.prevTxId.toString('hex').should.equal(expectedTxId); - should.exist(input.outputIndex); - should.exist(input.sequenceNumber); - should.exist(input._script); // coinbase input returns null - coinbaseTx.outputs.length.should.equal(1); - var output = coinbaseTx.outputs[0]; - output.satoshis.should.equal(coinbaseAmount + 2000); - }); - - it('should throw an error if coinbaseAddress not included', function() { - var db = new DB({path: 'path', store: memdown}); - (function() { - db.buildCoinbaseTransaction(); - }).should.throw('coinbaseAddress required to build coinbase'); - }); - - it('will build a coinbase database with different data', function() { - var db = new DB({path: 'path', store: memdown}); - db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; - var tx1 = db.buildCoinbaseTransaction().uncheckedSerialize(); - var tx2 = db.buildCoinbaseTransaction().uncheckedSerialize(); - tx1.should.not.equal(tx2); - }); - - it('can pass in custom data', function() { - var db = new DB({path: 'path', store: memdown}); - db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; - var tx1 = db.buildCoinbaseTransaction(null, new Buffer('abcdef', 'hex')); - var data = tx1.inputs[0]._script.getData(); - data.should.deep.equal(new Buffer('abcdef', 'hex')); - }); - - }); describe('#getOutputTotal', function() { it('should return the correct value including the coinbase', function() { @@ -343,7 +259,7 @@ describe('Bitcoin DB', function() { }; }); var grandTotal = db.getOutputTotal(transactions, true); - grandTotal.should.equal(50) + grandTotal.should.equal(50); }); }); @@ -381,7 +297,6 @@ describe('Bitcoin DB', function() { db.blockHandler = sinon.stub().callsArg(2); db._onChainAddBlock({hash: 'hash'}, function(err) { should.not.exist(err); - db.mempool.removeBlock.args[0][0].should.equal('hash'); db.blockHandler.args[0][1].should.equal(true); done(); }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index 4b8653aa..c65b9334 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -8,8 +8,7 @@ var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var EventEmitter = require('events').EventEmitter; var errors = bitcorenode.errors; -var chainlib = require('chainlib'); -var levelup = chainlib.deps.levelup; +var levelup = require('levelup'); var mockdb = { bitcoind: { diff --git a/test/node.unit.js b/test/node.unit.js index 831c2959..d410cb2b 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -2,42 +2,42 @@ var should = require('chai').should(); var sinon = require('sinon'); -var util = require('util'); var EventEmitter = require('events').EventEmitter; var bitcore = require('bitcore'); var Networks = bitcore.Networks; var blockData = require('./data/livenet-345003.json'); var Block = require('../lib/block'); var proxyquire = require('proxyquire'); -var chainlib = require('chainlib'); -var OriginalNode = chainlib.Node; +var index = require('..'); var fs = require('fs'); var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf'); var chainHashes = require('./data/hashes.json'); -var BaseNode = function() {}; -util.inherits(BaseNode, EventEmitter); -BaseNode.log = chainlib.log; -BaseNode.prototype._loadConfiguration = sinon.spy(); -BaseNode.prototype._initialize = sinon.spy(); -chainlib.Node = BaseNode; - -var BadNode = proxyquire('../lib/node', { - chainlib: chainlib, - fs: { - readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/data/badbitcoin.conf')) - } -}); - -var Node = proxyquire('../lib/node', { - chainlib: chainlib, - fs: { - readFileSync: sinon.stub().returns(bitcoinConfBuffer) - } -}); -chainlib.Node = OriginalNode; - describe('Bitcoind Node', function() { + + var Node; + var BadNode; + + before(function() { + + BadNode = proxyquire('../lib/node', { + fs: { + readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/data/badbitcoin.conf')) + } + }); + BadNode.prototype._loadConfiguration = sinon.spy(); + BadNode.prototype._initialize = sinon.spy(); + + Node = proxyquire('../lib/node', { + fs: { + readFileSync: sinon.stub().returns(bitcoinConfBuffer) + } + }); + Node.prototype._loadConfiguration = sinon.spy(); + Node.prototype._initialize = sinon.spy(); + + }); + describe('#openBus', function() { it('will create a new bus', function() { var node = new Node({}); @@ -89,13 +89,23 @@ describe('Bitcoind Node', function() { }); describe('#_loadConfiguration', function() { it('should call the necessary methods', function() { - var node = new Node({}); - node._loadBitcoinConf = sinon.spy(); - node._loadBitcoind = sinon.spy(); - node._loadConfiguration({}); - node._loadBitcoind.called.should.equal(true); - node._loadBitcoinConf.called.should.equal(true); - BaseNode.prototype._loadConfiguration.called.should.equal(true); + var TestNode = proxyquire('../lib/node', { + fs: { + readFileSync: sinon.stub().returns(bitcoinConfBuffer) + } + }); + TestNode.prototype._initialize = sinon.spy(); + TestNode.prototype._loadBitcoinConf = sinon.spy(); + TestNode.prototype._loadBitcoind = sinon.spy(); + TestNode.prototype._loadDB = sinon.spy(); + TestNode.prototype._loadAPI = sinon.spy(); + TestNode.prototype._loadConsensus = sinon.spy(); + var node = new TestNode({}); + node._loadBitcoind.callCount.should.equal(1); + node._loadBitcoinConf.callCount.should.equal(1); + node._loadDB.callCount.should.equal(1); + node._loadAPI.callCount.should.equal(1); + node._loadConsensus.callCount.should.equal(1); }); }); describe('#_loadBitcoinConf', function() { @@ -432,7 +442,12 @@ describe('Bitcoind Node', function() { }); }); describe('#_loadConsensus', function() { - var node = new Node({}); + + var node; + + before(function() { + node = new Node({}); + }); it('will set properties', function() { node._loadConsensus(); @@ -447,7 +462,21 @@ describe('Bitcoind Node', function() { var node; before(function() { - node = new Node({}); + var TestNode = proxyquire('../lib/node', { + fs: { + readFileSync: sinon.stub().returns(bitcoinConfBuffer) + } + }); + TestNode.prototype._loadConfiguration = sinon.spy(); + TestNode.prototype._initializeBitcoind = sinon.spy(); + TestNode.prototype._initializeDatabase = sinon.spy(); + TestNode.prototype._initializeChain = sinon.spy(); + + // mock the _initialize during construction + var _initialize = TestNode.prototype._initialize; + TestNode.prototype._initialize = sinon.spy(); + + node = new TestNode({}); node.chain = { on: sinon.spy() }; @@ -455,12 +484,12 @@ describe('Bitcoind Node', function() { node.bitcoind = { on: sinon.spy() }; - node._initializeBitcoind = sinon.spy(); - node._initializeDatabase = sinon.spy(); - node._initializeChain = sinon.spy(); node.db = { on: sinon.spy() }; + + // restore the original method + node._initialize = _initialize; }); it('should initialize', function(done) { @@ -544,11 +573,11 @@ describe('Bitcoind Node', function() { it('will log on ready event', function(done) { var node = new Node({}); node.db = new EventEmitter(); - sinon.stub(chainlib.log, 'info'); + sinon.stub(index.log, 'info'); node.db.on('ready', function() { setImmediate(function() { - chainlib.log.info.callCount.should.equal(1); - chainlib.log.info.restore(); + index.log.info.callCount.should.equal(1); + index.log.info.restore(); done(); }); }); diff --git a/test/transaction.unit.js b/test/transaction.unit.js index b5676c3f..62bf9449 100644 --- a/test/transaction.unit.js +++ b/test/transaction.unit.js @@ -8,8 +8,7 @@ var transactionData = require('./data/bitcoin-transactions.json'); var memdown = require('memdown'); var DB = bitcoinlib.DB; var db = new DB({store: memdown}); -var chainlib = require('chainlib'); -var levelup = chainlib.deps.levelup; +var levelup = require('levelup'); describe('Bitcoin Transaction', function() { describe('#populateInputs', function() {