diff --git a/lib/bcoin.js b/lib/bcoin.js index 473b0af4..40e93265 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -67,7 +67,12 @@ bcoin.coin = require('./bcoin/coin'); bcoin.tx = require('./bcoin/tx'); bcoin.mtx = require('./bcoin/mtx'); bcoin.txpool = require('./bcoin/tx-pool'); -bcoin.block = require('./bcoin/block'); + +bcoin.abstractblock = require('./bcoin/abstractblock'); +bcoin.block = require('./bcoin/block2'); +bcoin.merkleblock = require('./bcoin/merkleblock'); +bcoin.headers = require('./bcoin/headers'); + bcoin.ramdisk = require('./bcoin/ramdisk'); bcoin.blockdb = require('./bcoin/blockdb'); bcoin.spvnode = require('./bcoin/spvnode'); diff --git a/lib/bcoin/abstractblock.js b/lib/bcoin/abstractblock.js new file mode 100644 index 00000000..4c4650fe --- /dev/null +++ b/lib/bcoin/abstractblock.js @@ -0,0 +1,119 @@ +/** + * abstractblock.js - abstract block object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * AbstractBlock + */ + +function AbstractBlock(data) { + var self = this; + + if (!(this instanceof AbstractBlock)) + return new AbstractBlock(data); + + this.type = 'block'; + this.subtype = null; + this.version = data.version; + this.prevBlock = utils.toHex(data.prevBlock); + this.merkleRoot = utils.toHex(data.merkleRoot); + this.ts = data.ts; + this.bits = data.bits; + this.nonce = data.nonce; + this.totalTX = data.totalTX; + this.height = data.height != null ? data.height : -1; + + this._raw = data._raw || null; + this._size = data._size || 0; + + this.relayedBy = data.relayedBy || '0.0.0.0'; + + this._chain = data.chain; + + this.valid = null; + this._hash = null; + + // https://gist.github.com/sipa/bf69659f43e763540550 + // http://lists.linuxfoundation.org/pipermail/bitcoin-dev/2015-August/010396.html + this.versionBits = (this.version >>> 29) & 7; + this.realVersion = this.version & 0x1fffffff; + this.highVersion = this.version & 0x1ffffff8; + this.lowVersion = this.version & 7; +} + +AbstractBlock.prototype.hash = function hash(enc) { + if (!this._hash) + this._hash = utils.dsha256(this.abbr()); + + return enc === 'hex' ? utils.toHex(this._hash) : this._hash; +}; + +AbstractBlock.prototype.abbr = function abbr() { + var res; + + if (this._raw) + return this._raw.slice(0, 80); + + res = new Buffer(80); + utils.write32(res, this.version, 0); + utils.copy(new Buffer(this.prevBlock, 'hex'), res, 4); + utils.copy(new Buffer(this.merkleRoot, 'hex'), res, 36); + utils.writeU32(res, this.ts, 68); + utils.writeU32(res, this.bits, 72); + utils.writeU32(res, this.nonce, 76); + + return res; +}; + +AbstractBlock.prototype.getSize = function getSize() { + return this._size || this.render().length; +}; + +AbstractBlock.prototype.verify = function verify() { + if (this.valid == null) + this.valid = this._verify(); + return this.valid; +}; + +AbstractBlock.prototype.verifyHeaders = function verifyHeaders() { + // Check proof of work + if (!utils.testTarget(this.bits, this.hash())) { + utils.debug('Block failed POW test: %s', this.rhash); + return false; + } + + // Check timestamp against now + 2 hours + if (this.ts > utils.now() + 2 * 60 * 60) { + utils.debug('Block timestamp is too high: %s', this.rhash); + return false; + } + + return true; +}; + +AbstractBlock.prototype.isGenesis = function isGenesis() { + return this.hash('hex') === network.genesis.hash; +}; + +AbstractBlock.prototype.__defineGetter__('chain', function() { + return this._chain || bcoin.chain.global; +}); + +AbstractBlock.prototype.__defineGetter__('rhash', function() { + return utils.revHex(this.hash('hex')); +}); + +/** + * Expose + */ + +module.exports = AbstractBlock; diff --git a/lib/bcoin/block2.js b/lib/bcoin/block2.js new file mode 100644 index 00000000..9a7e9600 --- /dev/null +++ b/lib/bcoin/block2.js @@ -0,0 +1,322 @@ +/** + * block.js - block object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * Block + */ + +function Block(data) { + var self = this; + var tx, height; + + if (!(this instanceof Block)) + return new Block(data); + + bcoin.abstractblock.call(this, data); + + this.subtype = 'block'; + + this.txs = data.txs || []; + + this._cbHeight = null; + + this.txs = this.txs.map(function(data) { + if (data instanceof bcoin.tx) + return data; + + return bcoin.tx(data, self); + }); + + if (!this._raw) + this._raw = this.render(); + + if (!this._size) + this._size = this._raw.length; +} + +utils.inherits(Block, bcoin.abstractblock); + +Block.prototype.render = function render() { + if (this._raw) + return this._raw; + return bcoin.protocol.framer.block(this); +}; + +Block.prototype.getMerkleRoot = function getMerkleRoot() { + var hashes = []; + var i, root; + + assert(this.subtype === 'block'); + + for (i = 0; i < this.txs.length; i++) + hashes.push(this.txs[i].hash()); + + root = utils.getMerkleRoot(hashes); + + if (!root) + return utils.toHex(constants.zeroHash); + + return utils.toHex(root); +}; + +Block.prototype._verify = function _verify() { + var uniq = {}; + var i, tx, hash; + + if (!this.verifyHeaders()) + return false; + + // Size can't be bigger than MAX_BLOCK_SIZE + if (this.txs.length > constants.block.maxSize + || this.getSize() > constants.block.maxSize) { + utils.debug('Block is too large: %s', this.rhash); + return false; + } + + // First TX must be a coinbase + if (!this.txs.length || !this.txs[0].isCoinbase()) { + utils.debug('Block has no coinbase: %s', this.rhash); + return false; + } + + // Test all txs + for (i = 0; i < this.txs.length; i++) { + tx = this.txs[i]; + + // The rest of the txs must not be coinbases + if (i > 0 && tx.isCoinbase()) { + utils.debug('Block more than one coinbase: %s', this.rhash); + return false; + } + + // Check for duplicate txids + hash = tx.hash('hex'); + if (uniq[hash]) { + utils.debug('Block has duplicate txids: %s', this.rhash); + return false; + } + uniq[hash] = true; + } + + // Check merkle root + if (this.getMerkleRoot() !== this.merkleRoot) { + utils.debug('Block failed merkleroot test: %s', this.rhash); + return false; + } + + return true; +}; + +Block.prototype.getCoinbaseHeight = function getCoinbaseHeight() { + var coinbase, s, height; + + if (this.version < 2) + return -1; + + if (this._cbHeight != null) + return this._cbHeight; + + coinbase = this.txs[0]; + + if (!coinbase || coinbase.inputs.length === 0) + return -1; + + s = coinbase.inputs[0].script; + + if (Buffer.isBuffer(s[0])) + height = bcoin.script.num(s[0], true); + else + height = -1; + + this._cbHeight = height; + + return height; +}; + +Block.reward = function reward(height) { + var halvings = height / network.halvingInterval | 0; + var reward; + + if (height < 0) + return new bn(0); + + if (halvings >= 64) + return new bn(0); + + reward = new bn(50).mul(constants.coin); + reward.iushrn(halvings); + + return reward; +}; + +Block.prototype._getReward = function _getReward() { + var reward, base, fee, height; + + if (this._reward) + return this._reward; + + base = Block.reward(this.height); + + if (this.height === -1) { + return this._reward = { + fee: new bn(0), + reward: base, + base: base + }; + } + + reward = this.txs[0].outputs.reduce(function(total, output) { + total.iadd(output.value); + return total; + }, new bn(0)); + + fee = reward.sub(base); + + return this._reward = { + fee: fee, + reward: reward, + base: base + }; +}; + +Block.prototype.getBaseReward = function getBaseReward() { + return this._getReward().base; +}; + +Block.prototype.getReward = function getReward() { + return this._getReward().reward; +}; + +Block.prototype.getFee = function getFee() { + return this._getReward().fee; +}; + +Block.prototype.getCoinbase = function getCoinbase() { + var tx; + + tx = this.txs[0]; + if (!tx || !tx.isCoinbase()) + return; + + return tx; +}; + +Block.prototype.inspect = function inspect() { + var copy = bcoin.block(this); + copy.__proto__ = null; + delete copy._raw; + delete copy._chain; + copy.hash = this.hash('hex'); + copy.rhash = this.rhash; + copy.reward = utils.btc(this.getReward()); + copy.fee = utils.btc(this.getFee()); + copy.date = new Date((copy.ts || 0) * 1000).toISOString(); + return copy; +}; + +Block.prototype.toJSON = function toJSON() { + return { + type: 'block', + subtype: this.subtype, + height: this.height, + relayedBy: this.relayedBy, + hash: utils.revHex(this.hash('hex')), + version: this.version, + prevBlock: utils.revHex(this.prevBlock), + merkleRoot: utils.revHex(this.merkleRoot), + ts: this.ts, + bits: this.bits, + nonce: this.nonce, + totalTX: this.totalTX, + txs: this.txs.map(function(tx) { + return tx.toJSON(); + }) + }; +}; + +Block._fromJSON = function _fromJSON(json) { + json.prevBlock = utils.revHex(json.prevBlock); + json.merkleRoot = utils.revHex(json.merkleRoot); + json.txs = json.txs.map(function(tx) { + return bcoin.tx._fromJSON(tx); + }); + return json; +}; + +Block.fromJSON = function fromJSON(json) { + return new Block(Block._fromJSON(json)); +}; + +Block.prototype.toCompact = function toCompact() { + return { + type: 'block', + subtype: this.subtype, + hash: this.hash('hex'), + prevBlock: this.prevBlock, + ts: this.ts, + height: this.height, + relayedBy: this.relayedBy, + block: utils.toHex(this.render()) + }; +}; + +Block._fromCompact = function _fromCompact(json) { + var raw, parser, data; + + assert.equal(json.type, 'block'); + + raw = new Buffer(json.block, 'hex'); + + parser = new bcoin.protocol.parser(); + + data = parser.parseBlock(raw); + + data.height = json.height; + data.relayedBy = json.relayedBy; + + return data; +}; + +Block.fromCompact = function fromCompact(json) { + return new Block(Block._fromCompact(json)); +}; + +Block.prototype.toRaw = function toRaw(enc) { + var data; + + data = this.render(); + + if (enc === 'hex') + data = utils.toHex(data); + + return data; +}; + +Block._fromRaw = function _fromRaw(data, enc) { + var parser = new bcoin.protocol.parser(); + + if (enc === 'hex') + data = new Buffer(data, 'hex'); + + return parser.parseBlock(data); +}; + +Block.fromRaw = function fromRaw(data, enc) { + return new Block(Block._fromRaw(data, enc, subtype), subtype); +}; + +/** + * Expose + */ + +module.exports = Block; diff --git a/lib/bcoin/headers.js b/lib/bcoin/headers.js new file mode 100644 index 00000000..f3d946af --- /dev/null +++ b/lib/bcoin/headers.js @@ -0,0 +1,87 @@ +/** + * headers.js - headers object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * Headers + */ + +function Headers(data) { + var self = this; + + if (!(this instanceof Headers)) + return new Headers(data); + + bcoin.abstractblock.call(this, data); + + this.subtype = 'headers' + + if (!this._raw) + this._raw = this.render(); + + if (!this._size) + this._size = this._raw.length; +} + +utils.inherits(Headers, bcoin.abstractblock); + +Headers.prototype.render = function render() { + if (this._raw) + return this._raw; + return bcoin.protocol.framer.headers(this); +}; + +Headers.prototype._verify = function _verify() { + return this.verifyHeaders(); +}; + +Headers.prototype.inspect = function inspect() { + var copy = bcoin.headers(this); + copy.__proto__ = null; + delete copy._raw; + delete copy._chain; + copy.hash = this.hash('hex'); + copy.rhash = this.rhash; + copy.date = new Date((copy.ts || 0) * 1000).toISOString(); + return copy; +}; + +Headers.prototype.toRaw = function toRaw(enc) { + var data; + + data = this.render(); + + if (enc === 'hex') + data = utils.toHex(data); + + return data; +}; + +Headers._fromRaw = function _fromRaw(data, enc) { + var parser = new bcoin.protocol.parser(); + + if (enc === 'hex') + data = new Buffer(data, 'hex'); + + return parser.parseHeaders(data); +}; + +Headers.fromRaw = function fromRaw(data, enc) { + return new Headers(Headers._fromRaw(data, enc, subtype)); +}; + + +/** + * Expose + */ + +module.exports = Headers; diff --git a/lib/bcoin/merkleblock.js b/lib/bcoin/merkleblock.js new file mode 100644 index 00000000..3989a605 --- /dev/null +++ b/lib/bcoin/merkleblock.js @@ -0,0 +1,168 @@ +/** + * merkleblock.js - merkleblock object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; + +/** + * MerkleBlock + */ + +function MerkleBlock(data) { + if (!(this instanceof MerkleBlock)) + return new MerkleBlock(data); + + bcoin.abstractblock.call(this, data); + + this.subtype = 'merkleblock' + + this.hashes = (data.hashes || []).map(function(hash) { + return utils.toHex(hash); + }); + this.flags = data.flags || []; + this.txs = data.txs || []; + + // List of matched TXs + this.tx = []; + + // TXs that will be pushed on + this.txs = data.txs || []; + + if (!this._raw) + this._raw = this.render(); + + if (!this._size) + this._size = this._raw.length; +} + +utils.inherits(MerkleBlock, bcoin.abstractblock); + +MerkleBlock.prototype.render = function render() { + if (this._raw) + return this._raw; + return bcoin.protocol.framer.merkleBlock(this); +}; + +MerkleBlock.prototype.hasTX = function hasTX(hash) { + return this.tx.indexOf(hash) !== -1; +}; + +MerkleBlock.prototype._verifyPartial = function _verifyPartial() { + var height = 0; + var tx = []; + var i = 0; + var j = 0; + var hashes = this.hashes; + var flags = this.flags; + var i, root; + + if (this.subtype !== 'merkleblock') + return; + + // Count leaves + for (i = this.totalTX; i > 0; i >>= 1) + height++; + + if (this.totalTX > (1 << (height - 1))) + height++; + + function visit(depth) { + var flag, left, right; + + if (i === flags.length * 8 || j === hashes.length) + return null; + + flag = (flags[i >> 3] >>> (i & 7)) & 1; + i++; + + if (flag === 0 || depth === height) { + if (depth === height) + tx.push(hashes[j]); + return hashes[j++]; + } + + // Go deeper + left = visit(depth + 1); + if (!left) + return null; + + right = visit(depth + 1); + if (right === left) + return null; + + if (!right) + right = left; + + return utils.toHex(utils.dsha256(left + right, 'hex')); + } + + root = visit(1); + + if (!root || root !== this.merkleRoot) + return false; + + this.tx = tx; + + return true; +}; + +MerkleBlock.prototype._verify = function _verify() { + if (!this.verifyHeaders()) + return false; + + // Verify the partial merkle tree if we are a merkleblock. + if (!this._verifyPartial()) { + utils.debug('MerkleBlock failed merkle test: %s', this.rhash); + return false; + } + + return true; +}; + +MerkleBlock.prototype.inspect = function inspect() { + var copy = bcoin.merkleblock(this); + copy.__proto__ = null; + delete copy._raw; + delete copy._chain; + copy.hash = this.hash('hex'); + copy.rhash = this.rhash; + copy.date = new Date((copy.ts || 0) * 1000).toISOString(); + return copy; +}; + +MerkleBlock.prototype.toRaw = function toRaw(enc) { + var data; + + data = this.render(); + + if (enc === 'hex') + data = utils.toHex(data); + + return data; +}; + +MerkleBlock._fromRaw = function _fromRaw(data, enc) { + var parser = new bcoin.protocol.parser(); + + if (enc === 'hex') + data = new Buffer(data, 'hex'); + + return parser.parseMerkleBlock(data); +}; + +MerkleBlock.fromRaw = function fromRaw(data, enc) { + return new MerkleBlock(MerkleBlock._fromRaw(data, enc)); +}; + +/** + * Expose + */ + +module.exports = MerkleBlock; diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index bcda7d92..0ae3053d 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -1197,4 +1197,3 @@ MTX.prototype.toTX = function toTX() { */ module.exports = MTX; - diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index ebde3c3a..89ac9abc 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -417,11 +417,11 @@ Peer.prototype._onPacket = function onPacket(packet) { if (cmd === 'block') { payload.network = true; payload.relayedBy = this.host || '0.0.0.0'; - payload = bcoin.block(payload, 'block'); + payload = bcoin.block(payload); } else if (cmd === 'merkleblock') { payload.network = true; payload.relayedBy = this.host || '0.0.0.0'; - payload = bcoin.block(payload, 'merkleblock'); + payload = bcoin.merkleblock(payload); this.lastBlock = payload; return; } else if (cmd === 'tx') { diff --git a/test/block-test.js b/test/block-test.js index 0e50dff2..49cbbaba 100644 --- a/test/block-test.js +++ b/test/block-test.js @@ -4,7 +4,7 @@ var bcoin = require('../'); describe('Block', function() { var parser = bcoin.protocol.parser(); - var block = bcoin.block({ + var block = bcoin.merkleblock({ type: 'block', version: 2, prevBlock: 'd1831d4411bdfda89d9d8c842b541beafd1437fc560dbe5c0000000000000000', @@ -26,7 +26,7 @@ describe('Block', function() { '33825657ba32afe269819f01993bd77baba86379043168c94845d32370e53562' ], flags: [ 245, 90, 0 ] }, 'merkleblock'); - var raw = block.toCompact().block; + var raw = bcoin.utils.toHex(block.toRaw()); it('should parse partial merkle tree', function() { assert(block.verify()); @@ -40,21 +40,19 @@ describe('Block', function() { }); it('should decode/encode with parser/framer', function() { - var b = bcoin.block(parser.parseMerkleBlock(new Buffer(raw, 'hex')), 'merkleblock'); + var b = bcoin.merkleblock(parser.parseMerkleBlock(new Buffer(raw, 'hex'))); assert.equal(bcoin.utils.toHex(b.render()), raw); }); it('should be verifiable', function() { - var b = bcoin.block(parser.parseMerkleBlock(new Buffer(raw, 'hex')), 'merkleblock'); + var b = bcoin.merkleblock(parser.parseMerkleBlock(new Buffer(raw, 'hex'))); assert(b.verify()); }); it('should be jsonified and unjsonified and still verify', function() { - var json = block.toCompact(); - assert.equal(json.subtype, 'merkleblock'); - assert.equal(typeof json.block, 'string'); - var b = bcoin.block.fromCompact(json); - assert.equal(bcoin.utils.toHex(b.render()), json.block); + var json = block.toRaw(); + var b = bcoin.merkleblock.fromRaw(json); + assert.equal(b.render(), json); assert(b.verify()); }); });