incorporate chainlib bitcoin into bitcoind.js

This commit is contained in:
Patrick Nagurny 2015-07-15 18:13:41 -04:00
parent d0327b2f85
commit b8b4ac02bf
9 changed files with 1329 additions and 2 deletions

54
example/bitcoind.js Normal file
View File

@ -0,0 +1,54 @@
'use strict';
var BitcoinNode = require('../lib/node');
var chainlib = require('chainlib');
var log = chainlib.log;
log.debug = function() {};
var privkey = 'tprv8ZgxMBicQKsPdj1QowoT9z1tY5Et38qaMjCHZVoPdPFb6narfmYkqTygEVHfUmY78k3HcaEpkyNCAQDANaXtwNe1HLFvcA7nqYj1B7wTSTo';
var configuration = {
db: {
xprivkey: privkey,
path: './bitcoind.db'
},
p2p: {
addrs: [
{
ip: {
v4: '127.0.0.1'
},
port: 8333
}
],
dnsSeed: false
},
testnet: false
};
var node = new BitcoinNode(configuration);
var startHeight;
var count = 100;
var times = Array(count);
node.on('ready', function() {
times[node.chain.tip.__height % count] = Date.now();
startHeight = node.chain.tip.__height;
});
node.on('error', function(err) {
log.error(err);
});
node.chain.on('addblock', function(block) {
console.log('New Best Tip:', block.hash);
var startTime = times[node.chain.tip.__height % count];
if(startTime) {
var timeElapsed = (Date.now() - startTime) / 1000;
console.log(Math.round(count / timeElapsed) + ' blocks per second');
}
times[node.chain.tip.__height % count] = Date.now();
});

149
lib/block.js Normal file
View File

@ -0,0 +1,149 @@
'use strict';
var util = require('util');
var chainlib = require('chainlib');
var BaseBlock = chainlib.Block;
var bitcore = require('bitcore');
var BufferReader = bitcore.encoding.BufferReader;
var BN = bitcore.crypto.BN;
function Block(obj) {
if (!obj) {
obj = {};
}
BaseBlock.call(this, obj);
this.bits = obj.bits;
this.nonce = obj.nonce || 0;
}
util.inherits(Block, BaseBlock);
Block.prototype.validate = function(chain, callback) {
var self = this;
// Get previous block
chain.db.getBlock(self.prevHash, function(err, prevBlock) {
if (err) {
return callback(err);
}
// Validate POW
// First make sure the current block hash is less than the target derived from this block's bits
if (!self.validProofOfWork(chain)) {
return callback(new Error('Invalid proof of work (hash is greater than target)'));
}
// Second make sure that this block's bits is correct based off of the last block
chain.getNextWorkRequired(prevBlock, function(err, bits) {
if (err) {
return callback(err);
}
if (bits !== self.bits) {
return callback(new Error(
'Invalid proof of work, expected block bits "' + self.bits + '" to equal "' + bits + '"'
));
}
// Validate block data
chain.db.validateBlockData(self, callback);
});
});
};
/**
* @returns {Boolean} - If the proof-of-work hash satisfies the target difficulty
*/
Block.prototype.validProofOfWork = function validProofOfWork(chain) {
var pow = new BN(this.hash, 'hex');
var target = chain.getTargetFromBits(this.bits);
if (pow.cmp(target) > 0) {
return false;
}
return true;
};
Block.fromBuffer = function(buffer) {
var br = new BufferReader(buffer);
return Block.fromBufferReader(br);
};
Block.fromBufferReader = function(br) {
var obj = {};
obj.version = br.readUInt32LE();
obj.prevHash = BufferReader(br.read(32)).readReverse().toString('hex');
var nullHash = new Buffer(Array(32)).toString('hex');
if (obj.prevHash === nullHash) {
obj.prevHash = null;
}
obj.merkleRoot = BufferReader(br.read(32)).readReverse().toString('hex');
var timestamp = br.readUInt32LE();
obj.timestamp = new Date(timestamp * 1000);
obj.bits = br.readUInt32LE();
obj.nonce = br.readUInt32LE();
obj.data = br.readAll();
return new Block(obj);
};
Block.prototype.toObject = function() {
return {
version: this.version,
prevHash: this.prevHash,
merkleRoot: this.merkleRoot,
timestamp: this.timestamp.toISOString(),
bits: this.bits,
nonce: this.nonce,
data: this.data.toString('hex')
};
};
Block.prototype.headerToBufferWriter = function(bw) {
/* jshint maxstatements: 20 */
// version
bw.writeUInt32LE(this.version);
// prevhash
if (!this.prevHash) {
bw.write(new Buffer(Array(32)));
} else {
var prevHashBuffer = new Buffer(this.prevHash, 'hex');
prevHashBuffer = BufferReader(prevHashBuffer).readReverse();
if (prevHashBuffer.length !== 32) {
throw new Error('"prevHash" is expected to be 32 bytes');
}
bw.write(prevHashBuffer);
}
// merkleroot
if (!this.merkleRoot) {
bw.write(new Buffer(Array(32)));
} else {
var merkleRoot = new Buffer(this.merkleRoot, 'hex');
merkleRoot = BufferReader(merkleRoot).readReverse();
if (merkleRoot.length !== 32) {
throw new Error('"merkleRoot" is expected to be 32 bytes');
}
bw.write(merkleRoot);
}
// timestamp
bw.writeUInt32LE(Math.floor(this.timestamp.getTime() / 1000));
// bits
bw.writeUInt32LE(this.bits);
// nonce
bw.writeUInt32LE(this.nonce);
return bw;
};
module.exports = Block;

