From b8b4ac02bf45ba204f4f0d2e1882c799ca1c7a97 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Wed, 15 Jul 2015 18:13:41 -0400 Subject: [PATCH] incorporate chainlib bitcoin into bitcoind.js --- example/bitcoind.js | 54 +++++ lib/block.js | 149 ++++++++++++++ lib/chain.js | 238 ++++++++++++++++++++++ lib/db.js | 480 ++++++++++++++++++++++++++++++++++++++++++++ lib/errors.js | 8 + lib/genesis.json | 4 + lib/node.js | 254 +++++++++++++++++++++++ lib/transaction.js | 133 ++++++++++++ package.json | 11 +- 9 files changed, 1329 insertions(+), 2 deletions(-) create mode 100644 example/bitcoind.js create mode 100644 lib/block.js create mode 100644 lib/chain.js create mode 100644 lib/db.js create mode 100644 lib/errors.js create mode 100644 lib/genesis.json create mode 100644 lib/node.js create mode 100644 lib/transaction.js diff --git a/example/bitcoind.js b/example/bitcoind.js new file mode 100644 index 00000000..320ebf2a --- /dev/null +++ b/example/bitcoind.js @@ -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(); +}); \ No newline at end of file diff --git a/lib/block.js b/lib/block.js new file mode 100644 index 00000000..363d3e42 --- /dev/null +++ b/lib/block.js @@ -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; diff --git a/lib/chain.js b/lib/chain.js new file mode 100644 index 00000000..2d1a61eb --- /dev/null +++ b/lib/chain.js @@ -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; diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 00000000..bdac5058 --- /dev/null +++ b/lib/db.js @@ -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; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 00000000..cd6f5334 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,8 @@ +'use strict'; + +var createError = require('errno').create; +var chainlib = require('chainlib'); + +var errors = chainlib.errors; + +module.exports = errors; diff --git a/lib/genesis.json b/lib/genesis.json new file mode 100644 index 00000000..2c06930b --- /dev/null +++ b/lib/genesis.json @@ -0,0 +1,4 @@ +{ + "livenet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", + "testnet": "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000" +} diff --git a/lib/node.js b/lib/node.js new file mode 100644 index 00000000..264be92a --- /dev/null +++ b/lib/node.js @@ -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; diff --git a/lib/transaction.js b/lib/transaction.js new file mode 100644 index 00000000..af4a386b --- /dev/null +++ b/lib/transaction.js @@ -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; diff --git a/package.json b/package.json index 4f52ccd1..37a3706c 100644 --- a/package.json +++ b/package.json @@ -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",