Wip on block service.
This commit is contained in:
parent
fe6896ba98
commit
a8760b3451
83
child-pay-for-parent.js
Normal file
83
child-pay-for-parent.js
Normal 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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
45
lib/services/block/encoding.js
Normal file
45
lib/services/block/encoding.js
Normal 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
330
lib/services/block/index.js
Normal 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;
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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
263
regtest/block.js
Normal 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
126
regtest/timestamp.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
||||
@ -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);
|
||||
};
|
||||
|
||||
48
test/services/address/index.unit.js
Normal file
48
test/services/address/index.unit.js
Normal 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));
|
||||
|
||||
});
|
||||
});
|
||||
63
test/services/block/encoding.js
Normal file
63
test/services/block/encoding.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user