238
lib/chain.js Normal file
View File

@ -0,0 +1,238 @@
'use strict';
var util = require('util');
var bitcore = require('bitcore');
var chainlib = require('chainlib');
var BaseChain = chainlib.Chain;
var BN = bitcore.crypto.BN;
var Block = require('./block');
Chain.DEFAULTS = {
MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'),
TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks
TARGET_SPACING: 10 * 60 * 1000, // ten minutes
MAX_BITS: 0x1d00ffff,
MIN_BITS: 0x03000000
};
/**
* Will instantiate a new Chain instance
* @param {Object} options - The options for the chain
* @param {Number} options.minBits - The minimum number of bits
* @param {Number} options.maxBits - The maximum number of bits
* @param {BN|Number} options.targetTimespan - The number of milliseconds for difficulty retargeting
* @param {BN|Number} options.targetSpacing - The number of milliseconds between blocks
* @returns {Chain}
* @extends BaseChain
* @constructor
*/
function Chain(options) {
/* jshint maxstatements: 20 */
/* jshint maxcomplexity: 12 */
if (!(this instanceof Chain)) {
return new Chain(options);
}
if (!options) {
options = {};
}
BaseChain.call(this, options);
this.minBits = options.minBits || Chain.DEFAULTS.MIN_BITS;
this.maxBits = options.maxBits || Chain.DEFAULTS.MAX_BITS;
this.maxHashes = options.maxHashes || Chain.DEFAULTS.MAX_HASHES;
this.targetTimespan = options.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN;
this.targetSpacing = options.targetSpacing || Chain.DEFAULTS.TARGET_SPACING;
return this;
}
util.inherits(Chain, BaseChain);
Chain.prototype._writeBlock = function(block, callback) {
// Update hashes
this.cache.hashes[block.hash] = block.prevHash;
// call db.putBlock to update prevHash index, but it won't write the block to disk
this.db.putBlock(block, callback);
};
Chain.prototype._validateBlock = function(block, callback) {
// All validation is done by bitcoind
setImmediate(callback);
};
Chain.prototype.startBuilder = function() {
// Unused in bitcoind.js
};
Chain.prototype.buildGenesisBlock = function buildGenesisBlock(options) {
if (!options) {
options = {};
}
var genesis = new Block({
prevHash: null,
height: 0,
timestamp: options.timestamp || new Date(),
nonce: options.nonce || 0,
bits: options.bits || this.maxBits
});
var data = this.db.buildGenesisData();
genesis.merkleRoot = data.merkleRoot;
genesis.data = data.buffer;
return genesis;
};
/**
* Calculates the number of blocks for a retargeting interval
* @returns {BN}
*/
Chain.prototype.getDifficultyInterval = function getDifficultyInterval() {
return this.targetTimespan / this.targetSpacing;
};
/**
* Will recalculate the bits based on the hash rate of the previous interval
* @see https://en.bitcoin.it/wiki/Difficulty#How_is_difficulty_stored_in_blocks.3F
* @param {Number} bits - The bits of the previous interval block
* @param {Number} timespan - The number of milliseconds that elapsed in the last interval
* @returns {Number} The compacted target in bits
*/
Chain.prototype.getRetargetedBits = function getRetargetedBits(bits, timespan) {
// Based off Bitcoin's code:
// https://github.com/bitcoin/bitcoin/blob/master/src/pow.cpp#L53
if(timespan < this.targetTimespan / 4) {
timespan = Math.floor(this.targetTimespan / 4);
} else if(timespan > this.targetTimespan * 4) {
timespan = this.targetTimespan * 4;
}
var oldTarget = this.getTargetFromBits(bits);
var newTarget = oldTarget.mul(new BN(timespan, 10)).div(new BN(this.targetTimespan, 10));
var newBits = this.getBitsFromTarget(newTarget);
if(newBits > this.maxBits) {
newBits = this.maxBits;
}
return newBits;
};
/**
* Calculates the number of blocks for a retargeting interval
* @param {Block} - block - An instance of a block
* @param {Function} - callback - A callback function that accepts arguments: Error and Number
*/
Chain.prototype.getNextWorkRequired = function getNextWorkRequired(block, callback) {
var self = this;
var interval = this.getDifficultyInterval();
self.getHeightForBlock(block.hash, function(err, height) {
if (err) {
return callback(err);
}
if (height === 0) {
return callback(null, self.maxBits);
}
// not on interval, return the same amount of difficulty
if ((height + 1) % interval !== 0) {
return callback(null, block.bits);
}
// otherwise compute the new difficulty
self.getBlockAtHeight(block, height + 1 - interval, function(err, lastIntervalBlock){
if (err) {
callback(err);
}
var timespan = (Math.floor(block.timestamp.getTime() / 1000) * 1000) - (Math.floor(lastIntervalBlock.timestamp.getTime() / 1000) * 1000);
var bits = self.getRetargetedBits(lastIntervalBlock.bits, timespan);
return callback(null, bits);
});
});
};
/**
* Calculates the actual target from the compact form
* @param {Number} - bits
* @returns {BN}
*/
Chain.prototype.getTargetFromBits = function getTargetFromBits(bits) {
if(bits <= this.minBits) {
throw new Error('bits is too small (' + bits + ')');
}
if(bits > this.maxBits) {
throw new Error('bits is too big (' + bits + ')');
}
var a = bits & 0xffffff;
var b = bits >>> 24;
var exp = (8 * (b - 3));
// Exponents via bit shift (works for powers of 2)
var z = (new BN(2, 10)).shln(exp - 1);
var target = (new BN(a, 10)).mul(z);
return target;
};
/**
* Calculates the compact target "bits" from the target
* @param {BN|Number} - target
* @returns {Number}
*/
Chain.prototype.getBitsFromTarget = function getBitsFromTarget(target) {
target = new BN(target, 'hex');
var tmp = target;
var b = 0;
while(tmp.cmp(new BN(0, 10)) > 0) {
b++;
tmp = tmp.shrn(8);
}
var a = target.shrn((b - 3) * 8);
var bits = Number('0x' + b.toString(16) + a.toString(16, 6));
return bits;
};
Chain.prototype.getDifficultyFromBits = function getDifficultyFromBits(bits) {
var currentTarget = this.getTargetFromBits(bits);
var genesisTarget = this.getTargetFromBits(this.genesis.bits);
return genesisTarget.div(currentTarget);
};
Chain.prototype.getBlockWeight = function getBlockWeight(blockHash, callback) {
var self = this;
self.db.getBlock(blockHash, function(err, block) {
if(err) {
return callback(err);
} else if(!block) {
return callback(new Error('Block not found (' + blockHash + ')'));
}
var target = self.getTargetFromBits(block.bits);
var a = self.maxHashes.sub(target).sub(new BN(1, 10));
var b = target.add(new BN(1, 10));
var c = a.div(b);
var d = c.add(new BN(1, 10));
return callback(null, d);
});
};
module.exports = Chain;

