Wip on block service.

This commit is contained in:
Chris Kleeschulte 2017-05-18 09:48:28 -04:00
parent fe6896ba98
commit a8760b3451
19 changed files with 1101 additions and 1344 deletions

83
child-pay-for-parent.js Normal file
View File

@ -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

View File

@ -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;

View File

@ -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: <Transaction>
* }
* ]
* }
* @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;

View File

@ -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;

330
lib/services/block/index.js Normal file
View File

@ -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;

View File

@ -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();
});
}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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;
});

View File

@ -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);
}

View File

@ -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()
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

263
regtest/block.js Normal file
View File

@ -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();
});
});
});
});

126
regtest/timestamp.js Normal file
View File

@ -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();
});
});

View File

@ -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);
};

View File

@ -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));
});
});

View File

@ -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);
});
});