diff --git a/child-pay-for-parent.js b/child-pay-for-parent.js new file mode 100644 index 00000000..cc6cf676 --- /dev/null +++ b/child-pay-for-parent.js @@ -0,0 +1,83 @@ +var unmentionables = require('./keys.json'); +var assert = require('assert'); +var p2p = require('bitcore-p2p'); +var Peer = p2p.Peer; +var messages = new p2p.Messages(); + +var peer = new Peer({host: '68.101.164.118'}); + +// 1. prepare a low fee ts. +// 2. send low fee tx. The low fee tx should be enough to be relayed +// 3. 20 - 50 sats/byte is what we usually see +// 4. later, spend those outputs using an approproate fee + +var bitcore = require('bitcore-lib'); +var key1 = new bitcore.PrivateKey(unmentionables.key1); +var key2 = new bitcore.PrivateKey(unmentionables.key2); +var lowFeeRate = 40; //sat/byte +var highFeeRate = 260; + +var parentUtxo = { + txid: '100304043f19ea9c4faf0810c9432b806cf383de38d9138b004c8a8df7f76249', + outputIndex: 0, + address: '1DxgVtn7xUwX9Jwqx7YW7JfsDDDVHDxTwL', + script: '76a9148e295bd3b705aac6ba0cb02bb582f98c451b83ee88ac', + satoshis: 17532710 +}; + +var parentTx = new bitcore.Transaction(); + +var lowFee = lowFeeRate*193; +var highFee = highFeeRate*193; +var childToAddress = '12Awugz6fhM2BW4dH7Xx1ZKxy3CHWM6a8f'; + +parentTx.from(parentUtxo).to(childToAddress, (parentUtxo.satoshis - lowFee)).fee(lowFee).sign(key1); +console.log(parentTx.getFee()); +console.log(parentTx.verify()); +assert((parentTx.inputs[0].output.satoshis - parentTx.outputs[0].satoshis) === parentTx.getFee()); + +console.log(parentTx.toObject()); +console.log(parentTx.serialize()); + +peer.on('ready', function() { + console.log(peer.version, peer.subversion, peer.bestHeight); + setTimeout(function() { + peer.sendMessage(messages.Transaction(parentTx)); + setTimeout(function() { peer.disconnect(); }, 2000); + }, 2000); +}); + +peer.on('disconnect', function() { + console.log('connection closed'); +}); +peer.connect(); +//var childUtxo = { +// txid: parentTx.id, +// outputIndex: 0, +// address: childToAddress, +// script: parentTx.outputs[0].script.toHex(), +// satoshis: (parentUtxo.satoshis - lowFee) +//}; +// +//var childTx = new bitcore.Transaction(); +//childTx.from(childUtxo).to(childToAddress, (childUtxo.satoshis - highFee)).fee(highFee).sign(key2); +//console.log(childTx.getFee()); +//console.log(childTx.toObject()); + + + + + +//01000000 +//01 +//49 +//62f7f78d8a4c008b13d938de83f36c802b43c91008af4f9cea193f04040310000000006a47304402200c98dee6e5a2db276e24ac45c153aa5586455894efee060e95e0e7d017569df30220258b48ebd0253ca6b4b8b35d8f18b4bcb41040fb060f1ed59f7989185dd296170121031c8ed8aead402b7f6e02617d50b7a7ba3b07a489da0702f65f985bf0ebb64f3a +//ffffffff +//01 +//26 +//87 +//0b01000000001976a9140cd9b466eb74f45e3290b47fbbb622e458601267 +//88 +//ac +//00000000 + diff --git a/lib/constants.js b/lib/constants.js deleted file mode 100644 index c48407cf..00000000 --- a/lib/constants.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -var exports = {}; - -exports.PREFIXES = { - ADDRESS: new Buffer('02', 'hex'), - UNSPENT: new Buffer('03', 'hex'), - // OUTPUTS: new Buffer('02', 'hex'), // Query outputs by address and/or height - // SPENTS: new Buffer('03', 'hex'), // Query inputs by address and/or height - // SPENTSMAP: new Buffer('05', 'hex') // Get the input that spends an output -}; - -exports.MEMPREFIXES = { - OUTPUTS: new Buffer('01', 'hex'), // Query mempool outputs by address - SPENTS: new Buffer('02', 'hex'), // Query mempool inputs by address - SPENTSMAP: new Buffer('03', 'hex') // Query mempool for the input that spends an output -}; - -// To save space, we're only storing the PubKeyHash or ScriptHash in our index. -// To avoid intentional unspendable collisions, which have been seen on the blockchain, -// we must store the hash type (PK or Script) as well. -exports.HASH_TYPES = { - PUBKEY: new Buffer('01', 'hex'), - REDEEMSCRIPT: new Buffer('02', 'hex') -}; - -// Translates from our enum type back into the hash types returned by -// bitcore-lib/address. -exports.HASH_TYPES_READABLE = { - '01': 'pubkeyhash', - '02': 'scripthash' -}; - -exports.HASH_TYPES_MAP = { - 'pubkeyhash': exports.HASH_TYPES.PUBKEY, - 'scripthash': exports.HASH_TYPES.REDEEMSCRIPT -}; - -exports.SPACER_MIN = new Buffer('00', 'hex'); -exports.SPACER_MAX = new Buffer('ff', 'hex'); -exports.SPACER_HEIGHT_MIN = new Buffer('0000000000', 'hex'); -exports.SPACER_HEIGHT_MAX = new Buffer('ffffffffff', 'hex'); -exports.TIMESTAMP_MIN = new Buffer('0000000000000000', 'hex'); -exports.TIMESTAMP_MAX = new Buffer('ffffffffffffffff', 'hex'); - -// The maximum number of inputs that can be queried at once -exports.MAX_INPUTS_QUERY_LENGTH = 50000; -// The maximum number of outputs that can be queried at once -exports.MAX_OUTPUTS_QUERY_LENGTH = 50000; -// The maximum number of transactions that can be queried at once -exports.MAX_HISTORY_QUERY_LENGTH = 100; -// The maximum number of addresses that can be queried at once -exports.MAX_ADDRESSES_QUERY = 10000; -// The maximum number of simultaneous requests -exports.MAX_ADDRESSES_LIMIT = 5; - -module.exports = exports; - diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 8370fdbe..4d1c2097 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -8,32 +8,16 @@ var index = require('../../'); var log = index.log; var errors = index.errors; var bitcore = require('bitcore-lib'); -var levelup = require('levelup'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; var EventEmitter = require('events').EventEmitter; var Address = bitcore.Address; -var constants = require('../../constants'); var Encoding = require('./encoding'); var utils = require('../../utils'); -/** - * The Address Service builds upon the Database Service and the Bitcoin Service to add additional - * functionality for getting information by base58check encoded addresses. This includes getting the - * balance for an address, the history for a collection of addresses, and unspent outputs for - * constructing transactions. This is typically the core functionality for building a wallet. - * @param {Object} options - * @param {Node} options.node - An instance of the node - * @param {String} options.name - An optional name of the service - */ var AddressService = function(options) { BaseService.call(this, options); - - this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH; - this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH; - this.concurrency = options.concurrency || 20; - }; inherits(AddressService, BaseService); @@ -61,17 +45,9 @@ AddressService.prototype.start = function(callback) { }; AddressService.prototype.stop = function(callback) { - // TODO Keep track of ongoing db requests before shutting down - // this.node.services.bitcoind.removeListener('tx', this._bitcoindTransactionListener); - // this.node.services.bitcoind.removeListener('txleave', this._bitcoindTransactionLeaveListener); - // this.mempoolIndex.close(callback); setImmediate(callback); }; -/** - * Called by the Node to get the available API methods for this service, - * that can be exposed over the JSON-RPC interface. - */ AddressService.prototype.getAPIMethods = function() { return [ ['getBalance', this, this.getBalance, 2], @@ -84,34 +60,10 @@ AddressService.prototype.getAPIMethods = function() { ]; }; -/** - * Called by the Bus to get the available events for this service. - */ AddressService.prototype.getPublishEvents = function() { return []; - // return [ - // { - // name: 'address/transaction', - // scope: this, - // subscribe: this.subscribe.bind(this, 'address/transaction'), - // unsubscribe: this.unsubscribe.bind(this, 'address/transaction') - // }, - // { - // name: 'address/balance', - // scope: this, - // subscribe: this.subscribe.bind(this, 'address/balance'), - // unsubscribe: this.unsubscribe.bind(this, 'address/balance') - // } - // ]; }; -/** - * The Database Service will run this function when blocks are connected and - * disconnected to the chain during syncing and reorganizations. - * @param {Block} block - An instance of a Bitcore Block - * @param {Boolean} addOutput - If the block is being removed or added to the chain - * @param {Function} callback - */ AddressService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) { var self = this; @@ -481,478 +433,7 @@ AddressService.prototype.getBalance = function(address, queryMempool, callback) }); }; -/** - * Will give the input that spends an output if it exists with: - * inputTxId - The input txid hex string - * inputIndex - A number with the spending input index - * @param {String|Buffer} txid - The transaction hash with the output - * @param {Number} outputIndex - The output index in the transaction - * @param {Object} options - * @param {Object} options.queryMempool - Include mempool in results - * @param {Function} callback - */ -AddressService.prototype.getInputForOutput = function(txid, outputIndex, options, callback) { - $.checkArgument(_.isNumber(outputIndex)); - $.checkArgument(_.isObject(options)); - $.checkArgument(_.isFunction(callback)); - var self = this; - var txidBuffer; - if (Buffer.isBuffer(txid)) { - txidBuffer = txid; - } else { - txidBuffer = new Buffer(txid, 'hex'); - } - if (options.queryMempool) { - var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, outputIndex); - if (this.mempoolSpentIndex[spentIndexSyncKey]) { - return this._getSpentMempool(txidBuffer, outputIndex, callback); - } - } - var key = self._encoding.encodeInputKeyMap(txidBuffer, outputIndex); - var dbOptions = { - valueEncoding: 'binary', - keyEncoding: 'binary' - }; - this.db.get(key, dbOptions, function(err, buffer) { - if (err instanceof levelup.errors.NotFoundError) { - return callback(null, false); - } else if (err) { - return callback(err); - } - var value = self._encoding.decodeInputValueMap(buffer); - callback(null, { - inputTxId: value.inputTxId.toString('hex'), - inputIndex: value.inputIndex - }); - }); -}; -/** - * A streaming equivalent to `getInputs`, and returns a transform stream with data - * emitted in the same format as `getInputs`. - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Function} callback - */ -AddressService.prototype.createInputsStream = function(addressStr, options) { - var inputStream = new InputsTransformStream({ - address: new Address(addressStr, this.node.network), - tipHeight: this.node.services.db.tip.__height - }); - - var stream = this.createInputsDBStream(addressStr, options) - .on('error', function(err) { - // Forward the error - inputStream.emit('error', err); - inputStream.end(); - }).pipe(inputStream); - - return stream; - -}; - -AddressService.prototype.createInputsDBStream = function(addressStr, options) { - var stream; - var addrObj = this.encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - - if (options.start >= 0 && options.end >= 0) { - - var endBuffer = new Buffer(4); - endBuffer.writeUInt32BE(options.end, 0); - - var startBuffer = new Buffer(4); - // Because the key has additional data following it, we don't have an ability - // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number - // to be one value larger to include it. - var adjustedStart = options.start + 1; - startBuffer.writeUInt32BE(adjustedStart, 0); - - stream = this.db.createReadStream({ - gt: Buffer.concat([ - constants.PREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - endBuffer - ]), - lt: Buffer.concat([ - constants.PREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - startBuffer - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } else { - var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]); - stream = this.db.createReadStream({ - gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), - lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } - - return stream; -}; - -/** - * Will give inputs that spend previous outputs for an address as an object with: - * address - The base58check encoded address - * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' - * txid - A string of the transaction hash - * outputIndex - A number of corresponding transaction input - * height - The height of the block the transaction was included, will be -1 for mempool transactions - * confirmations - The number of confirmations, will equal 0 for mempool transactions - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Boolean} [options.queryMempool] - Include the mempool in the results - * @param {Function} callback - */ -AddressService.prototype.getInputs = function(addressStr, options, callback) { - - var self = this; - - var inputs = []; - - var addrObj = self._encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - - var stream = this.createInputsStream(addressStr, options); - var error; - - stream.on('data', function(input) { - inputs.push(input); - if (inputs.length > self.maxInputsQueryLength) { - log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for address '+ addressStr); - error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); - stream.end(); - } - }); - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('finish', function() { - if (error) { - return callback(error); - } - - if(options.queryMempool) { - self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { - if (err) { - return callback(err); - } - inputs = inputs.concat(mempoolInputs); - callback(null, inputs); - }); - } else { - callback(null, inputs); - } - - }); - - return stream; - -}; - -AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { - var self = this; - var mempoolInputs = []; - - var stream = self.mempoolIndex.createReadStream({ - gte: Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN - ]), - lte: Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MAX - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - var txid = data.value.slice(0, 32); - var inputIndex = data.value.readUInt32BE(32); - var timestamp = data.value.readDoubleBE(36); - var input = { - address: addressStr, - hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], - txid: txid.toString('hex'), //TODO use a buffer - inputIndex: inputIndex, - timestamp: timestamp, - height: -1, - confirmations: 0 - }; - mempoolInputs.push(input); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, mempoolInputs); - }); - -}; - -AddressService.prototype._getSpentMempool = function(txidBuffer, outputIndex, callback) { - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentIndexKey = Buffer.concat([ - constants.MEMPREFIXES.SPENTSMAP, - txidBuffer, - outputIndexBuffer - ]); - - this.mempoolIndex.get( - spentIndexKey, - function(err, mempoolValue) { - if (err) { - return callback(err); - } - var inputTxId = mempoolValue.slice(0, 32); - var inputIndex = mempoolValue.readUInt32BE(32); - callback(null, { - inputTxId: inputTxId.toString('hex'), - inputIndex: inputIndex - }); - } - ); -}; - -AddressService.prototype.createOutputsStream = function(addressStr, options) { - var outputStream = new OutputsTransformStream({ - address: new Address(addressStr, this.node.network), - tipHeight: this.node.services.db.tip.__height - }); - - var stream = this.createOutputsDBStream(addressStr, options) - .on('error', function(err) { - // Forward the error - outputStream.emit('error', err); - outputStream.end(); - }) - .pipe(outputStream); - - return stream; - -}; - -AddressService.prototype.createOutputsDBStream = function(addressStr, options) { - - var addrObj = this.encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - var stream; - - if (options.start >= 0 && options.end >= 0) { - - var endBuffer = new Buffer(4); - endBuffer.writeUInt32BE(options.end, 0); - - var startBuffer = new Buffer(4); - // Because the key has additional data following it, we don't have an ability - // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number - // to be one value larger to include it. - var startAdjusted = options.start + 1; - startBuffer.writeUInt32BE(startAdjusted, 0); - - stream = this.db.createReadStream({ - gt: Buffer.concat([ - constants.PREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - endBuffer - ]), - lt: Buffer.concat([ - constants.PREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - startBuffer - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } else { - var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]); - stream = this.db.createReadStream({ - gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), - lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } - - return stream; - -}; - -/** - * Will give outputs for an address as an object with: - * address - The base58check encoded address - * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' - * txid - A string of the transaction hash - * outputIndex - A number of corresponding transaction output - * height - The height of the block the transaction was included, will be -1 for mempool transactions - * satoshis - The satoshis value of the output - * script - The script of the output as a hex string - * confirmations - The number of confirmations, will equal 0 for mempool transactions - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Boolean} [options.queryMempool] - Include the mempool in the results - * @param {Function} callback - */ -AddressService.prototype.getOutputs = function(addressStr, options, callback) { - var self = this; - $.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.'); - $.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.'); - - var addrObj = self._encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - if (!hashTypeBuffer) { - return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr)); - } - - var outputs = []; - var stream = this.createOutputsStream(addressStr, options); - var error; - - stream.on('data', function(data) { - outputs.push(data); - if (outputs.length > self.maxOutputsQueryLength) { - log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for address ' + addressStr); - error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); - stream.end(); - } - }); - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('finish', function() { - if (error) { - return callback(error); - } - - if(options.queryMempool) { - self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { - if (err) { - return callback(err); - } - outputs = outputs.concat(mempoolOutputs); - callback(null, outputs); - }); - } else { - callback(null, outputs); - } - }); - - return stream; - -}; - -AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { - var self = this; - var mempoolOutputs = []; - - var stream = self.mempoolIndex.createReadStream({ - gte: Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN - ]), - lte: Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MAX - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - // Format of data: - // prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4 - var txid = data.key.slice(22, 54); - var outputIndex = data.key.readUInt32BE(54); - var value = self._encoding.decodeOutputMempoolValue(data.value); - var output = { - address: addressStr, - hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], - txid: txid.toString('hex'), //TODO use a buffer - outputIndex: outputIndex, - height: -1, - timestamp: value.timestamp, - satoshis: value.satoshis, - script: value.scriptBuffer.toString('hex'), //TODO use a buffer - confirmations: 0 - }; - mempoolOutputs.push(output); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, mempoolOutputs); - }); - -}; - -/** - * Will give unspent outputs for an address or an array of addresses. - * @param {Array|String} addresses - An array of addresses - * @param {Boolean} queryMempool - Include or exclude the mempool - * @param {Function} callback - */ AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) { var self = this; @@ -978,12 +459,6 @@ AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) }); }; -/** - * Will give unspent outputs for an address. - * @param {String} address - An address in base58check encoding - * @param {Boolean} queryMempool - Include or exclude the mempool - * @param {Function} callback - */ AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) { var self = this; @@ -1017,13 +492,6 @@ AddressService.prototype.getUtxosForAddress = function(address, queryMempool, ca }); }; -/** - * Will give the inverse of isSpent - * @param {Object} output - * @param {Object} options - * @param {Boolean} options.queryMempool - Include mempool in results - * @param {Function} callback - */ AddressService.prototype.isUnspent = function(output, options, callback) { $.checkArgument(_.isFunction(callback)); this.isSpent(output, options, function(spent) { @@ -1031,64 +499,6 @@ AddressService.prototype.isUnspent = function(output, options, callback) { }); }; -/** - * Will determine if an output is spent. - * @param {Object} output - An output as returned from getOutputs - * @param {Object} options - * @param {Boolean} options.queryMempool - Include mempool in results - * @param {Function} callback - */ -AddressService.prototype.isSpent = function(output, options, callback) { - $.checkArgument(_.isFunction(callback)); - var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; - var self = this; - var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; - var spent = self.node.services.bitcoind.isSpent(txid, output.outputIndex); - if (!spent && queryMempool) { - var txidBuffer = new Buffer(txid, 'hex'); - var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, output.outputIndex); - spent = self.mempoolSpentIndex[spentIndexSyncKey] ? true : false; - } - setImmediate(function() { - // TODO error should be the first argument? - callback(spent); - }); -}; - - -/** - * This will give the history for many addresses limited by a range of block heights (to limit - * the database lookup times) and/or paginated to limit the results length. - * - * The response format will be: - * { - * totalCount: 12 // the total number of items there are between the two heights - * items: [ - * { - * addresses: { - * '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX': { - * inputIndexes: [], - * outputIndexes: [0] - * } - * }, - * satoshis: 100, - * height: 300000, - * confirmations: 1, - * timestamp: 1442337090 // in seconds - * fees: 1000 // in satoshis - * tx: - * } - * ] - * } - * @param {Array} addresses - An array of addresses - * @param {Object} options - The options to limit the query - * @param {Number} [options.from] - The pagination "from" index - * @param {Number} [options.to] - The pagination "to" index - * @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height). - * @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive). - * @param {Boolean} [options.queryMempool] - Include the mempool in the query - * @param {Function} callback - */ AddressService.prototype.getAddressHistory = function(addresses, options, callback) { var self = this; @@ -1238,219 +648,5 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb }; -AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) { - var self = this; - var baseResult = { - appearanceIds: {}, - totalReceived: 0, - balance: 0, - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - - async.waterfall([ - function(next) { - self._getAddressConfirmedInputsSummary(address, baseResult, options, next); - }, - function(result, next) { - self._getAddressConfirmedOutputsSummary(address, result, options, next); - } - ], callback); - -}; - -AddressService.prototype._getAddressConfirmedInputsSummary = function(address, result, options, callback) { - $.checkArgument(address instanceof Address); - var self = this; - var error = null; - var count = 0; - - var inputsStream = self.createInputsStream(address, options); - inputsStream.on('data', function(input) { - var txid = input.txid; - result.appearanceIds[txid] = input.height; - - count++; - - if (count > self.maxInputsQueryLength) { - log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for summary of address ' + address.toString()); - error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); - inputsStream.end(); - } - - }); - - inputsStream.on('error', function(err) { - error = err; - }); - - inputsStream.on('end', function() { - if (error) { - return callback(error); - } - callback(null, result); - }); -}; - -AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) { - $.checkArgument(address instanceof Address); - $.checkArgument(!_.isUndefined(result) && - !_.isUndefined(result.appearanceIds) && - !_.isUndefined(result.unconfirmedAppearanceIds)); - - var self = this; - var count = 0; - - var outputStream = self.createOutputsStream(address, options); - var error = null; - - outputStream.on('data', function(output) { - - var txid = output.txid; - var outputIndex = output.outputIndex; - result.totalReceived += output.satoshis; - result.appearanceIds[txid] = output.height; - - if(!options.noBalance) { - - // Bitcoind's isSpent only works for confirmed transactions - var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex); - - if(!spentDB) { - result.balance += output.satoshis; - } - - if(options.queryMempool) { - // Check to see if this output is spent in the mempool and if so - // we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta) - var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey( - new Buffer(txid, 'hex'), // TODO: get buffer directly - outputIndex - ); - var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; - if(spentMempool) { - result.unconfirmedBalance -= output.satoshis; - } - } - } - - count++; - - if (count > self.maxOutputsQueryLength) { - log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for summary of address ' + address.toString()); - error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); - outputStream.end(); - } - - }); - - - outputStream.on('error', function(err) { - error = err; - }); - - outputStream.on('end', function() { - if (error) { - return callback(error); - } - callback(null, result); - }); - -}; - -AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(result, callback) { - result.txids = Object.keys(result.appearanceIds); - result.txids.sort(function(a, b) { - return result.appearanceIds[a] - result.appearanceIds[b]; - }); - result.unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds); - result.unconfirmedTxids.sort(function(a, b) { - return result.unconfirmedAppearanceIds[a] - result.unconfirmedAppearanceIds[b]; - }); - callback(null, result); -}; - -AddressService.prototype._getAddressMempoolSummary = function(address, options, result, callback) { - var self = this; - - // Skip if the options do not want to include the mempool - if (!options.queryMempool) { - return callback(null, result); - } - - var addressStr = address.toString(); - var hashBuffer = address.hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; - var addressIndexKey = self._encoding.encodeMempoolAddressIndexKey(hashBuffer, hashTypeBuffer); - - if(!this.mempoolAddressIndex[addressIndexKey]) { - return callback(null, result); - } - - async.waterfall([ - function(next) { - self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { - if (err) { - return next(err); - } - for(var i = 0; i < mempoolInputs.length; i++) { - var input = mempoolInputs[i]; - result.unconfirmedAppearanceIds[input.txid] = input.timestamp; - } - next(null, result); - }); - - }, function(result, next) { - self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { - if (err) { - return next(err); - } - for(var i = 0; i < mempoolOutputs.length; i++) { - var output = mempoolOutputs[i]; - - result.unconfirmedAppearanceIds[output.txid] = output.timestamp; - - if(!options.noBalance) { - var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey( - new Buffer(output.txid, 'hex'), // TODO: get buffer directly - output.outputIndex - ); - var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; - // Only add this to the balance if it's not spent in the mempool already - if(!spentMempool) { - result.unconfirmedBalance += output.satoshis; - } - } - } - next(null, result); - }); - } - ], callback); -}; - -AddressService.prototype._transformAddressSummaryFromResult = function(result, options) { - - var confirmedTxids = result.txids; - var unconfirmedTxids = result.unconfirmedTxids; - - var summary = { - totalReceived: result.totalReceived, - totalSpent: result.totalReceived - result.balance, - balance: result.balance, - appearances: confirmedTxids.length, - unconfirmedBalance: result.unconfirmedBalance, - unconfirmedAppearances: unconfirmedTxids.length - }; - - if (options.fullTxList) { - summary.appearanceIds = result.appearanceIds; - summary.unconfirmedAppearanceIds = result.unconfirmedAppearanceIds; - } else if (!options.noTxList) { - summary.txids = confirmedTxids.concat(unconfirmedTxids); - } - - return summary; - -}; module.exports = AddressService; diff --git a/lib/services/block/encoding.js b/lib/services/block/encoding.js new file mode 100644 index 00000000..bd17eec7 --- /dev/null +++ b/lib/services/block/encoding.js @@ -0,0 +1,45 @@ +'use strict'; + +function Encoding(servicePrefix) { + this.servicePrefix = servicePrefix; + this.hashPrefix = new Buffer('00', 'hex'); + this.heightPrefix = new Buffer('01', 'hex'); +} + +Encoding.prototype.encodeBlockHashKey = function(hash) { + return Buffer.concat([ this.servicePrefix, this.hashPrefix, new Buffer(hash, 'hex') ]); +}; + +Encoding.prototype.decodeBlockHashKey = function(buffer) { + return buffer.slice(3).toString('hex'); +}; + +Encoding.prototype.encodeBlockHashValue = function(hash) { + return new Buffer(hash, 'hex'); +}; + +Encoding.prototype.decodeBlockHashValue = function(buffer) { + return buffer.toString('hex'); +}; + +Encoding.prototype.encodeBlockHeightKey = function(height) { + var heightBuf = new Buffer(4); + heightBuf.writeUInt32BE(height); + return Buffer.concat([ this.servicePrefix, this.heightPrefix, heightBuf ]); +}; + +Encoding.prototype.decodeBlockHeightKey = function(buffer) { + return buffer.slice(3).readUInt32BE(); +}; + +Encoding.prototype.encodeBlockHeightValue = function(height) { + var heightBuf = new Buffer(4); + heightBuf.writeUInt32BE(height); + return heightBuf; +}; + +Encoding.prototype.decodeBlockHeightValue = function(buffer) { + return buffer.readUInt32BE(); +}; + +module.exports = Encoding; diff --git a/lib/services/block/index.js b/lib/services/block/index.js new file mode 100644 index 00000000..23ab4ae9 --- /dev/null +++ b/lib/services/block/index.js @@ -0,0 +1,330 @@ +'use strict'; + +var BaseService = require('../../service'); +var levelup = require('levelup'); +var bitcore = require('bitcore-lib'); +var Block = bitcore.Block; +var inherits = require('util').inherits; +var Encoding = require('./encoding'); +var index = require('../../'); +var log = index.log; +var BufferUtil = bitcore.util.buffer; +var utils = require('../../utils'); +var Reorg = require('./reorg'); +var $ = bitcore.util.preconditions; +var async = require('async'); +var Sync = require('./sync'); + +var BlockService = function(options) { + BaseService.call(this, options); + this.bitcoind = this.node.services.bitcoind; + this.db = this.node.services.db; + this._sync = new Sync(this.node, this); + this.syncing = true; + this._lockTimes = []; +}; + +inherits(BlockService, BaseService); + +BlockService.dependencies = [ + 'bitcoind', + 'db' +]; + +BlockService.prototype._log = function(msg, fn) { + if (!fn) { + return log.info('BlockService: ', msg); + } + +}; + +BlockService.prototype.start = function(callback) { + + var self = this; + + self.db.getPrefix(self.name, function(err, prefix) { + + if(err) { + return callback(err); + } + + self.prefix = prefix; + self.encoding = new Encoding(self.prefix); + self._setHandlers(); + callback(); + }); +}; + +BlockService.prototype._setHandlers = function() { + var self = this; + self.node.once('ready', function() { + + self.genesis = Block.fromBuffer(self.bitcoind.genesisBuffer); + self._loadTips(function(err) { + + if(err) { + throw err; + } + + self._log('Bitcoin network tip is currently: ' + self.bitcoind.tiphash + ' at height: ' + self.bitcoind.height); + self._sync.sync(); + + }); + + }); +}; + +BlockService.prototype.stop = function(callback) { + setImmediate(callback); +}; + +BlockService.prototype.pauseSync = function(callback) { + var self = this; + self._lockTimes.push(process.hrtime()); + if (self._sync.syncing) { + self._sync.once('synced', function() { + self._sync.paused = true; + callback(); + }); + } else { + self._sync.paused = true; + setImmediate(callback); + } +}; + +BlockService.prototype.resumeSync = function() { + this._log('Attempting to resume sync', log.debug); + var time = this._lockTimes.shift(); + if (this._lockTimes.length === 0) { + if (time) { + this._log('sync lock held for: ' + utils.diffTime(time) + ' secs', log.debug); + } + this._sync.paused = false; + this._sync.sync(); + } +}; + +BlockService.prototype.detectReorg = function(blocks) { + + var self = this; + + if (!blocks || blocks.length === 0) { + return; + } + + var tipHash = self.reorgTipHash || self.tip.hash; + var chainMembers = []; + + var loopIndex = 0; + var overallCounter = 0; + + while(overallCounter < blocks.length) { + + if (loopIndex >= blocks.length) { + overallCounter++; + loopIndex = 0; + } + + var prevHash = BufferUtil.reverse(blocks[loopIndex].header.prevHash).toString('hex'); + if (prevHash === tipHash) { + tipHash = blocks[loopIndex].hash; + chainMembers.push(blocks[loopIndex]); + } + loopIndex++; + + } + + for(var i = 0; i < blocks.length; i++) { + if (chainMembers.indexOf(blocks[i]) === -1) { + return blocks[i]; + } + self.reorgTipHash = blocks[i].hash; + } + +}; + +BlockService.prototype.handleReorg = function(forkBlock, callback) { + + var self = this; + self.printTipInfo('Reorg detected!'); + + self.reorg = true; + + var reorg = new Reorg(self.node, self); + + reorg.handleReorg(forkBlock.hash, function(err) { + + if(err) { + self._log('Reorg failed! ' + err, log.error); + self.node.stop(function() {}); + throw err; + } + + self.printTipInfo('Reorg successful!'); + self.reorg = false; + callback(); + + }); + +}; + +BlockService.prototype.printTipInfo = function(prependedMessage) { + + this._log( + prependedMessage + ' Serial Tip: ' + this.tip.hash + + ' Concurrent tip: ' + this.concurrentTip.hash + + ' Bitcoind tip: ' + this.bitcoind.tiphash + ); + +}; + +BlockService.prototype._loadTips = function(callback) { + + var self = this; + + var tipStrings = ['tip', 'concurrentTip']; + + async.each(tipStrings, function(tip, next) { + + self.db.get(self.dbPrefix + tip, self.dbOptions, function(err, tipData) { + + if(err && !(err instanceof levelup.errors.NotFoundError)) { + return next(err); + } + + var hash; + if (!tipData) { + hash = new Array(65).join('0'); + self[tip] = { + height: -1, + hash: hash, + '__height': -1 + }; + return next(); + } + + hash = tipData.slice(0, 32).toString('hex'); + + self.bitcoind.getBlock(hash, function(err, block) { + + if(err) { + return next(err); + } + + self[tip] = block; + self._log('loaded ' + tip + ' hash: ' + block.hash + ' height: ' + block.__height); + next(); + + }); + }); + + }, callback); + +}; + +BlockService.prototype.getConcurrentBlockOperations = function(block, add, callback) { + var operations = []; + + async.each( + this.node.services, + function(mod, next) { + if(mod.concurrentBlockHandler) { + $.checkArgument(typeof mod.concurrentBlockHandler === 'function', 'concurrentBlockHandler must be a function'); + + mod.concurrentBlockHandler.call(mod, block, add, function(err, ops) { + if (err) { + return next(err); + } + if (ops) { + $.checkArgument(Array.isArray(ops), 'concurrentBlockHandler for ' + mod.name + ' returned non-array'); + operations = operations.concat(ops); + } + + next(); + }); + } else { + setImmediate(next); + } + }, + function(err) { + if (err) { + return callback(err); + } + + callback(null, operations); + } + ); +}; + +BlockService.prototype.getSerialBlockOperations = function(block, add, callback) { + var operations = []; + + async.eachSeries( + this.node.services, + function(mod, next) { + if(mod.blockHandler) { + $.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function'); + + mod.blockHandler.call(mod, block, add, function(err, ops) { + if (err) { + return next(err); + } + if (ops) { + $.checkArgument(Array.isArray(ops), 'blockHandler for ' + mod.name + ' returned non-array'); + operations = operations.concat(ops); + } + + next(); + }); + } else { + setImmediate(next); + } + }, + function(err) { + if (err) { + return callback(err); + } + + callback(null, operations); + } + ); +}; + +BlockService.prototype.getTipOperation = function(block, add) { + var heightBuffer = new Buffer(4); + var tipData; + + if(add) { + heightBuffer.writeUInt32BE(block.__height); + tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]); + } else { + heightBuffer.writeUInt32BE(block.__height - 1); + tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]); + } + + return { + type: 'put', + key: this.dbPrefix + 'tip', + value: tipData + }; +}; + +BlockService.prototype.getConcurrentTipOperation = function(block, add) { + var heightBuffer = new Buffer(4); + var tipData; + if(add) { + heightBuffer.writeUInt32BE(block.__height); + tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]); + } else { + heightBuffer.writeUInt32BE(block.__height - 1); + tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]); + } + + return { + type: 'put', + key: this.dbPrefix + 'concurrentTip', + value: tipData + }; +}; + +module.exports = BlockService; diff --git a/lib/services/db/reorg.js b/lib/services/block/reorg.js similarity index 75% rename from lib/services/db/reorg.js rename to lib/services/block/reorg.js index 42370c62..d981ba0e 100644 --- a/lib/services/db/reorg.js +++ b/lib/services/block/reorg.js @@ -5,9 +5,9 @@ var BufferUtil = bitcore.util.buffer; var log = index.log; var async = require('async'); -function Reorg(node, db) { +function Reorg(node, block) { this.node = node; - this.db = db; + this.block = block; } Reorg.prototype.handleReorg = function(newBlockHash, callback) { @@ -18,7 +18,7 @@ Reorg.prototype.handleReorg = function(newBlockHash, callback) { return callback(err); } - self.findCommonAncestorAndNewHashes(self.db.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) { + self.findCommonAncestorAndNewHashes(self.block.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) { if(err) { return callback(err); } @@ -36,11 +36,11 @@ Reorg.prototype.handleReorg = function(newBlockHash, callback) { Reorg.prototype.handleConcurrentReorg = function(callback) { var self = this; - if(self.db.concurrentTip.hash === self.db.tip.hash) { + if(self.block.concurrentTip.hash === self.block.tip.hash) { return callback(); } - self.findCommonAncestorAndNewHashes(self.db.concurrentTip.hash, self.db.tip.hash, function(err, commonAncestor, newHashes) { + self.findCommonAncestorAndNewHashes(self.block.concurrentTip.hash, self.block.tip.hash, function(err, commonAncestor, newHashes) { if(err) { return callback(err); } @@ -60,28 +60,28 @@ Reorg.prototype.rewindConcurrentTip = function(commonAncestor, callback) { async.whilst( function() { - return self.db.concurrentTip.hash !== commonAncestor; + return self.block.concurrentTip.hash !== commonAncestor; }, function(next) { - self.db.getConcurrentBlockOperations(self.db.concurrentTip, false, function(err, operations) { + self.block.getConcurrentBlockOperations(self.block.concurrentTip, false, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getConcurrentTipOperation(self.db.concurrentTip, false)); - self.db.batch(operations, function(err) { + operations.push(self.block.getConcurrentTipOperation(self.block.concurrentTip, false)); + self.block.batch(operations, function(err) { if(err) { return next(err); } - var prevHash = BufferUtil.reverse(self.db.concurrentTip.header.prevHash).toString('hex'); + var prevHash = BufferUtil.reverse(self.block.concurrentTip.header.prevHash).toString('hex'); self.node.services.bitcoind.getBlock(prevHash, function(err, block) { if(err) { return next(err); } - self.db.concurrentTip = block; + self.block.concurrentTip = block; next(); }); }); @@ -102,18 +102,18 @@ Reorg.prototype.fastForwardConcurrentTip = function(newHashes, callback) { return next(err); } - self.db.getConcurrentBlockOperations(block, true, function(err, operations) { + self.block.getConcurrentBlockOperations(block, true, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getConcurrentTipOperation(block, true)); - self.db.batch(operations, function(err) { + operations.push(self.block.getConcurrentTipOperation(block, true)); + self.block.batch(operations, function(err) { if(err) { return next(err); } - self.db.concurrentTip = block; + self.block.concurrentTip = block; next(); }); }); @@ -126,27 +126,27 @@ Reorg.prototype.rewindBothTips = function(commonAncestor, callback) { async.whilst( function() { - return self.db.tip.hash !== commonAncestor; + return self.block.tip.hash !== commonAncestor; }, function(next) { async.parallel( [ function(next) { - self.db.getConcurrentBlockOperations(self.db.concurrentTip, false, function(err, operations) { + self.block.getConcurrentBlockOperations(self.block.concurrentTip, false, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getConcurrentTipOperation(self.db.concurrentTip, false)); + operations.push(self.block.getConcurrentTipOperation(self.block.concurrentTip, false)); next(null, operations); }); }, function(next) { - self.db.getSerialBlockOperations(self.db.tip, false, function(err, operations) { + self.block.getSerialBlockOperations(self.block.tip, false, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getTipOperation(self.db.tip, false)); + operations.push(self.block.getTipOperation(self.block.tip, false)); next(null, operations); }); } @@ -157,20 +157,20 @@ Reorg.prototype.rewindBothTips = function(commonAncestor, callback) { } var operations = results[0].concat(results[1]); - self.db.batch(operations, function(err) { + self.block.batch(operations, function(err) { if(err) { return next(err); } - var prevHash = BufferUtil.reverse(self.db.tip.header.prevHash).toString('hex'); + var prevHash = BufferUtil.reverse(self.block.tip.header.prevHash).toString('hex'); self.node.services.bitcoind.getBlock(prevHash, function(err, block) { if(err) { return next(err); } - self.db.concurrentTip = block; - self.db.tip = block; + self.block.concurrentTip = block; + self.block.tip = block; next(); }); }); @@ -193,22 +193,22 @@ Reorg.prototype.fastForwardBothTips = function(newHashes, callback) { async.parallel( [ function(next) { - self.db.getConcurrentBlockOperations(block, true, function(err, operations) { + self.block.getConcurrentBlockOperations(block, true, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getConcurrentTipOperation(block, true)); + operations.push(self.block.getConcurrentTipOperation(block, true)); next(null, operations); }); }, function(next) { - self.db.getSerialBlockOperations(block, true, function(err, operations) { + self.block.getSerialBlockOperations(block, true, function(err, operations) { if(err) { return next(err); } - operations.push(self.db.getTipOperation(block, true)); + operations.push(self.block.getTipOperation(block, true)); next(null, operations); }); } @@ -220,13 +220,13 @@ Reorg.prototype.fastForwardBothTips = function(newHashes, callback) { var operations = results[0].concat(results[1]); - self.db.batch(operations, function(err) { + self.block.batch(operations, function(err) { if(err) { return next(err); } - self.db.concurrentTip = block; - self.db.tip = block; + self.block.concurrentTip = block; + self.block.tip = block; next(); }); } diff --git a/lib/services/db/sync.js b/lib/services/block/sync.js similarity index 82% rename from lib/services/db/sync.js rename to lib/services/block/sync.js index 33728073..f4e79641 100644 --- a/lib/services/db/sync.js +++ b/lib/services/block/sync.js @@ -10,23 +10,24 @@ var Block = bitcore.Block; var index = require('../../index'); var log = index.log; -function BlockStream(highWaterMark, db, sync) { +function BlockStream(highWaterMark, sync) { Readable.call(this, {objectMode: true, highWaterMark: highWaterMark}); this.sync = sync; - this.db = db; - this.dbTip = this.db.tip; + this.block = this.sync.block; + this.dbTip = this.block.tip; this.lastReadHeight = this.dbTip.__height; this.lastEmittedHash = this.dbTip.hash; this.queue = []; this.processing = false; - this.bitcoind = this.db.bitcoind; + this.bitcoind = this.block.bitcoind; } inherits(BlockStream, Readable); -function ProcessConcurrent(highWaterMark, db) { +function ProcessConcurrent(highWaterMark, sync) { Transform.call(this, {objectMode: true, highWaterMark: highWaterMark}); - this.db = db; + this.block = sync.block; + this.db = sync.db; this.operations = []; this.lastBlock = 0; this.blockCount = 0; @@ -34,35 +35,38 @@ function ProcessConcurrent(highWaterMark, db) { inherits(ProcessConcurrent, Transform); -function ProcessSerial(highWaterMark, db, tip) { +function ProcessSerial(highWaterMark, sync) { Writable.call(this, {objectMode: true, highWaterMark: highWaterMark}); - this.db = db; - this.tip = tip; + this.block = sync.block; + this.db = sync.db; + this.tip = sync.block.tip; this.processBlockStartTime = []; this._lastReportedTime = Date.now(); } inherits(ProcessSerial, Writable); -function ProcessBoth(highWaterMark, db) { +function ProcessBoth(highWaterMark, sync) { Writable.call(this, {objectMode: true, highWaterMark: highWaterMark}); - this.db = db; + this.db = sync.db; } inherits(ProcessBoth, Writable); -function WriteStream(highWaterMark, db) { +function WriteStream(highWaterMark, sync) { Writable.call(this, {objectMode: true, highWaterMark: highWaterMark}); - this.db = db; + this.db = sync.db; this.writeTime = 0; this.lastConcurrentOutputHeight = 0; } inherits(WriteStream, Writable); -function Sync(node, db) { +function Sync(node, block) { this.node = node; - this.db = db; + this.db = this.node.services.db; + this.block = block; + console.log(block); this.syncing = false; this.paused = false; //we can't sync while one of our indexes is reading/writing separate from us this.highWaterMark = 10; @@ -80,10 +84,10 @@ Sync.prototype.sync = function() { self.syncing = true; - var blockStream = new BlockStream(self.highWaterMark, self.db, self); - var processConcurrent = new ProcessConcurrent(self.highWaterMark, self.db); - var writeStream = new WriteStream(self.highWaterMark, self.db); - var processSerial = new ProcessSerial(self.highWaterMark, self.db, self.db.tip); + var blockStream = new BlockStream(self.highWaterMark, self); + var processConcurrent = new ProcessConcurrent(self.highWaterMark, self); + var writeStream = new WriteStream(self.highWaterMark, self); + var processSerial = new ProcessSerial(self.highWaterMark, self); self._handleErrors(blockStream); self._handleErrors(processConcurrent); @@ -238,7 +242,7 @@ BlockStream.prototype._getBlocks = function(heights, callback) { ProcessSerial.prototype._reportStatus = function() { if ((Date.now() - this._lastReportedTime) > 1000) { this._lastReportedTime = Date.now(); - log.info('Sync: current height is: ' + this.db.tip.__height); + log.info('Sync: current height is: ' + this.block.tip.__height); } }; @@ -246,7 +250,7 @@ ProcessSerial.prototype._write = function(block, enc, callback) { var self = this; function check() { - return self.db.concurrentTip.__height >= block.__height; + return self.block.concurrentTip.__height >= block.__height; } if(check()) { @@ -255,7 +259,7 @@ ProcessSerial.prototype._write = function(block, enc, callback) { self.db.once('concurrentaddblock', function() { if(!check()) { - var err = new Error('Concurrent block ' + self.db.concurrentTip.__height + ' is less than ' + block.__height); + var err = new Error('Concurrent block ' + self.block.concurrentTip.__height + ' is less than ' + block.__height); return self.emit('error', err); } self._process(block, callback); @@ -271,7 +275,7 @@ ProcessSerial.prototype._process = function(block, callback) { return callback(err); } - operations.push(self.db.getTipOperation(block, true)); + operations.push(self.block.getTipOperation(block, true)); var obj = { tip: block, @@ -285,7 +289,7 @@ ProcessSerial.prototype._process = function(block, callback) { return callback(err); } - self.db.tip = block; + self.block.tip = block; self._reportStatus(); self.db.emit('addblock'); @@ -309,7 +313,7 @@ ProcessConcurrent.prototype._transform = function(block, enc, callback) { self.operations = self.operations.concat(operations); if(self.blockCount >= 1) { - self.operations.push(self.db.getConcurrentTipOperation(block, true)); + self.operations.push(self.block.getConcurrentTipOperation(block, true)); var obj = { concurrentTip: block, operations: self.operations @@ -326,7 +330,7 @@ ProcessConcurrent.prototype._transform = function(block, enc, callback) { ProcessConcurrent.prototype._flush = function(callback) { if(this.operations.length) { - this.operations.push(this.db.getConcurrentTipOperation(this.lastBlock, true)); + this.operations.push(this.block.getConcurrentTipOperation(this.lastBlock, true)); this.operations = []; return callback(null, this.operations); } @@ -344,9 +348,9 @@ WriteStream.prototype._write = function(obj, enc, callback) { return callback(err); } - self.db.concurrentTip = obj.concurrentTip; + self.block.concurrentTip = obj.concurrentTip; self.db.emit('concurrentaddblock'); - self.lastConcurrentOutputHeight = self.db.concurrentTip.__height; + self.lastConcurrentOutputHeight = self.block.concurrentTip.__height; callback(); }); }; @@ -359,7 +363,7 @@ ProcessBoth.prototype._write = function(block, encoding, callback) { if(err) { return callback(err); } - operations.push(self.db.getConcurrentTipOperation(block, true)); + operations.push(self.block.getConcurrentTipOperation(block, true)); next(null, operations); }); }, function(next) { @@ -367,7 +371,7 @@ ProcessBoth.prototype._write = function(block, encoding, callback) { if(err) { return callback(err); } - operations.push(self.db.getTipOperation(block, true)); + operations.push(self.block.getTipOperation(block, true)); next(null, operations); }); }], function(err, results) { @@ -379,8 +383,8 @@ ProcessBoth.prototype._write = function(block, encoding, callback) { if(err) { return callback(err); } - self.db.tip = block; - self.db.concurrentTip = block; + self.block.tip = block; + self.block.concurrentTip = block; callback(); }); }); diff --git a/lib/services/db/index.js b/lib/services/db/index.js index 4d481abb..1acb0569 100644 --- a/lib/services/db/index.js +++ b/lib/services/db/index.js @@ -7,25 +7,14 @@ var levelup = require('levelup'); var leveldown = require('leveldown'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore-lib'); -var BufferUtil = bitcore.util.buffer; var Networks = bitcore.Networks; var Block = bitcore.Block; var $ = bitcore.util.preconditions; var index = require('../../'); var log = index.log; var Service = require('../../service'); -var Sync = require('./sync'); -var Reorg = require('./reorg'); -var Block = bitcore.Block; -var utils = require('../../utils'); - /* - * @param {Object} options - * @param {Node} options.node - A reference to the node - * @param {Node} options.store - A levelup backend store - */ function DB(options) { - /* jshint maxstatements: 20 */ if (!(this instanceof DB)) { return new DB(options); @@ -60,43 +49,14 @@ function DB(options) { this.subscriptions = {}; - this._sync = new Sync(this.node, this); - this.syncing = true; this.bitcoind = this.node.services.bitcoind; this._operationsQueue = []; - this._lockTimes = []; } util.inherits(DB, Service); DB.dependencies = ['bitcoind']; -DB.prototype.pauseSync = function(callback) { - var self = this; - self._lockTimes.push(process.hrtime()); - if (self._sync.syncing) { - self._sync.once('synced', function() { - self._sync.paused = true; - callback(); - }); - } else { - self._sync.paused = true; - setImmediate(callback); - } -}; - -DB.prototype.resumeSync = function() { - log.debug('Attempting to resume sync'); - var time = this._lockTimes.shift(); - if (this._lockTimes.length === 0) { - if (time) { - log.debug('sync lock held for: ' + utils.diffTime(time) + ' secs'); - } - this._sync.paused = false; - this._sync.sync(); - } -}; - DB.prototype._setDataPath = function() { $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); if (this.node.network === Networks.livenet) { @@ -159,21 +119,6 @@ DB.prototype.start = function(callback) { self._store = levelup(self.dataPath, { db: self.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'}); - self.node.once('ready', function() { - - self.genesis = Block.fromBuffer(self.bitcoind.genesisBuffer); - self.loadTips(function(err) { - - if(err) { - throw err; - } - - log.info('Bitcoin network tip is currently: ' + self.bitcoind.tiphash + ' at height: ' + self.bitcoind.height); - self._sync.sync(); - - }); - - }); setImmediate(function() { self._checkVersion(self._setVersion.bind(self, callback)); @@ -239,80 +184,6 @@ DB.prototype.createKeyStream = function(op) { return stream; }; -DB.prototype.detectReorg = function(blocks) { - - var self = this; - - if (!blocks || blocks.length === 0) { - return; - } - - var tipHash = self.reorgTipHash || self.tip.hash; - var chainMembers = []; - - var loopIndex = 0; - var overallCounter = 0; - - while(overallCounter < blocks.length) { - - if (loopIndex >= blocks.length) { - overallCounter++; - loopIndex = 0; - } - - var prevHash = BufferUtil.reverse(blocks[loopIndex].header.prevHash).toString('hex'); - if (prevHash === tipHash) { - tipHash = blocks[loopIndex].hash; - chainMembers.push(blocks[loopIndex]); - } - loopIndex++; - - } - - for(var i = 0; i < blocks.length; i++) { - if (chainMembers.indexOf(blocks[i]) === -1) { - return blocks[i]; - } - self.reorgTipHash = blocks[i].hash; - } - -}; - -DB.prototype.handleReorg = function(forkBlock, callback) { - - var self = this; - self.printTipInfo('Reorg detected!'); - - self.reorg = true; - - var reorg = new Reorg(self.node, self); - - reorg.handleReorg(forkBlock.hash, function(err) { - - if(err) { - log.error('Reorg failed! ' + err); - self.node.stop(function() {}); - throw err; - } - - self.printTipInfo('Reorg successful!'); - self.reorg = false; - callback(); - - }); - -}; - -DB.prototype.printTipInfo = function(prependedMessage) { - - log.info( - prependedMessage + ' Serial Tip: ' + this.tip.hash + - ' Concurrent tip: ' + this.concurrentTip.hash + - ' Bitcoind tip: ' + this.bitcoind.tiphash - ); - -}; - DB.prototype.stop = function(callback) { var self = this; self._stopping = true; @@ -335,159 +206,11 @@ DB.prototype.getAPIMethods = function() { return []; }; -DB.prototype.loadTips = function(callback) { - - var self = this; - - var tipStrings = ['tip', 'concurrentTip']; - - async.each(tipStrings, function(tip, next) { - - self._store.get(self.dbPrefix + tip, self.dbOptions, function(err, tipData) { - - if(err && !(err instanceof levelup.errors.NotFoundError)) { - return next(err); - } - - var hash; - if (!tipData) { - hash = new Array(65).join('0'); - self[tip] = { - height: -1, - hash: hash, - '__height': -1 - }; - return next(); - } - - hash = tipData.slice(0, 32).toString('hex'); - - self.bitcoind.getBlock(hash, function(err, block) { - - if(err) { - return next(err); - } - - self[tip] = block; - log.info('loaded ' + tip + ', hash: ' + block.hash + ' height: ' + block.__height); - next(); - - }); - }); - - }, callback); - -}; DB.prototype.getPublishEvents = function() { return []; }; -DB.prototype.getConcurrentBlockOperations = function(block, add, callback) { - var operations = []; - - async.each( - this.node.services, - function(mod, next) { - if(mod.concurrentBlockHandler) { - $.checkArgument(typeof mod.concurrentBlockHandler === 'function', 'concurrentBlockHandler must be a function'); - - mod.concurrentBlockHandler.call(mod, block, add, function(err, ops) { - if (err) { - return next(err); - } - if (ops) { - $.checkArgument(Array.isArray(ops), 'concurrentBlockHandler for ' + mod.name + ' returned non-array'); - operations = operations.concat(ops); - } - - next(); - }); - } else { - setImmediate(next); - } - }, - function(err) { - if (err) { - return callback(err); - } - - callback(null, operations); - } - ); -}; - -DB.prototype.getSerialBlockOperations = function(block, add, callback) { - var operations = []; - - async.eachSeries( - this.node.services, - function(mod, next) { - if(mod.blockHandler) { - $.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function'); - - mod.blockHandler.call(mod, block, add, function(err, ops) { - if (err) { - return next(err); - } - if (ops) { - $.checkArgument(Array.isArray(ops), 'blockHandler for ' + mod.name + ' returned non-array'); - operations = operations.concat(ops); - } - - next(); - }); - } else { - setImmediate(next); - } - }, - function(err) { - if (err) { - return callback(err); - } - - callback(null, operations); - } - ); -}; - -DB.prototype.getTipOperation = function(block, add) { - var heightBuffer = new Buffer(4); - var tipData; - - if(add) { - heightBuffer.writeUInt32BE(block.__height); - tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]); - } else { - heightBuffer.writeUInt32BE(block.__height - 1); - tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]); - } - - return { - type: 'put', - key: this.dbPrefix + 'tip', - value: tipData - }; -}; - -DB.prototype.getConcurrentTipOperation = function(block, add) { - var heightBuffer = new Buffer(4); - var tipData; - if(add) { - heightBuffer.writeUInt32BE(block.__height); - tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]); - } else { - heightBuffer.writeUInt32BE(block.__height - 1); - tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]); - } - - return { - type: 'put', - key: this.dbPrefix + 'concurrentTip', - value: tipData - }; -}; - DB.prototype.getPrefix = function(service, callback) { var self = this; diff --git a/lib/services/timestamp/index.js b/lib/services/timestamp/index.js index fd59591d..10affa6c 100644 --- a/lib/services/timestamp/index.js +++ b/lib/services/timestamp/index.js @@ -1,13 +1,16 @@ 'use strict'; + var Encoding = require('./encoding'); var BaseService = require('../../service'); var inherits = require('util').inherits; -var MAINNET_BITCOIN_GENESIS_TIME = 1296688602; +var LRU = require('lru-cache'); +var utils = require('../../../lib/utils'); function TimestampService(options) { BaseService.call(this, options); this.currentBlock = null; this.currentTimestamp = null; + this._blockHandlerQueue = LRU(50); } inherits(TimestampService, BaseService); @@ -28,86 +31,72 @@ TimestampService.prototype.start = function(callback) { self.encoding = new Encoding(self.prefix); callback(); }); + + self.counter = 0; }; TimestampService.prototype.stop = function(callback) { setImmediate(callback); }; +TimestampService.prototype._processBlockHandlerQueue = function(block) { + var self = this; + //do we have a record in the queue that is waiting on me to consume it? + var prevHash = utils.getPrevHashString(block); + if (prevHash.length !== 64) { + return; + } + + var timestamp = self._blockHandlerQueue.get(prevHash); + + if (timestamp) { + if (block.header.timestamp <= timestamp) { + timestamp = block.header.timestamp + 1; + } + self.counter++; + timestamp = block.header.timestamp; + self._blockHandlerQueue.del(prevHash); + } else { + timestamp = block.header.timestamp; + } + + self._blockHandlerQueue.set(block.hash, timestamp); + return timestamp; +}; + TimestampService.prototype.blockHandler = function(block, connectBlock, callback) { + var self = this; - var action = 'put'; - if (!connectBlock) { - action = 'del'; + var action = connectBlock ? 'put' : 'del'; + + var timestamp = self._processBlockHandlerQueue(block); + + if (!timestamp) { + return callback(null, []); } var operations = []; - function getLastTimestamp(next) { - if(block.__height === 0) { - return next(null, (block.header.timestamp - 1)); + operations = operations.concat([ + { + type: action, + key: self.encoding.encodeTimestampBlockKey(timestamp), + value: self.encoding.encodeTimestampBlockValue(block.header.hash) + }, + { + type: action, + key: self.encoding.encodeBlockTimestampKey(block.header.hash), + value: self.encoding.encodeBlockTimestampValue(timestamp) } + ]); - self.getTimestamp(block.toObject().header.prevHash, next); - } - - getLastTimestamp(function(err, lastTimestamp) { - if(err) { - return callback(err); - } - - var timestamp = block.header.timestamp; - if(timestamp <= lastTimestamp) { - timestamp = lastTimestamp + 1; - } - - self.currentBlock = block.hash; - self.currentTimestamp = timestamp; - - operations = operations.concat( - [ - { - type: action, - key: self.encoding.encodeTimestampBlockKey(timestamp), - value: self.encoding.encodeTimestampBlockValue(block.header.hash) - }, - { - type: action, - key: self.encoding.encodeBlockTimestampKey(block.header.hash), - value: self.encoding.encodeBlockTimestampValue(timestamp) - } - ] - ); - - callback(null, operations); - }); -}; - -TimestampService.prototype.getTimestamp = function(hash, callback) { - var self = this; - - if (hash === self.currentBlock) { - return setImmediate(function() { - callback(null, self.currentTimestamp); - }); - } - - var key = self.encoding.encodeBlockTimestampKey(hash); - self.db.get(key, function(err, buffer) { - if(err) { - return callback(err); - } - - return callback(null, self.encoding.decodeBlockTimestampValue(buffer)); - }); + callback(null, operations); }; TimestampService.prototype.getBlockHeights = function(timestamps, callback) { var self = this; timestamps.sort(); - //due to a bug in the encoding routines, thers is a minimum timestamp that - //can be searched for, which is 1296688602 (the bitcoin mainnet genesis block timestamps = timestamps.map(function(timestamp) { return timestamp >= MAINNET_BITCOIN_GENESIS_TIME ? timestamp : MAINNET_BITCOIN_GENESIS_TIME; }); diff --git a/lib/services/transaction/index.js b/lib/services/transaction/index.js index fff4afac..3e565496 100644 --- a/lib/services/transaction/index.js +++ b/lib/services/transaction/index.js @@ -34,7 +34,7 @@ TransactionService.prototype.start = function(callback) { self.db = this.node.services.db; - self.node.services.db.getPrefix(self.name, function(err, prefix) { + self.db.getPrefix(self.name, function(err, prefix) { if(err) { return callback(err); } diff --git a/lib/services/wallet-api/index.js b/lib/services/wallet-api/index.js index ba3baa6f..e29849de 100644 --- a/lib/services/wallet-api/index.js +++ b/lib/services/wallet-api/index.js @@ -1307,25 +1307,9 @@ WalletService.prototype._endpointJobStatus = function() { }; -WalletService.prototype._endpointGetInfo = function() { - var self = this; - return function(req, res) { - res.jsonp({ - result: 'ok', - dbheight: self.node.services.db.tip.__height, - dbhash: self.node.services.db.tip.hash, - bitcoindheight: self.node.services.bitcoind.height, - bitcoindhash: self.node.services.bitcoind.tiphash - }); - }; -}; - WalletService.prototype._setupReadOnlyRoutes = function(app) { var s = this; - app.get('/info', - s._endpointGetInfo() - ); app.get('/wallets/:walletId/utxos', s._endpointUTXOs() ); diff --git a/lib/services/web/index.js b/lib/services/web/index.js index 443c913b..5e7d59a0 100644 --- a/lib/services/web/index.js +++ b/lib/services/web/index.js @@ -103,6 +103,10 @@ WebService.prototype.setupAllRoutes = function() { var subApp = new express(); var service = this.node.services[key]; + this.app.get('/info', + this._endpointGetInfo() + ); + if(service.getRoutePrefix && service.setupRoutes) { this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp); this.node.services[key].setupRoutes(subApp, express); @@ -275,4 +279,17 @@ WebService.prototype.transformHttpsOptions = function() { }; }; +WebService.prototype._endpointGetInfo = function() { + var self = this; + return function(req, res) { + res.jsonp({ + result: 'ok', + dbheight: self.node.services.block.tip.__height, + dbhash: self.node.services.block.tip.hash, + bitcoindheight: self.node.services.bitcoind.height, + bitcoindhash: self.node.services.bitcoind.tiphash + }); + }; +}; + module.exports = WebService; diff --git a/lib/transaction.js b/lib/transaction.js deleted file mode 100644 index f55df37b..00000000 --- a/lib/transaction.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -var async = require('async'); -var levelup = require('levelup'); -var bitcore = require('bitcore-lib'); -var Transaction = bitcore.Transaction; - -var MAX_TRANSACTION_LIMIT = 5; - -Transaction.prototype.populateInputs = function(db, poolTransactions, callback) { - var self = this; - - if(this.isCoinbase()) { - return setImmediate(callback); - } - - async.eachLimit( - this.inputs, - db.maxTransactionLimit || MAX_TRANSACTION_LIMIT, - function(input, next) { - self._populateInput(db, input, poolTransactions, next); - }, - callback - ); -}; - -Transaction.prototype._populateInput = function(db, input, poolTransactions, callback) { - if (!input.prevTxId || !Buffer.isBuffer(input.prevTxId)) { - return callback(new Error('Input is expected to have prevTxId as a buffer')); - } - var txid = input.prevTxId.toString('hex'); - db.getTransaction(txid, true, function(err, prevTx) { - if(err instanceof levelup.errors.NotFoundError) { - // Check the pool for transaction - for(var i = 0; i < poolTransactions.length; i++) { - if(txid === poolTransactions[i].hash) { - input.output = poolTransactions[i].outputs[input.outputIndex]; - return callback(); - } - } - - return callback(new Error('Previous tx ' + input.prevTxId.toString('hex') + ' not found')); - } else if(err) { - callback(err); - } else { - input.output = prevTx.outputs[input.outputIndex]; - callback(); - } - }); -}; - -Transaction.prototype._checkSpent = function(db, input, poolTransactions, callback) { - // TODO check and see if another transaction in the pool spent the output - db.isSpentDB(input, function(spent) { - if(spent) { - return callback(new Error('Input already spent')); - } else { - callback(); - } - }); -}; - -module.exports = Transaction; diff --git a/lib/utils.js b/lib/utils.js index 97d5928f..60e50e80 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,7 @@ 'use strict'; +var bitcore = require('bitcore-lib'); +var BufferUtil = bitcore.util.buffer; var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1 var utils = {}; @@ -76,4 +78,9 @@ utils.diffTime = function(time) { return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0); }; +utils.getPrevHashString = function(block) { + return BufferUtil.reverse(block.header.prevHash).toString('hex'); +}; + + module.exports = utils; diff --git a/regtest/block.js b/regtest/block.js new file mode 100644 index 00000000..ec9dbede --- /dev/null +++ b/regtest/block.js @@ -0,0 +1,263 @@ +'use strict'; + +var chai = require('chai'); +var should = chai.should(); +var async = require('async'); +var BitcoinRPC = require('bitcoind-rpc'); +var path = require('path'); +var utils = require('./utils'); +var crypto = require('crypto'); + +var debug = true; +var bitcoreDataDir = '/tmp/bitcore'; +var bitcoinDataDir = '/tmp/bitcoin'; + +var rpcConfig = { + protocol: 'http', + user: 'bitcoin', + pass: 'local321', + host: '127.0.0.1', + port: '58332', + rejectUnauthorized: false +}; + +var bitcoin = { + args: { + datadir: bitcoinDataDir, + listen: 0, + regtest: 1, + server: 1, + rpcuser: rpcConfig.user, + rpcpassword: rpcConfig.pass, + rpcport: rpcConfig.port, + zmqpubrawtx: 'tcp://127.0.0.1:38332', + zmqpubhashblock: 'tcp://127.0.0.1:38332' + }, + datadir: bitcoinDataDir, + exec: 'bitcoind', //if this isn't on your PATH, then provide the absolute path, e.g. /usr/local/bin/bitcoind + process: null +}; + +var bitcore = { + configFile: { + file: bitcoreDataDir + '/bitcore-node.json', + conf: { + network: 'regtest', + port: 53001, + datadir: bitcoreDataDir, + services: [ + 'bitcoind', + 'db', + 'block', + 'web' + ], + servicesConfig: { + bitcoind: { + connect: [ + { + rpcconnect: rpcConfig.host, + rpcport: rpcConfig.port, + rpcuser: rpcConfig.user, + rpcpassword: rpcConfig.pass, + zmqpubrawtx: bitcoin.args.zmqpubrawtx + } + ] + } + } + } + }, + httpOpts: { + protocol: 'http:', + hostname: 'localhost', + port: 53001, + }, + opts: { cwd: bitcoreDataDir }, + datadir: bitcoreDataDir, + exec: path.resolve(__dirname, '../bin/bitcore-node'), + args: ['start'], + process: null +}; + +var opts = { + debug: debug, + bitcore: bitcore, + bitcoin: bitcoin, + bitcoinDataDir: bitcoinDataDir, + bitcoreDataDir: bitcoreDataDir, + rpc: new BitcoinRPC(rpcConfig), + walletPassphrase: 'test', + txCount: 0, + blockHeight: 0, + walletPrivKeys: [], + initialTxs: [], + fee: 100000, + feesReceived: 0, + satoshisSent: 0, + walletId: crypto.createHash('sha256').update('test').digest('hex'), + satoshisReceived: 0, + initialHeight: 150 +}; + +describe('Wallet Operations', function() { + + this.timeout(60000); + + describe('Register, Upload, GetTransactions', function() { + + var self = this; + + after(function(done) { + utils.cleanup(self.opts, done); + }); + + before(function(done) { + self.opts = Object.assign({}, opts); + async.series([ + utils.startBitcoind.bind(utils, self.opts), + utils.waitForBitcoinReady.bind(utils, self.opts), + utils.unlockWallet.bind(utils, self.opts), + utils.setupInitialTxs.bind(utils, self.opts), + utils.startBitcoreNode.bind(utils, self.opts), + utils.waitForBitcoreNode.bind(utils, self.opts) + ], done); + }); + + it('should register wallet', function(done) { + + utils.registerWallet.call(utils, self.opts, function(err, res) { + + if (err) { + return done(err); + } + + res.should.deep.equal(JSON.stringify({ + walletId: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' + })); + done(); + }); + }); + + it('should upload a wallet', function(done) { + + utils.uploadWallet.call(utils, self.opts, done); + + }); + + it('should get a list of transactions', function(done) { + + //the wallet should be fully uploaded and indexed by the time this happens + utils.sendTxs.call(utils, self.opts, function(err) { + + if(err) { + return done(err); + } + utils.waitForBitcoreNode.call(utils, self.opts, function(err) { + + if(err) { + return done(err); + } + utils.getListOfTxs.call(utils, self.opts, done); + }); + }); + + }); + + }); + + describe('Load addresses after syncing the blockchain', function() { + + var self = this; + + self.opts = Object.assign({}, opts); + + after(utils.cleanup.bind(utils, self.opts)); + + before(function(done) { + async.series([ + utils.startBitcoind.bind(utils, self.opts), + utils.waitForBitcoinReady.bind(utils, self.opts), + utils.unlockWallet.bind(utils, self.opts), + utils.setupInitialTxs.bind(utils, self.opts), + utils.sendTxs.bind(utils, self.opts), + utils.startBitcoreNode.bind(utils, self.opts), + utils.waitForBitcoreNode.bind(utils, self.opts), + utils.registerWallet.bind(utils, self.opts), + utils.uploadWallet.bind(utils, self.opts) + ], done); + }); + + it('should get list of transactions', function(done) { + + utils.getListOfTxs.call(utils, self.opts, done); + + }); + + it('should get the balance of a wallet', function(done) { + + var httpOpts = utils.getHttpOpts.call( + utils, + self.opts, + { path: '/wallet-api/wallets/' + self.opts.walletId + '/balance' }); + + utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) { + if(err) { + return done(err); + } + var results = JSON.parse(res); + results.satoshis.should.equal(self.opts.satoshisReceived); + done(); + }); + + }); + + it('should get the set of utxos for the wallet', function(done) { + + var httpOpts = utils.getHttpOpts.call( + utils, + self.opts, + { path: '/wallet-api/wallets/' + opts.walletId + '/utxos' }); + + utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) { + + if(err) { + return done(err); + } + + var results = JSON.parse(res); + var balance = 0; + + results.utxos.forEach(function(utxo) { + balance += utxo.satoshis; + }); + + results.height.should.equal(self.opts.blockHeight); + balance.should.equal(self.opts.satoshisReceived); + done(); + }); + }); + + it('should get the list of jobs', function(done) { + var httpOpts = utils.getHttpOpts.call(utils, self.opts, { path: '/wallet-api/jobs' }); + utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) { + if(err) { + return done(err); + } + var results = JSON.parse(res); + results.jobCount.should.equal(1); + done(); + }); + }); + + it('should remove all wallets', function(done) { + var httpOpts = utils.getHttpOpts.call(utils, self.opts, { path: '/wallet-api/wallets', method: 'DELETE' }); + utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) { + if(err) { + return done(err); + } + var results = JSON.parse(res); + results.numberRemoved.should.equal(152); + done(); + }); + }); + }); +}); diff --git a/regtest/timestamp.js b/regtest/timestamp.js new file mode 100644 index 00000000..925f9df5 --- /dev/null +++ b/regtest/timestamp.js @@ -0,0 +1,126 @@ +'use strict'; + +var chai = require('chai'); +var should = chai.should(); +var async = require('async'); +var BitcoinRPC = require('bitcoind-rpc'); +var path = require('path'); +var utils = require('./utils'); +var crypto = require('crypto'); + +var debug = true; +var bitcoreDataDir = '/tmp/bitcore'; +var bitcoinDataDir = '/tmp/bitcoin'; + +var rpcConfig = { + protocol: 'http', + user: 'bitcoin', + pass: 'local321', + host: '127.0.0.1', + port: '58332', + rejectUnauthorized: false +}; + +var bitcoin = { + args: { + datadir: bitcoinDataDir, + listen: 0, + regtest: 1, + server: 1, + rpcuser: rpcConfig.user, + rpcpassword: rpcConfig.pass, + rpcport: rpcConfig.port, + zmqpubrawtx: 'tcp://127.0.0.1:38332', + zmqpubhashblock: 'tcp://127.0.0.1:38332' + }, + datadir: bitcoinDataDir, + exec: 'bitcoind', //if this isn't on your PATH, then provide the absolute path, e.g. /usr/local/bin/bitcoind + process: null +}; + +var bitcore = { + configFile: { + file: bitcoreDataDir + '/bitcore-node.json', + conf: { + network: 'regtest', + port: 53001, + datadir: bitcoreDataDir, + services: [ + 'bitcoind', + 'web', + 'db', + 'timestamp' + ], + servicesConfig: { + bitcoind: { + connect: [ + { + rpcconnect: rpcConfig.host, + rpcport: rpcConfig.port, + rpcuser: rpcConfig.user, + rpcpassword: rpcConfig.pass, + zmqpubrawtx: bitcoin.args.zmqpubrawtx + } + ] + } + } + } + }, + httpOpts: { + protocol: 'http:', + hostname: 'localhost', + port: 53001, + }, + opts: { cwd: bitcoreDataDir }, + datadir: bitcoreDataDir, + exec: path.resolve(__dirname, '../bin/bitcore-node'), + args: ['start'], + process: null +}; + +var opts = { + debug: debug, + bitcore: bitcore, + bitcoin: bitcoin, + bitcoinDataDir: bitcoinDataDir, + bitcoreDataDir: bitcoreDataDir, + rpc: new BitcoinRPC(rpcConfig), + walletPassphrase: 'test', + txCount: 0, + blockHeight: 0, + walletPrivKeys: [], + initialTxs: [], + fee: 100000, + feesReceived: 0, + satoshisSent: 0, + walletId: crypto.createHash('sha256').update('test').digest('hex'), + satoshisReceived: 0, + initialHeight: 150 +}; + +describe('Timestamp Index', function() { + + var self = this; + self.timeout(60000); + + after(function(done) { + utils.cleanup(self.opts, done); + }); + + before(function(done) { + self.opts = Object.assign({}, opts); + async.series([ + utils.startBitcoind.bind(utils, self.opts), + utils.waitForBitcoinReady.bind(utils, self.opts), + utils.unlockWallet.bind(utils, self.opts), + utils.sendTxs.bind(utils, self.opts), + utils.startBitcoreNode.bind(utils, self.opts), + utils.waitForBitcoreNode.bind(utils, self.opts), + ], done); + }); + + it('should sync timestamps', function(done) { + done(); + }); + +}); diff --git a/regtest/utils.js b/regtest/utils.js index c7c3f219..dbd15b7d 100644 --- a/regtest/utils.js +++ b/regtest/utils.js @@ -99,7 +99,7 @@ utils.waitForBitcoreNode = function(opts, callback) { } }; - var httpOpts = self.getHttpOpts(opts, { path: '/wallet-api/info', errorFilter: errorFilter }); + var httpOpts = self.getHttpOpts(opts, { path: '/info', errorFilter: errorFilter }); self.waitForService(self.queryBitcoreNode.bind(self, httpOpts), callback); }; diff --git a/test/services/address/index.unit.js b/test/services/address/index.unit.js new file mode 100644 index 00000000..af42d344 --- /dev/null +++ b/test/services/address/index.unit.js @@ -0,0 +1,48 @@ +'use strict'; + +var should = require('chai').should(); +var bitcore = require('bitcore-lib'); +var Script = bitcore.Script; +var PrivateKey = bitcore.PrivateKey; + + +var AddressService = require('../../../lib/services/address'); +var utils = require('../../../lib/utils'); + +describe('Address Service', function() { + var address; + + var sig = new Buffer('3045022100e8b654c91770402bf35d207406c7d4967605f99478954c8030cf7060160b5c730220296690debdd354d5fa17a61379cfdce9fdea136a4b234664e41c1c7cd840098901', 'hex'); + + var pks = [ new PrivateKey(), new PrivateKey() ]; + + var pubKeys = [ pks[0].publicKey, pks[1].publicKey ]; + + var scripts = { + p2pkhIn: Script.buildPublicKeyHashIn(pubKeys[0], sig), + p2pkhOut: Script.buildPublicKeyHashOut(pubKeys[0]), + p2shIn: Script.buildP2SHMultisigIn(pubKeys, 2, [sig, sig]), + p2shOut: Script.buildScriptHashOut(Script.fromAddress(pks[0].toAddress())), + p2pkIn: Script.buildPublicKeyIn(sig), + p2pkOut: Script.buildPublicKeyOut(pubKeys[0]), + p2bmsIn: Script.buildMultisigIn(pubKeys, 2, [sig, sig]), + p2bmsOut: Script.buildMultisigOut(pubKeys, 2) + }; + + + before(function(done) { + address = new AddressService({ node: { name: 'address' } }); + done(); + }); + + it('should get an address from a script buffer', function() { + var start = process.hrtime(); + for(var key in scripts) { + var ret = address.getAddressString({ script: scripts[key] }); + console.log(ret); + }; + + console.log(utils.diffTime(start)); + + }); +}); diff --git a/test/services/block/encoding.js b/test/services/block/encoding.js new file mode 100644 index 00000000..36a47356 --- /dev/null +++ b/test/services/block/encoding.js @@ -0,0 +1,63 @@ +'use strict'; + +var should = require('chai').should(); + +var Encoding = require('../../../lib/services/block/encoding'); + +describe('Wallet-Api service encoding', function() { + + var servicePrefix = new Buffer('0000', 'hex'); + var encoding = new Encoding(servicePrefix); + var block = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add'; + var height = 1; + + it('should encode block hash key' , function() { + encoding.encodeBlockHashKey(block).should.deep.equal(Buffer.concat([ + servicePrefix, + new Buffer('00', 'hex'), + new Buffer(block, 'hex') + ])); + }); + + it('should decode block hash key' , function() { + encoding.decodeBlockHashKey(Buffer.concat([ + servicePrefix, + new Buffer('00', 'hex'), + new Buffer(block, 'hex') + ])).should.deep.equal(block); + }); + + it('should encode block hash value', function() { + encoding.encodeBlockHashValue(block).should.deep.equal( + new Buffer(block, 'hex')); + }); + + it('shound decode block hash value', function() { + encoding.decodeBlockHashValue(new Buffer(block, 'hex')).should.deep.equal(block); + }); + + it('should encode block height key', function() { + encoding.encodeBlockHeightKey(height).should.deep.equal(Buffer.concat([ + servicePrefix, + new Buffer('01', 'hex'), + new Buffer('00000001', 'hex') + ])); + }); + + it('should decode block height key', function() { + encoding.decodeBlockHeightKey(Buffer.concat([ + servicePrefix, + new Buffer('01', 'hex'), + new Buffer('00000001', 'hex') + ])).should.deep.equal(height); + }); + + it('should encode block height value', function() { + encoding.encodeBlockHeightValue(height).should.deep.equal(new Buffer('00000001', 'hex')); + }); + + it('should decode block height value', function() { + encoding.decodeBlockHeightValue(new Buffer('00000001', 'hex')).should.deep.equal(height); + }); +}); +