480
lib/db.js Normal file
View File

@ -0,0 +1,480 @@
'use strict';
var util = require('util');
var chainlib = require('chainlib');
var BaseDB = chainlib.DB;
var Transaction = require('./transaction');
var async = require('async');
var bitcore = require('bitcore');
var BufferWriter = bitcore.encoding.BufferWriter;
var errors = require('./errors');
var levelup = chainlib.deps.levelup;
var log = chainlib.log;
var PublicKey = bitcore.PublicKey;
var Address = bitcore.Address;
function DB(options) {
if(!options) {
options = {};
}
BaseDB.call(this, options);
this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8;
this.Transaction = Transaction;
this.network = bitcore.Networks.get(options.network) || bitcore.Networks.testnet;
}
util.inherits(DB, BaseDB);
DB.PREFIXES = {
TX: 'tx',
SPENTS: 'sp',
OUTPUTS: 'outs'
};
DB.CONCURRENCY = 10;
DB.prototype.getBlock = function(hash, callback) {
var self = this;
// get block from bitcoind
this.bitcoind.getBlock(hash, function(err, blockData) {
if(err) {
return callback(err);
}
callback(null, self.Block.fromBuffer(blockData));
});
};
DB.prototype.putBlock = function(block, callback) {
// block is already stored in bitcoind, but we need to update
// our prevhash index still
this._updatePrevHashIndex(block, callback);
};
/*DB.prototype.getTransaction = function(txid, queryMempool, callback) {
};*/
DB.prototype.validateBlockData = function(block, callback) {
// bitcoind does the validation
return callback();
};
DB.prototype.buildGenesisData = function() {
var coinbaseTx = this.buildCoinbaseTransaction();
var bw = new BufferWriter();
bw.writeVarintNum(1);
bw.write(coinbaseTx.toBuffer());
var merkleRoot = this.getMerkleRoot([coinbaseTx]);
var buffer = bw.concat();
return {
merkleRoot: merkleRoot,
buffer: buffer
};
};
DB.prototype.buildCoinbaseTransaction = function(transactions, data) {
if(!this.wallet) {
throw new Error('Wallet required to build coinbase');
}
if(!data) {
data = bitcore.crypto.Random.getRandomBuffer(40);
}
var fees = 0;
if(transactions && transactions.length) {
fees = this.getInputTotal(transactions) - this.getOutputTotal(transactions, true);
}
var coinbaseTx = new this.Transaction();
coinbaseTx.to('1JwSSubhmg6iPtRjtyqhUYYH7bZg3Lfy1T', this.coinbaseAmount + fees);
var script = bitcore.Script.buildDataOut(data);
var input = new bitcore.Transaction.Input({
prevTxId: '0000000000000000000000000000000000000000000000000000000000000000',
outputIndex: 0xffffffff,
sequenceNumber: 4294967295,
script: script
});
coinbaseTx.inputs = [input];
return coinbaseTx;
};
DB.prototype.getOutputTotal = function(transactions, excludeCoinbase) {
var totals = transactions.map(function(tx) {
if(tx.isCoinbase() && excludeCoinbase) {
return 0;
} else {
return tx._getOutputAmount();
}
});
var grandTotal = totals.reduce(function(previousValue, currentValue) {
return previousValue + currentValue;
});
return grandTotal;
};
DB.prototype.getInputTotal = function(transactions) {
var totals = transactions.map(function(tx) {
if(tx.isCoinbase()) {
return 0;
} else {
return tx._getInputAmount();
}
});
var grandTotal = totals.reduce(function(previousValue, currentValue) {
return previousValue + currentValue;
});
return grandTotal;
};
DB.prototype._updateOutputs = function(block, addOutput, callback) {
var txs = this.getTransactionsFromBlock(block);
log.debug('Processing transactions', txs);
log.debug('Updating outputs');
var action = 'put';
if (!addOutput) {
action = 'del';
}
var operations = [];
for (var i = 0; i < txs.length; i++) {
var tx = txs[i];
var txid = tx.id;
var inputs = tx.inputs;
var outputs = tx.outputs;
for (var j = 0; j < outputs.length; j++) {
var output = outputs[j];
var script = output.script;
if(!script) {
log.debug('Invalid script');
continue;
}
if (!script.isPublicKeyHashOut() && !script.isScriptHashOut() && !script.isPublicKeyOut()) {
// ignore for now
log.debug('script was not pubkeyhashout, scripthashout, or pubkeyout');
continue;
}
var address;
if(script.isPublicKeyOut()) {
var pubkey = script.chunks[0].buf;
address = Address.fromPublicKey(new PublicKey(pubkey), this.network);
} else {
address = output.script.toAddress(this.network);
}
var outputIndex = j;
var timestamp = block.timestamp.getTime();
var height = block.height;
operations.push({
type: action,
key: [DB.PREFIXES.OUTPUTS, address, timestamp, txid, outputIndex].join('-'),
value: [output.satoshis, script, height].join(':')
});
}
if(tx.isCoinbase()) {
continue;
}
for (var j = 0; j < inputs.length; j++) {
var input = inputs[j];
var prevTxId = input.prevTxId.toString('hex');
var prevOutputIndex = input.outputIndex;
var timestamp = block.timestamp.getTime();
var inputIndex = j;
operations.push({
type: action,
key: [DB.PREFIXES.SPENTS, prevTxId, prevOutputIndex].join('-'),
value: [txid, inputIndex, timestamp].join(':')
});
}
}
setImmediate(function() {
callback(null, operations);
});
};
DB.prototype._updateTransactions = function(block, addTransaction, callback) {
var self = this;
DB.super_.prototype._updateTransactions.call(self, block, addTransaction, function(err, operations) {
if(err || !addTransaction) {
return callback(err, operations);
}
// Remove transactions from mempool with inputs that were spent
var mempoolTransactions = self.mempool.getTransactions();
var blockTransactions = self.getTransactionsFromBlock(block);
var newMempoolTransactions = [];
for(var i = 0; i < mempoolTransactions.length; i++) {
var txHasInputsInBlock = false;
for(var j = 0; j < mempoolTransactions[i].inputs.length; j++) {
for(var k = 0; k < blockTransactions.length; k++) {
for(var l = 0; l < blockTransactions[k].inputs.length; l++) {
var mempoolInput = mempoolTransactions[i].inputs[j];
var blockInput = blockTransactions[k].inputs[l];
if(mempoolInput.prevTxId === blockInput.prevTxId &&
mempoolInput.outputIndex === blockInput.outputIndex) {
txHasInputsInBlock = true;
break;
}
}
if(txHasInputsInBlock) {
break;
}
}
if(txHasInputsInBlock) {
break;
}
}
if(!txHasInputsInBlock) {
newMempoolTransactions.push(mempoolTransactions[i]);
}
}
self.mempool.transactions = newMempoolTransactions;
callback(null, operations);
});
};
DB.prototype._onChainAddBlock = function(block, callback) {
var self = this;
log.debug('DB handling new chain block');
// Remove block from mempool
self.mempool.removeBlock(block.hash);
async.series([
this._updateOutputs.bind(this, block, true), // add outputs
this._updateTransactions.bind(this, block, true) // add transactions
], function(err, results) {
if (err) {
return callback(err);
}
var operations = [];
for (var i = 0; i < results.length; i++) {
operations = operations.concat(results[i]);
}
log.debug('Updating the database with operations', operations);
self.store.batch(operations, callback);
});
};
DB.prototype._onChainRemoveBlock = function(block, callback) {
var self = this;
async.series([
this._updateOutputs.bind(this, block, false), // remove outputs
this._updateTransactions.bind(this, block, false) // remove transactions
], function(err, results) {
if (err) {
return callback(err);
}
var operations = [];
for (var i = 0; i < results.length; i++) {
operations = operations.concat(results[i]);
}
self.store.batch(operations, callback);
});
};
DB.prototype.getAPIMethods = function() {
return [
['getTransaction', this, this.getTransaction, 2],
['getBalance', this, this.getBalance, 2],
['sendFunds', this, this.sendFunds, 2],
['getOutputs', this, this.getOutputs, 2],
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
['isSpent', this, this.isSpent, 2]
];
};
DB.prototype.getBalance = function(address, queryMempool, callback) {
this.getUnspentOutputs(address, queryMempool, function(err, outputs) {
if(err) {
return callback(err);
}
var satoshis = outputs.map(function(output) {
return output.satoshis;
});
var sum = satoshis.reduce(function(a, b) {
return a + b;
}, 0);
return callback(null, sum);
});
};
DB.prototype.getOutputs = function(address, queryMempool, callback) {
var self = this;
var outputs = [];
var key = [DB.PREFIXES.OUTPUTS, address].join('-');
var stream = this.store.createReadStream({
start: key,
end: key + '~'
});
stream.on('data', function(data) {
var key = data.key.split('-');
var value = data.value.split(':');
var output = {
address: key[1],
txid: key[3],
outputIndex: Number(key[4]),
satoshis: Number(value[0]),
script: value[1],
blockHeight: Number(value[2])
};
outputs.push(output);
});
var error;
stream.on('error', function(streamError) {
if (streamError) {
error = streamError;
}
});
stream.on('close', function() {
if (error) {
return callback(error);
}
if(queryMempool) {
var mempoolOutputs = self._getMempoolOutputs(address);
outputs = outputs.concat(self._getMempoolOutputs(address));
}
callback(null, outputs);
});
return stream;
};
DB.prototype._getMempoolOutputs = function(address) {
var outputs = [];
var transactions = this.mempool.getTransactions();
transactions.forEach(function(tx) {
// add additional info to outputs
var outputObjects = [];
for(var i = 0; i < tx.outputs.length; i++) {
var output = {};
output.script = tx.outputs[i].script.toString();
output.satoshis = tx.outputs[i].satoshis;
output.txid = tx.hash;
output.outputIndex = i;
output.address = tx.outputs[i].script.toAddress().toString();
outputObjects.push(output);
}
var filtered = outputObjects.filter(function(output) {
return address.toString() === output.address;
});
if(filtered.length) {
outputs = outputs.concat(filtered);
}
});
return outputs;
};
DB.prototype.getUnspentOutputs = function(address, queryMempool, callback) {
var self = this;
this.getOutputs(address, queryMempool, function(err, outputs) {
if (err) {
return callback(err);
} else if(!outputs.length) {
return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []);
}
var isUnspent = function(output, callback) {
self.isUnspent(output, queryMempool, callback);
};
async.filter(outputs, isUnspent, function(results) {
callback(null, results);
});
});
};
DB.prototype.isUnspent = function(output, queryMempool, callback) {
this.isSpent(output, queryMempool, function(spent) {
callback(!spent);
});
};
DB.prototype.isSpent = function(output, queryMempool, callback) {
if(queryMempool && this._isSpentMempool(output)) {
return callback(true);
}
this.isSpentDB(output, callback);
};
DB.prototype.isSpentDB = function(output, callback) {
// Query bitcoind
return callback(null, true);
};
DB.prototype._isSpentMempool = function(output) {
// Query bitcoind
return true;
};
module.exports = DB;

