diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 148ce031..684c39b1 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -42,6 +42,8 @@ function Block(data, subtype) { this.network = data.network || false; this.relayedBy = data.relayedBy || '0.0.0.0'; + this._chain = data.chain; + this.valid = null; this._hash = null; @@ -226,7 +228,8 @@ Block.prototype.getMerkleRoot = function getMerkleRoot() { // This mimics the behavior of CheckBlockHeader() // and CheckBlock() in bitcoin/src/main.cpp. Block.prototype._verify = function _verify() { - var i, unique, hash, merkleRoot; + var uniq = {}; + var i, tx, hash; // Check proof of work matches claimed amount if (!utils.testTarget(this.bits, this.hash())) @@ -251,55 +254,106 @@ Block.prototype._verify = function _verify() { } // First TX must be a coinbase - if (!this.txs.length - || this.txs[0].inputs.length !== 1 - || +this.txs[0].inputs[0].out.hash !== 0) + if (!this.txs.length || !this.txs[0].isCoinbase()) return false; - // The rest of the txs must not be coinbases - for (i = 1; i < this.txs.length; i++) { - if (this.txs[i].inputs.length === 1 - && +this.txs[i].inputs[0].out.hash === 0) - return false; - } - - // Check for duplicate tx ids - unique = {}; + // Test all txs for (i = 0; i < this.txs.length; i++) { - hash = this.txs[i].hash('hex'); - if (unique[hash]) - return false; - unique[hash] = true; - } + tx = this.txs[i]; - // Build MerkleTree - merkleRoot = this.getMerkleRoot(); + // The rest of the txs must not be coinbases + if (i > 0 && tx.isCoinbase()) + return false; + + // Check for duplicate txids + hash = tx.hash('hex'); + if (uniq[hash]) + return false; + uniq[hash] = true; + } // Check merkle root - if (merkleRoot !== this.merkleRoot) + if (this.getMerkleRoot() !== this.merkleRoot) return false; return true; }; -Block.prototype.getHeight = function getHeight(chain) { - chain = chain || bcoin.chain.global; +Block.prototype.postVerify = function postVerify() { + var prev, i, tx, cb; - if (!chain) - return -1; + if (this.subtype !== 'block') + return true; - return chain.getHeight(this.hash('hex')); + if (!this.chain) + return true; + + prev = this.chain.getBlock(this.prevBlock); + + // Ensure it's not an orphan + if (!prev) + return false; + + // Ensure the timestamp is correct + if (this.ts <= prev.getMedianTime()) + return false; + + // Test all txs + for (i = 0; i < this.txs.length; i++) { + tx = this.txs[i]; + + // TXs must be finalized with regards to seq and locktime + if (!tx.isFinal(this, prev)) + return false; + } + + if (this.bits !== this.chain.target(prev, this)) + return false; + + if (this.version < 2 && prev.isOutdated(2)) + return false; + + if (this.version < 3 && prev.isOutdated(3)) + return false; + + if (this.version < 4 && prev.isOutdated(4)) + return false; + + // Enforce height in coinbase + if (this.version >= 2 && prev.needsUpgrade(2)) { + cb = bcoin.script.isCoinbase(this.txs[0].inputs[0].script, this); + + if (!cb) + return false; + + if (cb.height !== prev.height + 1) + return false; + } + + // sig validation (bip66) + if (this.version >= 3 && prev.needsUpgrade(3)) + this.scriptFlags |= 1; // dersig + + // checklocktimeverify (bip65) + if (this.version >= 4 && prev.needsUpgrade(4)) + this.scriptFlags |= 2; + + return true; }; -Block.prototype.getNextBlock = function getNextBlock(chain) { +Block.prototype.getHeight = function getHeight() { + if (!this.chain) + return -1; + return this.chain.getHeight(this.hash('hex')); +}; + +Block.prototype.getNextBlock = function getNextBlock() { var next; - chain = chain || bcoin.chain.global; - - if (!chain) + if (!this.chain) return utils.toHex(constants.zeroHash); - next = chain.getNextBlock(this.hash('hex')); + next = this.chain.getNextBlock(this.hash('hex')); if (!next) return utils.toHex(constants.zeroHash); @@ -361,26 +415,32 @@ Block.prototype.getReward = function getReward() { }; }; -Block.prototype.getEntry = function getEntry(chain) { - chain = chain || bcoin.chain.global; - return chain.getBlock(this); +Block.prototype.getEntry = function getEntry() { + if (!this.chain) + return; + return this.chain.getBlock(this); }; -Block.prototype.isOrphan = function isOrphan(chain) { - chain = chain || bcoin.chain.global; - return chain.hasBlock(this.prevBlock); +Block.prototype.isOrphan = function isOrphan() { + if (!this.chain) + return true; + return this.chain.hasBlock(this.prevBlock); }; +Block.prototype.__defineGetter__('chain', function() { + return this._chain || bcoin.chain.global; +}); + Block.prototype.__defineGetter__('rhash', function() { return utils.revHex(this.hash('hex')); }); Block.prototype.__defineGetter__('height', function() { - return this.getHeight(bcoin.chain.global); + return this.getHeight(); }); Block.prototype.__defineGetter__('nextBlock', function() { - return this.getNextBlock(bcoin.chain.global); + return this.getNextBlock(); }); Block.prototype.__defineGetter__('reward', function() { @@ -399,17 +459,18 @@ Block.prototype.__defineGetter__('coinbase', function() { }); Block.prototype.__defineGetter__('entry', function() { - return this.getEntry(bcoin.chain.global); + return this.getEntry(); }); Block.prototype.__defineGetter__('orphan', function() { - return this.isOrphan(bcoin.chain.global); + return this.isOrphan(); }); Block.prototype.inspect = function inspect() { var copy = bcoin.block(this, this.subtype); copy.__proto__ = null; delete copy._raw; + delete copy._chain; copy.hash = this.hash('hex'); copy.rhash = this.rhash; copy.height = this.height; diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index fb942284..ce7f2208 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -149,7 +149,7 @@ Chain.prototype._addIndex = function _addIndex(entry) { return Chain.codes.unchanged; } - // Duplcate height + // Duplicate height if (this.index.hashes[entry.height] === entry.hash) return Chain.codes.unchanged; @@ -294,8 +294,16 @@ Chain.prototype.add = function add(block, peer) { // If we have a block at the same height, use chain with higher work if (this.index.hashes[entry.height]) { if (this.tip.chainwork.cmp(entry.chainwork) < 0) { + if (!block.postVerify()) { + throw new Error; + //code = Chain.codes.invalid; + //break; + } this.resetHeight(entry.height - 1); - this._addIndex(entry); + code = this._addIndex(entry); + assert(code !== Chain.codes.unchanged); + if (code !== Chain.codes.okay) + break; code = Chain.codes.forked; // Breaking here only works because // we deleted the orphan map in resetHeight. @@ -308,7 +316,15 @@ Chain.prototype.add = function add(block, peer) { } // Validated known block at this point - add it to index + if (!block.postVerify()) { + throw new Error; + //code = Chain.codes.invalid; + //break; + } code = this._addIndex(entry); + assert(code !== Chain.codes.unchanged); + if (code !== Chain.codes.okay) + break; this.emit('block', block, peer); this.emit('entry', entry); if (block !== initial) @@ -531,26 +547,24 @@ Chain.prototype.height = function height() { }; Chain.prototype.target = function target(last, block) { - var proofOfWorkLimit = utils.toCompact(network.powLimit); - var adjustmentInterval = network.powTargetTimespan / network.powTargetSpacing; - var newBlockTs, heightFirst, first; - - adjustmentInterval |= 0; + var powLimit = utils.toCompact(network.powLimit); + var interval = network.powTargetTimespan / network.powTargetSpacing | 0; + var first, ts; if (!last) last = this.getTip(); // Do not retarget - if ((last.height + 1) % adjustmentInterval) { + if ((last.height + 1) % interval) { if (network.powAllowMinDifficultyBlocks) { // Special behavior for testnet: - newBlockTs = block ? block.ts : utils.now(); - if (newBlockTs > last.ts + network.powTargetSpacing * 2) - return proofOfWorkLimit; + ts = block ? (block.ts || block) : utils.now(); + if (ts > last.ts + network.powTargetSpacing * 2) + return powLimit; while (last.prev - && last.height % adjustmentInterval !== 0 - && last.bits !== proofOfWorkLimit) { + && last.height % interval !== 0 + && last.bits !== powLimit) { last = last.prev; } @@ -560,23 +574,22 @@ Chain.prototype.target = function target(last, block) { } // Back 2 weeks - heightFirst = last.height - (adjustmentInterval - 1); - first = this.byHeight(heightFirst); + first = this.byHeight(last.height - (interval - 1)); if (!first) return 0; - return this.retarget(last, first.ts); + return this.retarget(last, first); }; -Chain.prototype.retarget = function retarget(last, firstTs) { +Chain.prototype.retarget = function retarget(last, first) { var powTargetTimespan = new bn(network.powTargetTimespan); var actualTimespan, powLimit, target; if (network.powNoRetargeting) return last.bits; - actualTimespan = new bn(last.ts).subn(firstTs); + actualTimespan = new bn(last.ts).subn(first.ts); if (actualTimespan.cmp(powTargetTimespan.divn(4)) < 0) actualTimespan = powTargetTimespan.divn(4); @@ -752,6 +765,45 @@ ChainBlock.prototype.getChainwork = function() { return (this.prev ? this.prev.chainwork : new bn(0)).add(this.proof); }; +ChainBlock.prototype.getMedianTime = function() { + var entry = this; + var median = []; + var timeSpan = constants.block.medianTimespan; + var i; + + for (i = 0; i < timeSpan && entry; i++, entry = entry.prev) + median.push(entry.ts); + + median = median.sort(); + + return median[median.length / 2 | 0]; +}; + +ChainBlock.prototype.isOutdated = function(version) { + return this.isSuperMajority(version, + network.block.majorityRejectBlockOutdated); +}; + +ChainBlock.prototype.needsUpgrade = function(version) { + return this.isSuperMajority(version, + network.block.majorityEnforceBlockUpgrade); +}; + +ChainBlock.prototype.isSuperMajority = function(version, required) { + var entry = this; + var found = 0; + var majorityWindow = network.block.majorityWindow; + var i; + + for (i = 0; i < majorityWindow && found < required && entry; i++) { + if (entry.version >= version) + found++; + entry = entry.prev; + } + + return found >= required; +}; + ChainBlock.prototype.toJSON = function() { // return [ // this.hash, diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index b27086d3..8bea3ba7 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -134,12 +134,17 @@ Miner.prototype.addTX = function addTX(tx) { return; } - // Pretty important + // Ignore if it's already in a block + if (tx.height !== -1) + return; + if (!tx.verify()) return; - // Ignore if it's already in a block - if (tx.height !== -1) + if (tx.isCoinbase()) + return; + + if (!tx.isFinal(this.block, this.last)) return; // Deliver me from the block size debate, please @@ -161,10 +166,12 @@ Miner.prototype.addTX = function addTX(tx) { }; Miner.prototype.createBlock = function createBlock(tx) { - var target, coinbase, headers, block; + var ts, target, coinbase, headers, block; - // Update target - target = this.chain.target(this.last); + ts = Math.max(utils.now(), this.last.ts + 1); + + // Find target + target = this.chain.target(this.last, ts); // Create a coinbase coinbase = bcoin.tx(); @@ -194,7 +201,7 @@ Miner.prototype.createBlock = function createBlock(tx) { ? this.last.hash('hex') : this.last.hash, merkleRoot: utils.toHex(constants.zeroHash.slice()), - ts: utils.now(), + ts: ts, bits: utils.toCompact(target), nonce: 0 }; diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index e7296264..a5fc2133 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -149,8 +149,9 @@ Peer.prototype._init = function init() { this._req('verack', function(err, payload) { if (err) { + self._error(err); self.destroy(); - return self._error(err); + return; } self.ack = true; self.emit('ack'); @@ -318,7 +319,7 @@ Peer.prototype._res = function _res(cmd, payload) { for (i = 0; i < this._request.queue.length; i++) { entry = this._request.queue[i]; - if (!entry || entry.cmd && entry.cmd !== cmd) + if (!entry || (entry.cmd && entry.cmd !== cmd)) return false; res = entry.cb(null, payload, cmd); diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 94eeba5c..89896eea 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -43,7 +43,9 @@ function Pool(options) { this.size = options.size || 32; this.parallel = options.parallel || 2000; this.redundancy = options.redundancy || 2; - this.seeds = network.seeds.slice(); + this.seeds = options.seeds + ? options.seeds.slice() + : network.seeds.slice(); this._createConnection = options.createConnection; this._createSocket = options.createSocket; @@ -280,6 +282,10 @@ Pool.prototype._addLoader = function _addLoader() { relay: this.options.relay }); + peer.once('socket', function() { + self.emit('debug', 'Added loader peer: %s', peer.host); + }); + this.peers.load = peer; this.peers.all.push(peer); @@ -494,8 +500,10 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer) { return; // Make sure the block is valid - if (!block.verify()) + if (!block.verify()) { + this.emit('debug', 'Block verification failed for %s', block.hash('hex')); return; + } // Resolve orphan chain if (!this.options.headers) { @@ -768,8 +776,15 @@ Pool.prototype._removePeer = function _removePeer(peer) { if (i !== -1) this.peers.all.splice(i, 1); - if (this.peers.load === peer) + if (this.peers.load === peer) { + this.emit('debug', 'Removed loader peer (%s).', peer.host); this.peers.load = null; + // i = this.seeds.indexOf(peer.host); + // if (i === -1) + // i = this.seeds.indexOf(peer.host + ':' + peer.port); + // if (i !== -1) + // this.seeds.splice(i, 1); + } }; Pool.prototype.watch = function watch(id) { @@ -1407,9 +1422,15 @@ Pool.prototype.usableSeed = function usableSeed(addrs, force) { if (!addrs) addrs = this.seeds; - for (i = 0; i < addrs.length; i++) { - if (!this.getPeer(addrs[i])) - return addrs[i]; + addrs = addrs.slice().sort(function() { + return Math.random() > 0.50 ? 1 : -1; + }); + + if (this.peers.loader) { + for (i = 0; i < addrs.length; i++) { + if (!this.getPeer(addrs[i])) + return addrs[i]; + } } if (!force) diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 8b1e2320..2717d4ca 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -155,7 +155,8 @@ exports.hashTypeByVal = Object.keys(exports.hashType).reduce(function(out, type) exports.block = { maxSize: 1000000, maxSigops: 1000000 / 50, - maxOrphanTx: 1000000 / 100 + maxOrphanTx: 1000000 / 100, + medianTimeSpan: 11 }; exports.script = { diff --git a/lib/bcoin/protocol/network.js b/lib/bcoin/protocol/network.js index 3a0c926b..81246dc8 100644 --- a/lib/bcoin/protocol/network.js +++ b/lib/bcoin/protocol/network.js @@ -140,6 +140,12 @@ main.powTargetSpacing = 10 * 60; main.powAllowMinDifficultyBlocks = false; main.powNoRetargeting = false; +main.block = { + majorityEnforceBlockUpgrade: 750, + majorityRejectBlockOutdated: 950, + majorityWindow: 1000 +}; + /** * Testnet (v3) * https://en.bitcoin.it/wiki/Testnet @@ -237,3 +243,9 @@ testnet.powTargetTimespan = 14 * 24 * 60 * 60; // two weeks testnet.powTargetSpacing = 10 * 60; testnet.powAllowMinDifficultyBlocks = true; testnet.powNoRetargeting = false; + +testnet.block = { + majorityEnforceBlockUpgrade: 51, + majorityRejectBlockOutdated: 75, + majorityWindow: 100 +}; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 7a993df7..a6c658f5 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -37,6 +37,8 @@ function TX(data, block) { this.network = data.network || false; this.relayedBy = data.relayedBy || '0.0.0.0'; + this._chain = data.chain; + this._lock = this.lock; if (data.inputs) { @@ -834,25 +836,66 @@ TX.prototype.funds = function funds(side) { return acc; }; -TX.prototype.getHeight = function getHeight(chain) { - chain = chain || bcoin.chain.global; - - if (!chain) - return -1; - - return this.block ? chain.getHeight(this.block) : -1; +// Used for postVerify/ContextualBlockCheck and miner isFinalTx call. +// BIP113 will require that time-locked transactions have nLockTime set to +// less than the median time of the previous block they're contained in. +TX.prototype.isFinal = function isFinal(block, prev) { + var height = prev.height + 1; + var ts = this.locktimeMedian ? prev.getMedianTime() : block.ts; + return this._isFinal(height, ts); }; -TX.prototype.getConfirmations = function getConfirmations(chain) { +// Used in AcceptToMemoryPool +TX.prototype.isFinalAccept = function isFinalAccept() { + var height = this.chain.height() + 1; + var ts = this.lockTimeMedian + ? this.chain.getTip().getMedianTime() + : utils.now(); + return this._isFinalTx(height, ts); +}; + +// Used in the original bitcoind code for AcceptBlock +TX.prototype._isFinalOriginal = function _isFinalOriginal(block) { + var ts = block ? block.ts : utils.now(); + var height = this.chain.height(); + return this._isFinalTx(height, ts); +}; + +TX.prototype._isFinal = function _isFinal(height, ts) { + var threshold = constants.locktimeThreshold; + var i; + + if (!this.chain) + return true; + + if (this.lock === 0) + return true; + + if (this.lock < (this.lock < threshold ? height : ts)) + return true; + + for (i = 0; i < this.inputs.length; i++) { + if (this.inputs[i].seq !== 0xffffffff) + return false; + } + + return true; +}; + +TX.prototype.getHeight = function getHeight() { + if (!this.chain) + return -1; + return this.block ? this.chain.getHeight(this.block) : -1; +}; + +TX.prototype.getConfirmations = function getConfirmations() { var top, height; - chain = chain || bcoin.chain.global; - - if (!chain) + if (!this.chain) return 0; - top = chain.height(); - height = this.getHeight(chain); + top = this.chain.height(); + height = this.getHeight(); if (height === -1) return 0; @@ -860,6 +903,10 @@ TX.prototype.getConfirmations = function getConfirmations(chain) { return top - height + 1; }; +TX.prototype.__defineGetter__('chain', function() { + return this._chain || bcoin.chain.global; +}); + TX.prototype.__defineGetter__('rblock', function() { return this.block ? utils.revHex(this.block) @@ -879,11 +926,11 @@ TX.prototype.__defineGetter__('value', function() { }); TX.prototype.__defineGetter__('height', function() { - return this.getHeight(bcoin.chain.global); + return this.getHeight(); }); TX.prototype.__defineGetter__('confirmations', function() { - return this.getConfirmations(bcoin.chain.global); + return this.getConfirmations(); }); TX.prototype.inspect = function inspect() { @@ -892,6 +939,7 @@ TX.prototype.inspect = function inspect() { if (this.block) copy.block = this.block; delete copy._raw; + delete copy._chain; copy.hash = this.hash('hex'); copy.rhash = this.rhash; copy.rblock = this.rblock;