diff --git a/lib/services/address/index.js b/lib/services/address/index.js index a31da1bb..c6a553f4 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -63,6 +63,7 @@ AddressService.prototype.getAddressBalance = function(addresses, options, callba var addresses = self._normalizeAddressArg(addressArg); var cacheKey = addresses.join(''); var balance = self.balanceCache.get(cacheKey); + if (balance) { return setImmediate(function() { callback(null, balance); @@ -80,8 +81,8 @@ AddressService.prototype.getAddressBalance = function(addresses, options, callba }; AddressService.prototype.getAddressHistory = function(addresses, options, callback) { - var self = this; + var self = this; var txids = []; async.eachLimit(addresses, 4, function(address, next) { @@ -361,7 +362,7 @@ AddressService.prototype._processTransaction = function(opts, tx) { }; -AddressService.prototype._onBlock = function(block, connect) { +AddressService.prototype.onBlock = function(block, connect) { var self = this; diff --git a/lib/services/block/encoding.js b/lib/services/block/encoding.js index ec471f3d..fd9f398c 100644 --- a/lib/services/block/encoding.js +++ b/lib/services/block/encoding.js @@ -6,12 +6,11 @@ var Block = require('bitcore-lib').Block; function Encoding(servicePrefix) { this.servicePrefix = servicePrefix; this.blockPrefix = new Buffer('00', 'hex'); - this.hashPrefix = new Buffer('01', 'hex'); - this.heightPrefix = new Buffer('02', 'hex'); + this.metaPrefix = new Buffer('01', 'hex'); } -// ---- hash --> block +// ---- hash --> rawblock Encoding.prototype.encodeBlockKey = function(hash) { return Buffer.concat([ this.servicePrefix, this.blockPrefix, new Buffer(hash, 'hex') ]); }; @@ -28,38 +27,27 @@ Encoding.prototype.decodeBlockValue = function(buffer) { return Block.fromBuffer(buffer); }; - -// ---- hash --> header -Encoding.prototype.encodeHashKey = function(hash) { - var hashBuf = new Buffer(hash, 'hex'); - return Buffer.concat([ this.servicePrefix, this.hashPrefix, hashBuf ]); -}; - -Encoding.prototype.decodeHashKey = function(buffer) { - return buffer.slice(3).toString('hex'); -}; - -Encoding.prototype.encodeHeaderValue = function(header) { - return new Buffer(JSON.stringify(header), 'utf8'); -}; - -Encoding.prototype.decodeHeaderValue = function(buffer) { - return JSON.parse(buffer.toString('utf8')); -}; - - -// ---- height --> header -Encoding.prototype.encodeHeightKey = function(height) { - - var heightBuf = new Buffer(4); +// ---- height --> hash, chainwork +Encoding.prototype.encodeMetaKey = function(height) { + var heigthtBuf = new Buffer(4); heightBuf.writeUInt32BE(height); - return Buffer.concat([ this.servicePrefix, this.heightPrefix, heightBuf ]); - + return Buffer.concat([ this.blockPrefix, this.metaPrefix, heightBuf ]); }; -Encoding.prototype.decodeHeightKey = function(buffer) { - return buffer.readUInt32BE(3); +Encoding.prototype.decodeMetaKey = function(buffer) { + return heightBuf.readUInt32BE(3); }; +Encoding.prototype.encodeMetaValue = function(value) { + // { chainwork: hex-string, hash: hex-string } + return Buffer([ new Buffer(value.hash, 'hex'), new Buffer(value.chainwork, 'hex') ]); +}; + +Encoding.prototype.decodeMetaValue = function(buffer) { + return { + hash: buffer.slice(0, 32).toString('hex'), + chainwork: buffer.slice(32).toString('hex') + }; +}; module.exports = Encoding; diff --git a/lib/services/block/index.js b/lib/services/block/index.js index e760a826..114cfd6f 100644 --- a/lib/services/block/index.js +++ b/lib/services/block/index.js @@ -14,17 +14,23 @@ var consensus = require('bcoin').consensus; var constants = require('../../constants'); var BlockService = function(options) { + BaseService.call(this, options); + this._tip = null; this._p2p = this.node.services.p2p; this._db = this.node.services.db; + this._subscriptions = {}; this._subscriptions.block = []; this._subscriptions.reorg = []; // meta is [{ chainwork: chainwork, hash: hash }] - this._meta = []; // properties that apply to blocks that are not already stored on the blocks, yet we still needed, i.e. chainwork, height + // properties that apply to blocks that are not already stored on the blocks, + // i.e. chainwork, height + this._meta = []; + // this is the in-memory full/raw block cache this._blockQueue = LRU({ max: 50 * (1 * 1024 * 1024), // 50 MB of blocks, length: function(n) { @@ -32,6 +38,9 @@ var BlockService = function(options) { } }); // hash -> block + // keep track of out-of-order blocks, this is a list of chains (which are lists themselves) + // e.g. [ [ block1, block5 ], [ block2, block6 ] ]; + this._incompleteChains = []; this._chainTips = []; // list of all chain tips, including main chain and any chains that were orphaned after a reorg this._blockCount = 0; this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName()]; @@ -56,11 +65,18 @@ BlockService.prototype.start = function(callback) { return callback(err); } - self.prefix = prefix; - self._encoding = new Encoding(self.prefix); - self._loadMeta(); - self._setListeners(); - callback(); + self._db.getServiceTip('block', function(err, tip) { + + if (err) { + return callback(err); + } + self.prefix = prefix; + self._encoding = new Encoding(self.prefix); + self._loadMeta(); + self._setListeners(); + self._onTipBlock(tip); + callback(); + }); }); }; @@ -177,8 +193,8 @@ BlockService.prototype._computeChainwork = function(bits, prev) { BlockService.prototype._determineBlockState = function(block) { - if (this._isOrphanBlock(block)) { - return 'orphaned'; + if (this._isOutOfOrder(block)) { + return 'outoforder'; } if (this._isChainReorganizing(block)) { @@ -330,26 +346,27 @@ BlockService.prototype._handleReorg = function(block) { BlockService.prototype._isChainReorganizing = function(block) { + // if we aren't an out of order block, then is this block's prev hash our tip? var prevHash = utils.reverseBufferToString(block.header.prevHash); return prevHash !== this._tip.hash; }; -BlockService.prototype._isOrphanBlock = function(block) { +BlockService.prototype._isOutOfOrder = function(block) { - // so this should fail - "is orphan" + // is this block the direct child of one of our chain tips? If so, not an out of order block + var prevHash = utils.reverseBufferToString(block.header.prevHash); - // we'll consult our metadata - var i = this._meta.length - 1; - for(; i >= 0; --i) { - // if we find this block's prev hash in our meta collection, then we know all of its ancestors are there too - if (block.hash === this._meta[i].hash) { + for(var i = 0; i < this._chainTips.length; i++) { + var chainTip = this._chainTips[i]; + if (chainTip === prevHash) { return false; } } return true; + }; BlockService.prototype._loadMeta = function() { @@ -359,20 +376,10 @@ BlockService.prototype._loadMeta = function() { } }; -BlockService.prototype._loadTip = function() { - var self = this; - self._db.getServiceTip('block'); -}; - BlockService.prototype._onBestHeight = function(height) { var self = this; self._bestHeight = height; - - // once we have best height, we know the p2p network is ready to go - self._db.once('tip-block', self._onTipBlock.bind(self)); - - self._loadTip(); }; BlockService.prototype._onBlock = function(block) { @@ -388,14 +395,11 @@ BlockService.prototype._onBlock = function(block) { // 3. store the block for safe keeping this._cacheBlock(block); - // 4. add block to meta - this._updateMeta(block); - - // 5. determine block state, reorg, orphaned, normal + // 4. determine block state, reorg, outoforder, normal var blockState = this._determineBlockState(block); - // 6. add block to chainTips - this._updateChainTips(block, blockState); + // 5. update internal data structures depending on blockstate + this._updateChainInfo(block, blockState); // 7. react to state of block switch (blockState) { @@ -407,6 +411,7 @@ BlockService.prototype._onBlock = function(block) { break; default: // send all unsent blocks now that we have a complete chain + this._updateMeta(block); this._sendDelta(); break; } @@ -419,23 +424,10 @@ BlockService.prototype._onDbError = function(err) { }; -// --- mark 1 BlockService.prototype._onTipBlock = function(tip) { var self = this; - if (tip) { - - self._tip = tip; - - } else { - - self._tip = { - height: 0, - hash: self.GENESIS_HASH - }; - - } - + self._tip = tip; self._chainTips.push(self._tip.hash); self._startSubscriptions(); self._startSync(); @@ -588,31 +580,63 @@ BlockService.prototype._sync = function() { }; -BlockService.prototype._updateChainTips = function(block, state) { +BlockService.prototype._updateNormalStateChainInfo = function(block, prevHash) { + var index = this._chainTips.indexOf(prevHash); + assert(index > -1, 'Block state is normal, ' + + 'yet the previous block hash is missing from the chain tips collection.'); + + this._chainTips.push(block.hash); + this._chainTips.splice(index, 1); +}; + +BlockService.prototype._updateReorgStateChainInfo = function(block) { + this._chainTips.push(block.hash); +}; + +BlockService.prototype._updateOutOfOrderStateChainInfo = function(block, prevHash) { + // add this block to the incomplete chains + // are we the parent of some other block in here? + // are we the child of some other block in here? + // block chains in here go newest to oldest block. + // When the oldest block's prevHash is one of the chainTips, the + // incomplete chain is now complete. + for(var i = 0; i < this._incompleteChains.length; i++) { + var chain = this._incompleteChains[i]; + // does the last block in this chain have a prevHash that equal's our block's hash? + // if so, this block should be pushed on the end + if (chain.length > 0) { + + if (utils.reverseBufferToString(chain[chain.length - 1].header.prevHash) === block.hash) { + chain.push(block); + } + + // is our block's prevHash equal to the first block's hash? + if (chain[0].hash === prevHash) { + chain.unshift(block); + } + } + } +}; + +BlockService.prototype._updateChainInfo = function(block, state) { var prevHash = utils.reverseBufferToString(block.header.prevHash); - // otherwise we are not an orphan, so we could be a reorg or normal - // if this is a normal state, then we are just adding ourselves as the tip on the main chain if (state === 'normal') { - - var index = this._chainTips.indexOf(prevHash); - assert(index > -1, 'Block state is normal, ' + - 'yet the previous block hash is missing from the chain tips collection.'); - - this._chainTips.push(block.hash); - this._chainTips.splice(index, 1); + this._updateNormalStateChainInfo(block, prevHash); return; } // if this is a reorg state, then a new chain tip will be added to the list if (state === 'reorg') { - - this._chainTips.push(block.hash); - + this._updateReorgStateChainInfo(block); + return; } + this._updateOutOfOrderStateChainInfo(block, prevHash); + + }; BlockService.prototype._updateMeta = function(block) { diff --git a/lib/services/db/index.js b/lib/services/db/index.js index ebec6849..fc0c28f5 100644 --- a/lib/services/db/index.js +++ b/lib/services/db/index.js @@ -37,6 +37,7 @@ function DB(options) { this.subscriptions = {}; this._operationsQueue = 0; + this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.getNetworkName()]; } util.inherits(DB, Service); @@ -260,27 +261,37 @@ DB.prototype.getPublishEvents = function() { return []; }; -DB.prototype.getServiceTip = function(serviceName) { +DB.prototype.getServiceTip = function(serviceName, callback) { var keyBuf = Buffer.concat([ this.dbPrefix, new Buffer('tip-' + serviceName, 'utf8') ]); var self = this; self.get(keyBuf, function(err, tipBuf) { + if (err) { - this.emit('error', err); - return; + return callback(err); } - var tip = null; + var tip; if (tipBuf) { + var heightBuf = new Buffer(4); + tip = { height: heightBuf.readUInt32BE(), hash: tipBuf.slice(4) }; + + } else { + + tip = { + height: 0, + hash: self.GENESIS_HASH + }; + } - self.emit('tip-' + serviceName, tip); + callback(null, tip); }); }; diff --git a/lib/services/p2p/index.js b/lib/services/p2p/index.js index 0d0ccafa..8b733019 100644 --- a/lib/services/p2p/index.js +++ b/lib/services/p2p/index.js @@ -35,11 +35,12 @@ P2P.prototype.clearInventoryCache = function() { P2P.prototype.getAPIMethods = function() { var methods = [ - ['getInfo', this, this.getInfo, 0], - ['getHeaders', this, this.getHeaders, 1], - ['getMempool', this, this.getMempool, 0], + ['clearInventoryCache', this, this.clearInventoryCache, 0], ['getBlocks', this, this.getBlocks, 1], - ['clearInventoryCache', this, this.clearInventoryCache, 0] + ['getHeaders', this, this.getHeaders, 1], + ['getInfo', this, this.getInfo, 0], + ['getMempool', this, this.getMempool, 0], + ['sendTransaction', this, this.sendTransaction, 1] ]; return methods; }; @@ -94,6 +95,11 @@ P2P.prototype.getPublishEvents = function() { }; +P2P.prototype.sendTransaction = function(tx, callback) { + p2p.sendMessage(this.messages.Inventory(tx)); +}; + + P2P.prototype.start = function(callback) { var self = this; diff --git a/lib/services/transaction/index.js b/lib/services/transaction/index.js index a97ae078..45a229dd 100644 --- a/lib/services/transaction/index.js +++ b/lib/services/transaction/index.js @@ -7,17 +7,9 @@ var inherits = require('util').inherits; var Encoding = require('./encoding'); var levelup = require('levelup'); -/** - * The Transaction Service builds upon the Database Service and the Bitcoin Service to add additional - * functionality for getting information by bitcoin transaction hash/id. This includes the current - * bitcoin memory pool as validated by a trusted bitcoind instance. - * @param {Object} options - * @param {Node} options.node - An instance of the node - * @param {String} options.name - An optional name of the service - */ function TransactionService(options) { BaseService.call(this, options); - this.concurrency = options.concurrency || 20; + this._db = this.node.services.db; this.currentTransactions = {}; } @@ -40,26 +32,59 @@ TransactionService.prototype.getAPIMethods = function() { ]; }; -TransactionService.prototype.start = function(callback) { - var self = this; - - self.db = this.node.services.db; - - self.node.services.db.getPrefix(self.name, function(err, prefix) { - if(err) { +TransactionService.prototype.getRawTransaction = function(txid, callback) { + this.getTransaction(txid, function(err, tx) { + if (err) { return callback(err); } - self.prefix = prefix; - self.encoding = new Encoding(self.prefix); - callback(); + return tx.serialize(); }); }; -TransactionService.prototype.stop = function(callback) { - setImmediate(callback); +TransactionService.prototype.getDetailedTransaction = TransactionService.prototype.getTransaction = function(txid, options, callback) { + + var self = this; + + assert(txid.length === 64, 'Transaction, Txid: ' + txid + ' with length: ' + txid.length + ' does not resemble a txid.'); + + if(self.currentTransactions[txid]) { + return setImmediate(function() { + callback(null, self.currentTransactions[txid]); + }); + } + + var key = self.encoding.encodeTransactionKey(txid); + + async.waterfall([ + function(next) { + self.node.services.db.get(key, function(err, buffer) { + if (err instanceof levelup.errors.NotFoundError) { + return next(null, false); + } else if (err) { + return callback(err); + } + var tx = self.encoding.decodeTransactionValue(buffer); + next(null, tx); + }); + }, function(tx, next) { + if (tx) { + return next(null, tx); + } + if (!options || !options.queryMempool) { + return next(new Error('Transaction: ' + txid + ' not found in index')); + } + self.node.services.mempool.getTransaction(txid, function(err, tx) { + if (err instanceof levelup.errors.NotFoundError) { + return callback(new Error('Transaction: ' + txid + ' not found in index or mempool')); + } else if (err) { + return callback(err); + } + self._getMissingInputValues(tx, next); + }); + }], callback); }; -TransactionService.prototype.blockHandler = function(block, connectBlock, callback) { +TransactionService.prototype.onBlock = function(block, connectBlock, callback) { var self = this; var action = 'put'; var reverseAction = 'del'; @@ -114,7 +139,45 @@ TransactionService.prototype.blockHandler = function(block, connectBlock, callba }); }; + +TransactionService.prototype.sendTransaction = function(tx, callback) { + this._p2p.sendTransaction(tx, callback); +}; + +TransactionService.prototype.start = function(callback) { + var self = this; + + self._db.getPrefix(self.name, function(err, prefix) { + + if(err) { + return callback(err); + } + + self._db.getServiceTip('transaction', function(err, tip) { + + if (err) { + return callback(err); + } + + self._tip = tip; + self.prefix = prefix; + self.encoding = new Encoding(self.prefix); + callback(); + + }); + }); +}; + +TransactionService.prototype.stop = function(callback) { + setImmediate(callback); +}; + +TransactionService.prototype.syncPercentage = function(callback) { + +}; + TransactionService.prototype._getMissingInputValues = function(tx, callback) { + var self = this; if (tx.isCoinbase()) { @@ -140,6 +203,7 @@ TransactionService.prototype._getMissingInputValues = function(tx, callback) { next(); }); }, callback); + }; TransactionService.prototype._getInputValues = function(tx, callback) { @@ -163,47 +227,4 @@ TransactionService.prototype._getInputValues = function(tx, callback) { }, callback); }; -TransactionService.prototype.getTransaction = function(txid, options, callback) { - var self = this; - - assert(txid.length === 64, 'Transaction, Txid: ' + txid + ' with length: ' + txid.length + ' does not resemble a txid.'); - - if(self.currentTransactions[txid]) { - return setImmediate(function() { - callback(null, self.currentTransactions[txid]); - }); - } - - var key = self.encoding.encodeTransactionKey(txid); - - async.waterfall([ - function(next) { - self.node.services.db.get(key, function(err, buffer) { - if (err instanceof levelup.errors.NotFoundError) { - return next(null, false); - } else if (err) { - return callback(err); - } - var tx = self.encoding.decodeTransactionValue(buffer); - next(null, tx); - }); - }, function(tx, next) { - if (tx) { - return next(null, tx); - } - if (!options || !options.queryMempool) { - return next(new Error('Transaction: ' + txid + ' not found in index')); - } - self.node.services.mempool.getTransaction(txid, function(err, tx) { - if (err instanceof levelup.errors.NotFoundError) { - return callback(new Error('Transaction: ' + txid + ' not found in index or mempool')); - } else if (err) { - return callback(err); - } - self._getMissingInputValues(tx, next); - }); - }], callback); -}; - - module.exports = TransactionService;