8
lib/errors.js Normal file
View File

@ -0,0 +1,8 @@
'use strict';
var createError = require('errno').create;
var chainlib = require('chainlib');
var errors = chainlib.errors;
module.exports = errors;

4
lib/genesis.json Normal file
View File

@ -0,0 +1,4 @@
{
"livenet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000",
"testnet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"
}

254
lib/node.js Normal file
View File

@ -0,0 +1,254 @@
'use strict';
var async = require('async');
var Chain = require('./chain');
var Block = require('./block');
var DB = require('./db');
var chainlib = require('chainlib');
var P2P = chainlib.P2P;
var BaseNode = chainlib.Node;
var util = require('util');
var log = chainlib.log;
var bitcore = require('bitcore');
var Networks = bitcore.Networks;
var _ = bitcore.deps._;
var genesis = require('./genesis.json');
var bitcoind = require('./bitcoind');
function Node(config) {
BaseNode.call(this, config);
this.testnet = config.testnet;
}
util.inherits(Node, BaseNode);
Node.prototype._loadConfiguration = function(config) {
var self = this;
this._loadBitcoind(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.prototype._loadBitcoind = function(config) {
var bitcoindConfig = {};
if (config.testnet) {
bitcoindConfig.directory = '~/.bitcoin/testnet3';
} else {
bitcoindConfig.directory = '~/.bitcoin';
}
// start the bitcoind daemon
this.bitcoind = bitcoind(bitcoindConfig);
};
Node.prototype._syncBitcoind = function() {
var self = this;
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) {
self.bitcoind.getBlock(height + 1, function(err, blockBuffer) {
if (err) {
return next(err);
}
self.chain.addBlock(self.Block.fromBuffer(blockBuffer), next);
});
}, function(err) {
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');
});
};
Node.prototype._loadNetwork = function(config) {
if (config.network) {
Networks.add(config.network);
this.network = Networks.get(config.network.name);
} else if (config.testnet) {
this.network = Networks.get('testnet');
} else {
this.network = Networks.get('livenet');
}
};
Node.prototype._loadDB = function(config) {
if (config.DB) {
// Other modules can inherit from our DB and replace it with their own
DB = config.DB;
}
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;
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 = {};
}
this.Block = Block;
var genesisBlock;
if (config.genesis) {
genesisBlock = config.genesis;
} else if (config.testnet) {
genesisBlock = genesis.testnet;
} else {
genesisBlock = genesis.livenet;
}
if (_.isString(genesisBlock)) {
genesisBlock = this.Block.fromBuffer(new Buffer(genesisBlock, 'hex'));
}
// pass genesis to chain
config.consensus.genesis = genesisBlock;
this.chain = new Chain(config.consensus);
};
Node.prototype._initializeBitcoind = function() {
var self = this;
// Bitcoind
this.bitcoind.on('ready', function(status) {
log.info('Bitcoin Daemon Ready');
self.db.initialize();
});
this.bitcoind.on('open', function(status) {
log.info('Bitcoin Core Daemon Status:', status);
});
this.bitcoind.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype._initializeDatabase = function() {
var self = this;
// Database
this.db.on('ready', function() {
log.info('Bitcoin Database Ready');
self.chain.initialize();
});
this.db.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype._initializeChain = function() {
var self = this;
// 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.emit('ready');
});
this.p2p.on('synced', function() {
log.info('Bitcoin P2P Synced');
self.emit('synced');
});
this.p2p.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype._initialize = function() {
var self = this;
// DB References
this.db.chain = this.chain;
this.db.Block = this.Block;
this.db.bitcoind = this.bitcoind;
// 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);
});
};
module.exports = Node;

