Merge pull request #51 from braydonf/freshstart
Initial daemon sync and setup
This commit is contained in:
commit
bee86257ca
@ -23,6 +23,7 @@ var sinon = require('sinon');
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var blockHashes = [];
|
||||
var utxo;
|
||||
var client;
|
||||
var coinbasePrivateKey;
|
||||
var privateKey = bitcore.PrivateKey();
|
||||
var destKey = bitcore.PrivateKey();
|
||||
@ -72,7 +73,7 @@ describe('Daemon Binding Functionality', function() {
|
||||
|
||||
bitcoind.on('ready', function() {
|
||||
|
||||
var client = new BitcoinRPC({
|
||||
client = new BitcoinRPC({
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: 18332,
|
||||
@ -161,56 +162,6 @@ describe('Daemon Binding Functionality', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('mempool functionality', function() {
|
||||
|
||||
var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
|
||||
var utxo1 = {
|
||||
address: fromAddress,
|
||||
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
|
||||
outputIndex: 0,
|
||||
script: bitcore.Script.buildPublicKeyHashOut(fromAddress).toString(),
|
||||
satoshis: 100000
|
||||
};
|
||||
var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
|
||||
var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up';
|
||||
var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf';
|
||||
var privateKey1 = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
|
||||
var private1 = '6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976';
|
||||
var private2 = 'c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890';
|
||||
var tx = new bitcore.Transaction();
|
||||
tx.from(utxo1);
|
||||
tx.to(toAddress, 50000);
|
||||
tx.change(changeAddress);
|
||||
tx.sign(privateKey1);
|
||||
|
||||
it('will add an unchecked transaction', function() {
|
||||
var added = bitcoind.addMempoolUncheckedTransaction(tx.serialize());
|
||||
added.should.equal(true);
|
||||
bitcoind.getTransaction(tx.hash, true, function(err, txBuffer) {
|
||||
if(err) {
|
||||
throw err;
|
||||
}
|
||||
var expected = tx.toBuffer().toString('hex');
|
||||
txBuffer.toString('hex').should.equal(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('get outputs by address', function() {
|
||||
var outputs = bitcoind.getMempoolOutputs(changeAddress);
|
||||
var expected = [
|
||||
{
|
||||
script: 'OP_DUP OP_HASH160 073b7eae2823efa349e3b9155b8a735526463a0f OP_EQUALVERIFY OP_CHECKSIG',
|
||||
satoshis: 40000,
|
||||
txid: tx.hash,
|
||||
outputIndex: 1
|
||||
}
|
||||
];
|
||||
outputs.should.deep.equal(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('get blocks by hash', function() {
|
||||
|
||||
[0,1,2,3,5,6,7,8,9].forEach(function(i) {
|
||||
@ -277,7 +228,71 @@ describe('Daemon Binding Functionality', function() {
|
||||
// test sending the transaction
|
||||
var hash = bitcoind.sendTransaction(tx.serialize());
|
||||
hash.should.equal(tx.hash);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('tip updates', function() {
|
||||
it('will get an event when the tip is new', function(done) {
|
||||
this.timeout(4000);
|
||||
bitcoind.on('tip', function(height) {
|
||||
height.should.equal(152);
|
||||
done();
|
||||
});
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mempool functionality', function() {
|
||||
|
||||
var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
|
||||
var utxo1 = {
|
||||
address: fromAddress,
|
||||
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
|
||||
outputIndex: 0,
|
||||
script: bitcore.Script.buildPublicKeyHashOut(fromAddress).toString(),
|
||||
satoshis: 100000
|
||||
};
|
||||
var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
|
||||
var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up';
|
||||
var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf';
|
||||
var privateKey1 = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
|
||||
var private1 = '6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976';
|
||||
var private2 = 'c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890';
|
||||
var tx = new bitcore.Transaction();
|
||||
tx.from(utxo1);
|
||||
tx.to(toAddress, 50000);
|
||||
tx.change(changeAddress);
|
||||
tx.sign(privateKey1);
|
||||
|
||||
it('will add an unchecked transaction', function() {
|
||||
var added = bitcoind.addMempoolUncheckedTransaction(tx.serialize());
|
||||
added.should.equal(true);
|
||||
bitcoind.getTransaction(tx.hash, true, function(err, txBuffer) {
|
||||
if(err) {
|
||||
throw err;
|
||||
}
|
||||
var expected = tx.toBuffer().toString('hex');
|
||||
txBuffer.toString('hex').should.equal(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('get outputs by address', function() {
|
||||
var outputs = bitcoind.getMempoolOutputs(changeAddress);
|
||||
var expected = [
|
||||
{
|
||||
script: 'OP_DUP OP_HASH160 073b7eae2823efa349e3b9155b8a735526463a0f OP_EQUALVERIFY OP_CHECKSIG',
|
||||
satoshis: 40000,
|
||||
txid: tx.hash,
|
||||
outputIndex: 1
|
||||
}
|
||||
];
|
||||
outputs.should.deep.equal(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
/**
|
||||
* bitcoind.js
|
||||
* Copyright (c) 2014, BitPay (MIT License)
|
||||
* A bitcoind node.js binding.
|
||||
*/
|
||||
|
||||
var net = require('net');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var bitcoindjs = require('bindings')('bitcoindjs.node');
|
||||
@ -11,13 +5,8 @@ var util = require('util');
|
||||
var fs = require('fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var tiny = require('tiny').json;
|
||||
|
||||
// Compatibility with old node versions:
|
||||
var setImmediate = global.setImmediate || process.nextTick.bind(process);
|
||||
|
||||
/**
|
||||
* Daemon
|
||||
*/
|
||||
var bitcore = require('bitcore');
|
||||
var $ = bitcore.util.preconditions;
|
||||
|
||||
var daemon = Daemon;
|
||||
|
||||
@ -29,74 +18,25 @@ function Daemon(options) {
|
||||
}
|
||||
|
||||
if (Object.keys(this.instances).length) {
|
||||
throw new
|
||||
Error('bitcoind.js cannot be instantiated more than once.');
|
||||
throw new Error('Daemon cannot be instantiated more than once.');
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
$.checkArgument(options.datadir, 'Please specify a datadir');
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
|
||||
if (!this.options.datadir) {
|
||||
this.options.datadir = '~/.bitcoind.js';
|
||||
}
|
||||
|
||||
this.options.datadir = this.options.datadir.replace(/^~/, process.env.HOME);
|
||||
|
||||
this.datadir = this.options.datadir;
|
||||
|
||||
this.config = this.datadir + '/bitcoin.conf';
|
||||
this.network = Daemon['livenet'];
|
||||
|
||||
this.network = Daemon.livenet;
|
||||
|
||||
if (this.options.network === 'testnet') {
|
||||
this.network = Daemon['testnet'];
|
||||
this.network = Daemon.testnet;
|
||||
} else if(this.options.network === 'regtest') {
|
||||
this.network = Daemon['regtest'];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.datadir)) {
|
||||
mkdirp.sync(this.datadir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.config)) {
|
||||
var password = ''
|
||||
+ Math.random().toString(36).slice(2)
|
||||
+ Math.random().toString(36).slice(2)
|
||||
+ Math.random().toString(36).slice(2);
|
||||
fs.writeFileSync(this.config, ''
|
||||
+ 'rpcuser=bitcoinrpc\n'
|
||||
+ 'rpcpassword=' + password + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
// Add hardcoded peers
|
||||
var data = fs.readFileSync(this.config, 'utf8');
|
||||
if (this.network.peers.length) {
|
||||
var peers = this.network.peers.reduce(function(out, peer) {
|
||||
if (!~data.indexOf('addnode=' + peer)) {
|
||||
return out + 'addnode=' + peer + '\n';
|
||||
}
|
||||
return out;
|
||||
}, '\n');
|
||||
fs.writeFileSync(data + peers);
|
||||
}
|
||||
|
||||
if (this.network.name === 'testnet') {
|
||||
if (!fs.existsSync(this.datadir + '/testnet3')) {
|
||||
fs.mkdirSync(this.datadir + '/testnet3');
|
||||
}
|
||||
fs.writeFileSync(
|
||||
this.datadir + '/testnet3/bitcoin.conf',
|
||||
fs.readFileSync(this.config));
|
||||
}
|
||||
|
||||
if (this.network.name === 'regtest') {
|
||||
if (!fs.existsSync(this.datadir + '/regtest')) {
|
||||
fs.mkdirSync(this.datadir + '/regtest');
|
||||
}
|
||||
fs.writeFileSync(
|
||||
this.datadir + '/regtest/bitcoin.conf',
|
||||
fs.readFileSync(this.config));
|
||||
this.network = Daemon.regtest;
|
||||
}
|
||||
|
||||
Object.keys(exports).forEach(function(key) {
|
||||
@ -260,6 +200,18 @@ Daemon.prototype.start = function(options, callback) {
|
||||
});
|
||||
|
||||
bitcoindjs.onBlocksReady(function(err, result) {
|
||||
|
||||
function onTipUpdateListener(result) {
|
||||
if (result) {
|
||||
// Emit and event that the tip was updated
|
||||
self.emit('tip', result);
|
||||
// Recursively wait until the next update
|
||||
bitcoindjs.onTipUpdate(onTipUpdateListener);
|
||||
}
|
||||
}
|
||||
|
||||
bitcoindjs.onTipUpdate(onTipUpdateListener);
|
||||
|
||||
self.emit('ready', result);
|
||||
});
|
||||
|
||||
@ -423,7 +375,6 @@ Daemon.prototype.getAddresses = function() {
|
||||
};
|
||||
|
||||
Daemon.prototype.getProgress = function(callback) {
|
||||
if (daemon.stopping) return [];
|
||||
return bitcoindjs.getProgress(callback);
|
||||
};
|
||||
|
||||
|
||||
283
lib/node.js
283
lib/node.js
@ -5,10 +5,10 @@ var Chain = require('./chain');
|
||||
var Block = require('./block');
|
||||
var DB = require('./db');
|
||||
var chainlib = require('chainlib');
|
||||
var P2P = chainlib.P2P;
|
||||
var fs = require('fs');
|
||||
var BaseNode = chainlib.Node;
|
||||
var util = require('util');
|
||||
var mkdirp = require('mkdirp');
|
||||
var log = chainlib.log;
|
||||
var bitcore = require('bitcore');
|
||||
var Networks = bitcore.Networks;
|
||||
@ -31,30 +31,23 @@ Node.prototype._loadConfiguration = function(config) {
|
||||
Node.super_.prototype._loadConfiguration.call(self, config);
|
||||
};
|
||||
|
||||
Node.SYNC_STRATEGIES = {
|
||||
P2P: 'p2p',
|
||||
BITCOIND: 'bitcoind'
|
||||
};
|
||||
|
||||
Node.prototype.setSyncStrategy = function(strategy) {
|
||||
this.syncStrategy = strategy;
|
||||
|
||||
if (this.syncStrategy === Node.SYNC_STRATEGIES.P2P) {
|
||||
this.p2p.startSync();
|
||||
} else if (this.syncStrategy === Node.SYNC_STRATEGIES.BITCOIND) {
|
||||
this.p2p.disableSync = true;
|
||||
this._syncBitcoind();
|
||||
} else {
|
||||
throw new Error('Strategy "' + strategy + '" is unknown.');
|
||||
}
|
||||
|
||||
};
|
||||
Node.DEFAULT_DAEMON_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n';
|
||||
|
||||
Node.prototype._loadBitcoinConf = function(config) {
|
||||
$.checkArgument(config.datadir, 'Please specify "datadir" in configuration options');
|
||||
var datadir = config.datadir.replace(/^~/, process.env.HOME);
|
||||
var configPath = datadir + '/bitcoin.conf';
|
||||
this.bitcoinConfiguration = {};
|
||||
var file = fs.readFileSync(datadir + '/bitcoin.conf');
|
||||
|
||||
if (!fs.existsSync(datadir)) {
|
||||
mkdirp.sync(datadir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.writeFileSync(configPath, Node.DEFAULT_DAEMON_CONFIG);
|
||||
}
|
||||
|
||||
var file = fs.readFileSync(configPath);
|
||||
var unparsed = file.toString().split('\n');
|
||||
for(var i = 0; i < unparsed.length; i++) {
|
||||
var line = unparsed[i];
|
||||
@ -69,6 +62,7 @@ Node.prototype._loadBitcoinConf = function(config) {
|
||||
this.bitcoinConfiguration[option[0]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Node.prototype._loadBitcoind = function(config) {
|
||||
@ -81,36 +75,184 @@ Node.prototype._loadBitcoind = function(config) {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will find the common ancestor between the current chain and a forked block,
|
||||
* by moving backwards from the forked block until it meets the current chain.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
Node.prototype._syncBitcoindAncestor = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// The current chain of hashes will likely already be available in a cache.
|
||||
self.chain.getHashes(self.chain.tip.hash, function(err, currentHashes) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
// Create a hash map for faster lookups
|
||||
var currentHashesMap = {};
|
||||
var length = currentHashes.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
currentHashesMap[currentHashes[i]] = true;
|
||||
}
|
||||
|
||||
var ancestorHash = block.prevHash;
|
||||
|
||||
// We only need to go back until we meet the main chain for the forked block
|
||||
// and thus don't need to find the entire chain of hashes.
|
||||
|
||||
async.whilst(function() {
|
||||
// Wait until the previous hash is in the current chain
|
||||
return ancestorHash && !currentHashesMap[ancestorHash];
|
||||
}, function(next) {
|
||||
self.bitcoind.getBlockIndex(ancestorHash, function(err, blockIndex) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ancestorHash = blockIndex.prevHash;
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
|
||||
// Hash map is no-longer needed, quickly let
|
||||
// scavenging garbage collection know to cleanup
|
||||
currentHashesMap = null;
|
||||
|
||||
if (err) {
|
||||
return done(err);
|
||||
} else if (!ancestorHash) {
|
||||
return done(new Error('Unknown common ancestor.'));
|
||||
}
|
||||
|
||||
done(null, ancestorHash);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will attempt to rewind the chain to the common ancestor
|
||||
* between the current chain and a forked block.
|
||||
* @param {Block} block - The new tip that forks the current chain.
|
||||
* @param {Function} done - A callback function that is called when complete.
|
||||
*/
|
||||
Node.prototype._syncBitcoindRewind = function(block, done) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self._syncBitcoindAncestor(block, function(err, ancestorHash) {
|
||||
|
||||
// Rewind the chain to the common ancestor
|
||||
async.whilst(
|
||||
function() {
|
||||
// Wait until the tip equals the ancestor hash
|
||||
return self.chain.tip.hash !== ancestorHash;
|
||||
},
|
||||
function(removeDone) {
|
||||
|
||||
var tip = self.chain.tip;
|
||||
|
||||
self.getBlock(tip.prevHash, function(err, previousTip) {
|
||||
if (err) {
|
||||
removeDone(err);
|
||||
}
|
||||
|
||||
// Undo the related indexes for this block
|
||||
self.db._onChainRemoveBlock(tip, function(err) {
|
||||
if (err) {
|
||||
return removeDone(err);
|
||||
}
|
||||
|
||||
// Set the new tip
|
||||
delete self.chain.tip.__transactions;
|
||||
previousTip.__height = self.chain.tip.__height - 1;
|
||||
self.chain.tip = previousTip;
|
||||
self.chain.saveMetadata();
|
||||
self.chain.emit('removeblock', tip);
|
||||
removeDone();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, done
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function will synchronize additional indexes for the chain based on
|
||||
* the current active chain in the bitcoin daemon. In the event that there is
|
||||
* a reorganization in the daemon, the chain will rewind to the last common
|
||||
* ancestor and then resume syncing.
|
||||
*/
|
||||
Node.prototype._syncBitcoind = function() {
|
||||
var self = this;
|
||||
|
||||
if (self.bitcoindSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.chain.tip) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.bitcoindSyncing = true;
|
||||
|
||||
log.info('Starting Bitcoind Sync');
|
||||
|
||||
var info = self.bitcoind.getInfo();
|
||||
var height;
|
||||
|
||||
async.whilst(function() {
|
||||
if (self.syncStrategy !== Node.SYNC_STRATEGIES.BITCOIND) {
|
||||
log.info('Stopping Bitcoind Sync');
|
||||
return false;
|
||||
}
|
||||
height = self.chain.tip.__height;
|
||||
return height < info.blocks;
|
||||
}, function(next) {
|
||||
return height < self.bitcoindHeight;
|
||||
}, function(done) {
|
||||
self.bitcoind.getBlock(height + 1, function(err, blockBuffer) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
return done(err);
|
||||
}
|
||||
var block = self.Block.fromBuffer(blockBuffer);
|
||||
if (block.prevHash === self.chain.tip.hash) {
|
||||
|
||||
// This block appends to the current chain tip and we can
|
||||
// immediately add it to the chain and create indexes.
|
||||
|
||||
// Populate height
|
||||
block.__height = self.chain.tip.__height + 1;
|
||||
|
||||
// Update chain hashes
|
||||
self.chain.cache.hashes[block.hash] = block.prevHash;
|
||||
|
||||
// Create indexes
|
||||
self.db._onChainAddBlock(block, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
delete self.chain.tip.__transactions;
|
||||
self.chain.tip = block;
|
||||
log.debug('Saving metadata');
|
||||
self.chain.saveMetadata();
|
||||
log.debug('Chain added block to main chain');
|
||||
self.chain.emit('addblock', block);
|
||||
done();
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// This block doesn't progress the current tip, so we'll attempt
|
||||
// to rewind the chain to the common ancestor of the block and
|
||||
// then we can resume syncing.
|
||||
self._syncBitcoindRewind(block, done);
|
||||
|
||||
}
|
||||
self.chain.addBlock(self.Block.fromBuffer(blockBuffer), next);
|
||||
});
|
||||
}, function(err) {
|
||||
log.info('Stopping Bitcoind Sync');
|
||||
self.bitcoindSyncing = false;
|
||||
if (err) {
|
||||
Error.captureStackTrace(err);
|
||||
return self.emit('error', err);
|
||||
}
|
||||
// we're done resume syncing via p2p to handle forks
|
||||
self.p2p.synced = true;
|
||||
self.setSyncStrategy(Node.SYNC_STRATEGIES.P2P);
|
||||
self.emit('synced');
|
||||
});
|
||||
|
||||
@ -167,38 +309,13 @@ Node.prototype._loadDB = function(config) {
|
||||
}
|
||||
config.db.network = this.network;
|
||||
|
||||
if (!fs.existsSync(config.db.path)) {
|
||||
mkdirp.sync(config.db.path);
|
||||
}
|
||||
|
||||
this.db = new DB(config.db);
|
||||
};
|
||||
|
||||
Node.prototype._loadP2P = function(config) {
|
||||
if (!config.p2p) {
|
||||
config.p2p = {};
|
||||
}
|
||||
config.p2p.noListen = true;
|
||||
config.p2p.network = this.network;
|
||||
|
||||
// We only want to directly connect via p2p to the trusted bitcoind daemon
|
||||
var port = 8333;
|
||||
if (this.bitcoinConfiguration && this.bitcoinConfiguration.port) {
|
||||
port = this.bitcoinConfiguration.port;
|
||||
} else if (this.network === Networks.testnet) {
|
||||
port = 18333;
|
||||
}
|
||||
config.p2p.addrs = [
|
||||
{
|
||||
ip: {
|
||||
v4: '127.0.0.1'
|
||||
},
|
||||
port: port
|
||||
}
|
||||
];
|
||||
config.p2p.dnsSeed = false;
|
||||
config.p2p.Transaction = this.db.Transaction;
|
||||
config.p2p.Block = this.Block;
|
||||
config.p2p.disableSync = true; // Disable p2p syncing and instead use bitcoind sync
|
||||
this.p2p = new P2P(config.p2p);
|
||||
};
|
||||
|
||||
Node.prototype._loadConsensus = function(config) {
|
||||
if (!config.consensus) {
|
||||
config.consensus = {};
|
||||
@ -227,9 +344,11 @@ Node.prototype._loadConsensus = function(config) {
|
||||
Node.prototype._initializeBitcoind = function() {
|
||||
var self = this;
|
||||
|
||||
// Bitcoind
|
||||
this.bitcoind.on('ready', function(status) {
|
||||
log.info('Bitcoin Daemon Ready');
|
||||
// Set the current chain height
|
||||
var info = self.bitcoind.getInfo();
|
||||
self.bitcoindHeight = info.blocks;
|
||||
self.db.initialize();
|
||||
});
|
||||
|
||||
@ -237,6 +356,13 @@ Node.prototype._initializeBitcoind = function() {
|
||||
log.info('Bitcoin Core Daemon Status:', status);
|
||||
});
|
||||
|
||||
// Notify that there is a new tip
|
||||
this.bitcoind.on('tip', function(height) {
|
||||
log.info('Bitcoin Core Daemon New Height:', height);
|
||||
self.bitcoindHeight = height;
|
||||
self._syncBitcoind();
|
||||
});
|
||||
|
||||
this.bitcoind.on('error', function(err) {
|
||||
Error.captureStackTrace(err);
|
||||
self.emit('error', err);
|
||||
@ -265,30 +391,11 @@ Node.prototype._initializeChain = function() {
|
||||
// Chain
|
||||
this.chain.on('ready', function() {
|
||||
log.info('Bitcoin Chain Ready');
|
||||
self.p2p.initialize();
|
||||
});
|
||||
|
||||
this.chain.on('error', function(err) {
|
||||
Error.captureStackTrace(err);
|
||||
self.emit('error', err);
|
||||
});
|
||||
};
|
||||
|
||||
Node.prototype._initializeP2P = function() {
|
||||
var self = this;
|
||||
|
||||
// Peer-to-Peer
|
||||
this.p2p.on('ready', function() {
|
||||
log.info('Bitcoin P2P Ready');
|
||||
self._syncBitcoind();
|
||||
self.emit('ready');
|
||||
});
|
||||
|
||||
this.p2p.on('synced', function() {
|
||||
log.info('Bitcoin P2P Synced');
|
||||
self.emit('synced');
|
||||
});
|
||||
|
||||
this.p2p.on('error', function(err) {
|
||||
this.chain.on('error', function(err) {
|
||||
Error.captureStackTrace(err);
|
||||
self.emit('error', err);
|
||||
});
|
||||
@ -305,21 +412,11 @@ Node.prototype._initialize = function() {
|
||||
|
||||
// Chain References
|
||||
this.chain.db = this.db;
|
||||
this.chain.p2p = this.p2p;
|
||||
|
||||
// P2P References
|
||||
this.p2p.db = this.db;
|
||||
this.p2p.chain = this.chain;
|
||||
|
||||
// Setup Chain of Events
|
||||
this._initializeBitcoind();
|
||||
this._initializeDatabase();
|
||||
this._initializeChain();
|
||||
this._initializeP2P();
|
||||
|
||||
this.on('ready', function() {
|
||||
self.setSyncStrategy(Node.SYNC_STRATEGIES.BITCOIND);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -23,11 +23,18 @@ using namespace v8;
|
||||
extern void WaitForShutdown(boost::thread_group* threadGroup);
|
||||
static termios orig_termios;
|
||||
extern CTxMemPool mempool;
|
||||
extern int64_t nTimeBestReceived;
|
||||
|
||||
/**
|
||||
* Node.js Internal Function Templates
|
||||
*/
|
||||
|
||||
static void
|
||||
async_tip_update(uv_work_t *req);
|
||||
|
||||
static void
|
||||
async_tip_update_after(uv_work_t *req);
|
||||
|
||||
static void
|
||||
async_start_node(uv_work_t *req);
|
||||
|
||||
@ -84,6 +91,11 @@ static bool g_txindex = false;
|
||||
* Used for async functions and necessary linked lists at points.
|
||||
*/
|
||||
|
||||
struct async_tip_update_data {
|
||||
size_t result;
|
||||
Eternal<Function> callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* async_node_data
|
||||
* Where the uv async request data resides.
|
||||
@ -188,6 +200,70 @@ set_cooked(void);
|
||||
* Functions
|
||||
*/
|
||||
|
||||
NAN_METHOD(OnTipUpdate) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
|
||||
Local<Function> callback;
|
||||
callback = Local<Function>::Cast(args[0]);
|
||||
|
||||
async_tip_update_data *data = new async_tip_update_data();
|
||||
|
||||
Eternal<Function> eternal(isolate, callback);
|
||||
|
||||
data->callback = eternal;
|
||||
uv_work_t *req = new uv_work_t();
|
||||
req->data = data;
|
||||
|
||||
int status = uv_queue_work(uv_default_loop(),
|
||||
req, async_tip_update,
|
||||
(uv_after_work_cb)async_tip_update_after);
|
||||
|
||||
assert(status == 0);
|
||||
|
||||
NanReturnValue(Undefined(isolate));
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
async_tip_update(uv_work_t *req) {
|
||||
async_tip_update_data *data = static_cast<async_tip_update_data*>(req->data);
|
||||
|
||||
size_t lastHeight = chainActive.Height();
|
||||
|
||||
while(lastHeight == (size_t)chainActive.Height() && !shutdown_complete) {
|
||||
usleep(1E6);
|
||||
}
|
||||
|
||||
data->result = chainActive.Height();
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
async_tip_update_after(uv_work_t *req) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
async_tip_update_data *data = static_cast<async_tip_update_data*>(req->data);
|
||||
|
||||
Local<Function> cb = data->callback.Get(isolate);
|
||||
const unsigned argc = 1;
|
||||
Local<Value> result = Undefined(isolate);
|
||||
if (!shutdown_complete) {
|
||||
result = NanNew<Number>(data->result);
|
||||
}
|
||||
Local<Value> argv[argc] = {
|
||||
Local<Value>::New(isolate, result)
|
||||
};
|
||||
TryCatch try_catch;
|
||||
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
|
||||
if (try_catch.HasCaught()) {
|
||||
node::FatalException(try_catch);
|
||||
}
|
||||
|
||||
delete data;
|
||||
delete req;
|
||||
}
|
||||
|
||||
NAN_METHOD(OnBlocksReady) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
@ -212,7 +288,6 @@ NAN_METHOD(OnBlocksReady) {
|
||||
assert(status == 0);
|
||||
|
||||
NanReturnValue(Undefined(isolate));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -291,7 +366,6 @@ async_blocks_ready_after(uv_work_t *req) {
|
||||
* bitcoind.start(callback)
|
||||
* Start the bitcoind node with AppInit2() on a separate thread.
|
||||
*/
|
||||
|
||||
NAN_METHOD(StartBitcoind) {
|
||||
Isolate* isolate = Isolate::GetCurrent();
|
||||
HandleScope scope(isolate);
|
||||
@ -1232,6 +1306,7 @@ init(Handle<Object> target) {
|
||||
|
||||
NODE_SET_METHOD(target, "start", StartBitcoind);
|
||||
NODE_SET_METHOD(target, "onBlocksReady", OnBlocksReady);
|
||||
NODE_SET_METHOD(target, "onTipUpdate", OnTipUpdate);
|
||||
NODE_SET_METHOD(target, "stop", StopBitcoind);
|
||||
NODE_SET_METHOD(target, "stopping", IsStopping);
|
||||
NODE_SET_METHOD(target, "stopped", IsStopped);
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
NAN_METHOD(StartBitcoind);
|
||||
NAN_METHOD(OnBlocksReady);
|
||||
NAN_METHOD(OnTipUpdate);
|
||||
NAN_METHOD(IsStopping);
|
||||
NAN_METHOD(IsStopped);
|
||||
NAN_METHOD(StopBitcoind);
|
||||
@ -28,4 +29,3 @@ NAN_METHOD(GetMempoolOutputs);
|
||||
NAN_METHOD(AddMempoolUncheckedTransaction);
|
||||
NAN_METHOD(VerifyScript);
|
||||
NAN_METHOD(SendTransaction);
|
||||
|
||||
|
||||
1
test/data/hashes.json
Normal file
1
test/data/hashes.json
Normal file
File diff suppressed because one or more lines are too long
@ -12,7 +12,8 @@ var proxyquire = require('proxyquire');
|
||||
var chainlib = require('chainlib');
|
||||
var OriginalNode = chainlib.Node;
|
||||
var fs = require('fs');
|
||||
var bitcoinConfBuffer = fs.readFileSync('./test/data/bitcoin.conf');
|
||||
var bitcoinConfBuffer = fs.readFileSync(__dirname + '/data/bitcoin.conf');
|
||||
var chainHashes = require('./data/hashes.json');
|
||||
|
||||
var BaseNode = function() {};
|
||||
util.inherits(BaseNode, EventEmitter);
|
||||
@ -41,30 +42,6 @@ describe('Bitcoind Node', function() {
|
||||
BaseNode.prototype._loadConfiguration.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
describe('#setSyncStrategy', function() {
|
||||
it('will call p2p.startSync', function() {
|
||||
var node = new Node({});
|
||||
node.p2p = {
|
||||
startSync: sinon.spy()
|
||||
};
|
||||
node.setSyncStrategy(Node.SYNC_STRATEGIES.P2P);
|
||||
node.p2p.startSync.callCount.should.equal(1);
|
||||
});
|
||||
it('will call this._syncBitcoind and disable p2p sync', function() {
|
||||
var node = new Node({});
|
||||
node.p2p = {};
|
||||
node._syncBitcoind = sinon.spy();
|
||||
node.setSyncStrategy(Node.SYNC_STRATEGIES.BITCOIND);
|
||||
node._syncBitcoind.callCount.should.equal(1);
|
||||
node.p2p.disableSync.should.equal(true);
|
||||
});
|
||||
it('will error with an unknown strategy', function() {
|
||||
var node = new Node({});
|
||||
(function(){
|
||||
node.setSyncStrategy('unknown');
|
||||
}).should.throw('Strategy "unknown" is unknown');
|
||||
});
|
||||
});
|
||||
describe('#_loadBitcoinConf', function() {
|
||||
it('will parse a bitcoin.conf file', function() {
|
||||
var node = new Node({});
|
||||
@ -84,53 +61,142 @@ describe('Bitcoind Node', function() {
|
||||
describe('#_loadBitcoind', function() {
|
||||
it('should initialize', function() {
|
||||
var node = new Node({});
|
||||
node._loadBitcoind({});
|
||||
node._loadBitcoind({datadir: './test'});
|
||||
should.exist(node.bitcoind);
|
||||
});
|
||||
it('should initialize with testnet', function() {
|
||||
var node = new Node({});
|
||||
node._loadBitcoind({testnet: true});
|
||||
node._loadBitcoind({datadir: './test', testnet: true});
|
||||
should.exist(node.bitcoind);
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoindAncestor', function() {
|
||||
it('will find an ancestor 6 deep', function() {
|
||||
var node = new Node({});
|
||||
node.chain = {
|
||||
getHashes: function(tipHash, callback) {
|
||||
callback(null, chainHashes);
|
||||
},
|
||||
tip: {
|
||||
hash: chainHashes[chainHashes.length]
|
||||
}
|
||||
};
|
||||
var expectedAncestor = chainHashes[chainHashes.length - 6];
|
||||
var forkedBlocks = {
|
||||
'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': {
|
||||
prevHash: '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a'
|
||||
},
|
||||
'76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': {
|
||||
prevHash: 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c'
|
||||
},
|
||||
'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': {
|
||||
prevHash: '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31'
|
||||
},
|
||||
'2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': {
|
||||
prevHash: 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453'
|
||||
},
|
||||
'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': {
|
||||
prevHash: '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618'
|
||||
},
|
||||
'3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': {
|
||||
prevHash: expectedAncestor
|
||||
},
|
||||
};
|
||||
node.bitcoind = {
|
||||
getBlockIndex: function(hash) {
|
||||
return forkedBlocks[hash];
|
||||
}
|
||||
};
|
||||
var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82'];
|
||||
node._syncBitcoindAncestor(block, function(err, ancestorHash) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
ancestorHash.should.equal(expectedAncestor);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoindRewind', function() {
|
||||
it('will undo blocks 6 deep', function() {
|
||||
var node = new Node({});
|
||||
var ancestorHash = chainHashes[chainHashes.length - 6];
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 10,
|
||||
hash: chainHashes[chainHashes.length],
|
||||
prevHash: chainHashes[chainHashes.length - 1]
|
||||
},
|
||||
saveMetadata: sinon.stub(),
|
||||
emit: sinon.stub()
|
||||
};
|
||||
node.getBlock = function(hash, callback) {
|
||||
setImmediate(function() {
|
||||
for(var i = chainHashes.length; i > 0; i--) {
|
||||
if (chainHashes[i] === hash) {
|
||||
callback(null, {
|
||||
hash: chainHashes[i],
|
||||
prevHash: chainHashes[i - 1]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
node.db = {
|
||||
_onChainRemoveBlock: function(block, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
};
|
||||
node._syncBitcoindAncestor = function(block, callback) {
|
||||
setImmediate(function() {
|
||||
callback(null, ancestorHash);
|
||||
});
|
||||
};
|
||||
var forkedBlock = {};
|
||||
node._syncBitcoindRewind(forkedBlock, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
node.chain.tip.__height.should.equal(4);
|
||||
node.chain.tip.hash.should.equal(ancestorHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#_syncBitcoind', function() {
|
||||
it('will get and add block up to the tip height', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = {
|
||||
synced: false
|
||||
};
|
||||
node.Block = Block;
|
||||
node.syncStrategy = Node.SYNC_STRATEGIES.BITCOIND;
|
||||
node.setSyncStrategy = sinon.stub();
|
||||
node.bitcoindHeight = 1;
|
||||
var blockBuffer = new Buffer(blockData);
|
||||
var block = Block.fromBuffer(blockBuffer);
|
||||
node.bitcoind = {
|
||||
getInfo: sinon.stub().returns({blocks: 2}),
|
||||
getBlock: sinon.stub().callsArgWith(1, null, new Buffer(blockData))
|
||||
getBlock: sinon.stub().callsArgWith(1, null, blockBuffer)
|
||||
};
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 0
|
||||
__height: 0,
|
||||
hash: block.prevHash
|
||||
},
|
||||
addBlock: function(block, callback) {
|
||||
saveMetadata: sinon.stub(),
|
||||
emit: sinon.stub(),
|
||||
cache: {
|
||||
hashes: {}
|
||||
}
|
||||
};
|
||||
node.db = {
|
||||
_onChainAddBlock: function(block, callback) {
|
||||
node.chain.tip.__height += 1;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
node.on('synced', function() {
|
||||
node.p2p.synced.should.equal(true);
|
||||
node.setSyncStrategy.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
node._syncBitcoind();
|
||||
});
|
||||
it('will exit and emit error with error from bitcoind.getBlock', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = {
|
||||
synced: false
|
||||
};
|
||||
node.syncStrategy = Node.SYNC_STRATEGIES.BITCOIND;
|
||||
node.setSyncStrategy = sinon.stub();
|
||||
node.bitcoindHeight = 1;
|
||||
node.bitcoind = {
|
||||
getInfo: sinon.stub().returns({blocks: 2}),
|
||||
getBlock: sinon.stub().callsArgWith(1, new Error('test error'))
|
||||
};
|
||||
node.chain = {
|
||||
@ -144,28 +210,6 @@ describe('Bitcoind Node', function() {
|
||||
});
|
||||
node._syncBitcoind();
|
||||
});
|
||||
it('will exit if sync strategy is changed to bitcoind', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = {
|
||||
synced: false
|
||||
};
|
||||
node.syncStrategy = Node.SYNC_STRATEGIES.P2P;
|
||||
node.setSyncStrategy = sinon.stub();
|
||||
node.bitcoind = {
|
||||
getInfo: sinon.stub().returns({blocks: 2})
|
||||
};
|
||||
node.chain = {
|
||||
tip: {
|
||||
__height: 0
|
||||
}
|
||||
};
|
||||
node.on('synced', function() {
|
||||
node.p2p.synced.should.equal(true);
|
||||
node.setSyncStrategy.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
node._syncBitcoind();
|
||||
});
|
||||
});
|
||||
describe('#_loadNetwork', function() {
|
||||
it('should use the testnet network if testnet is specified', function() {
|
||||
@ -280,22 +324,6 @@ describe('Bitcoind Node', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#_loadP2P', function() {
|
||||
it('should load p2p', function() {
|
||||
var config = {};
|
||||
|
||||
var node = new Node(config);
|
||||
node.db = {
|
||||
Transaction: bitcore.Transaction
|
||||
};
|
||||
node.network = Networks.get('testnet');
|
||||
node._loadP2P(config);
|
||||
should.exist(node.p2p);
|
||||
node.p2p.noListen.should.equal(true);
|
||||
node.p2p.pool.network.should.deep.equal(node.network);
|
||||
node.db.Transaction.should.equal(bitcore.Transaction);
|
||||
});
|
||||
});
|
||||
describe('#_loadConsensus', function() {
|
||||
var node = new Node({});
|
||||
|
||||
@ -327,6 +355,7 @@ describe('Bitcoind Node', function() {
|
||||
it('will call db.initialize() on ready event', function(done) {
|
||||
var node = new Node({});
|
||||
node.bitcoind = new EventEmitter();
|
||||
node.bitcoind.getInfo = sinon.stub().returns({blocks: 10});
|
||||
node.db = {
|
||||
initialize: sinon.spy()
|
||||
};
|
||||
@ -336,6 +365,7 @@ describe('Bitcoind Node', function() {
|
||||
chainlib.log.info.callCount.should.equal(1);
|
||||
chainlib.log.info.restore();
|
||||
node.db.initialize.callCount.should.equal(1);
|
||||
node.bitcoindHeight.should.equal(10);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -388,24 +418,6 @@ describe('Bitcoind Node', function() {
|
||||
});
|
||||
|
||||
describe('#_initializeChain', function() {
|
||||
it('will call p2p.initialize() on ready event', function(done) {
|
||||
var node = new Node({});
|
||||
node.chain = new EventEmitter();
|
||||
node.p2p = {
|
||||
initialize: sinon.spy()
|
||||
};
|
||||
sinon.stub(chainlib.log, 'info');
|
||||
node.chain.on('ready', function() {
|
||||
setImmediate(function() {
|
||||
chainlib.log.info.callCount.should.equal(1);
|
||||
chainlib.log.info.restore();
|
||||
node.p2p.initialize.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
node._initializeChain();
|
||||
node.chain.emit('ready');
|
||||
});
|
||||
it('will call emit an error from chain', function(done) {
|
||||
var node = new Node({});
|
||||
node.chain = new EventEmitter();
|
||||
@ -419,41 +431,6 @@ describe('Bitcoind Node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_initializeP2P', function() {
|
||||
it('will emit node "ready" when p2p is ready', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = new EventEmitter();
|
||||
sinon.stub(chainlib.log, 'info');
|
||||
node.on('ready', function() {
|
||||
chainlib.log.info.callCount.should.equal(1);
|
||||
chainlib.log.info.restore();
|
||||
done();
|
||||
});
|
||||
node._initializeP2P();
|
||||
node.p2p.emit('ready');
|
||||
});
|
||||
it('will call emit an error from p2p', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = new EventEmitter();
|
||||
node.on('error', function(err) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('test error');
|
||||
done();
|
||||
});
|
||||
node._initializeP2P();
|
||||
node.p2p.emit('error', new Error('test error'));
|
||||
});
|
||||
it('will relay synced event from p2p to node', function(done) {
|
||||
var node = new Node({});
|
||||
node.p2p = new EventEmitter();
|
||||
node.on('synced', function() {
|
||||
done();
|
||||
});
|
||||
node._initializeP2P();
|
||||
node.p2p.emit('synced');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_initialize', function() {
|
||||
|
||||
it('should initialize', function(done) {
|
||||
@ -461,13 +438,11 @@ describe('Bitcoind Node', function() {
|
||||
node.chain = {};
|
||||
node.Block = 'Block';
|
||||
node.bitcoind = 'bitcoind';
|
||||
node.p2p = {};
|
||||
node.db = {};
|
||||
|
||||
node._initializeBitcoind = sinon.spy();
|
||||
node._initializeDatabase = sinon.spy();
|
||||
node._initializeChain = sinon.spy();
|
||||
node._initializeP2P = sinon.spy();
|
||||
node._initialize();
|
||||
|
||||
// references
|
||||
@ -475,21 +450,16 @@ describe('Bitcoind Node', function() {
|
||||
node.db.Block.should.equal(node.Block);
|
||||
node.db.bitcoind.should.equal(node.bitcoind);
|
||||
node.chain.db.should.equal(node.db);
|
||||
node.chain.p2p.should.equal(node.p2p);
|
||||
node.chain.db.should.equal(node.db);
|
||||
node.p2p.db.should.equal(node.db);
|
||||
node.p2p.chain.should.equal(node.chain);
|
||||
|
||||
// events
|
||||
node._initializeBitcoind.callCount.should.equal(1);
|
||||
node._initializeDatabase.callCount.should.equal(1);
|
||||
node._initializeChain.callCount.should.equal(1);
|
||||
node._initializeP2P.callCount.should.equal(1);
|
||||
|
||||
// start syncing
|
||||
node.setSyncStrategy = sinon.spy();
|
||||
node.on('ready', function() {
|
||||
node.setSyncStrategy.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
node.emit('ready');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user