133
lib/transaction.js Normal file
View File

@ -0,0 +1,133 @@
'use strict';
var async = require('async');
var bitcore = require('bitcore');
var bitcoinconsensus = require('libbitcoinconsensus');
var Transaction = bitcore.Transaction;
var chainlib = require('chainlib');
var BaseTransaction = chainlib.Transaction;
var BaseDatabase = chainlib.DB;
var levelup = chainlib.deps.levelup;
Transaction.prototype.validate = function(db, poolTransactions, callback) {
var self = this;
if (!(db instanceof BaseDatabase)) {
throw new Error('First argument is expected to be an instance of Database');
}
// coinbase is valid
if (this.isCoinbase()) {
return callback();
}
var verified = this.verify();
if(verified !== true) {
return callback(new Error(verified));
}
async.series(
[
self._validateInputs.bind(self, db, poolTransactions),
self._validateOutputs.bind(self),
self._checkSufficientInputs.bind(self)
],
callback
);
};
Transaction.prototype._validateInputs = function(db, poolTransactions, callback) {
var self = this;
// Verify inputs are unspent
async.each(self.inputs, function(input, next) {
async.series(
[
self._populateInput.bind(self, db, input, poolTransactions),
self._checkSpent.bind(self, db, input, poolTransactions),
self._checkScript.bind(self, input, self.inputs.indexOf(input))
],
next
);
}, callback);
};
Transaction.prototype.populateInputs = function(db, poolTransactions, callback) {
var self = this;
async.each(
this.inputs,
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.getTransactionFromDB(txid, 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();
}
});
};
Transaction.prototype._checkScript = function(input, index, callback) {
if (input.output.script) {
var scriptPubkey = input.output._scriptBuffer;
var txTo = this.toBuffer();
var valid = bitcoinconsensus.verifyScript(scriptPubkey, txTo, index);
if(valid) {
return callback();
}
}
return callback(new Error('Script does not validate'));
};
Transaction.prototype._validateOutputs = function(callback) {
setImmediate(callback);
};
Transaction.prototype._checkSufficientInputs = function(callback) {
var inputTotal = this._getInputAmount();
var outputTotal = this._getOutputAmount();
if(inputTotal < outputTotal) {
return callback(new Error('Insufficient inputs'));
} else {
return callback();
}
};
Transaction.manyToBuffer = function(transactions) {
return BaseTransaction.manyToBuffer(transactions);
};
module.exports = Transaction;

View File

@ -20,6 +20,10 @@
{
"name": "Chris Kleeschulte",
"email": "chrisk@bitpay.com"
},
{
"name": "Patrick Nagurny",
"email": "patrick@bitpay.com"
}
],
"scripts": {
@ -36,10 +40,13 @@
"bindings": "^1.2.1",
"mkdirp": "0.5.0",
"nan": "1.3.0",
"tiny": "0.0.10"
"tiny": "0.0.10",
"chainlib": "^0.1.1",
"libbitcoinconsensus": "^0.0.3",
"errno": "^0.1.2",
"async": "1.3.0"
},
"devDependencies": {
"async": "1.2.1",
"benchmark": "1.0.0",
"bitcoin": "^2.3.2",
"bitcore": "^0.12.12",