From e258755671cbfdf3eb23cb80878aa0e59798d795 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 27 Apr 2016 16:56:57 -0700 Subject: [PATCH] stop using vsize. switch to cost. --- lib/bcoin/block.js | 60 ++++++++++++++++++--------------- lib/bcoin/chain.js | 34 ++++++++++--------- lib/bcoin/mempool.js | 4 ++- lib/bcoin/miner.js | 7 ++-- lib/bcoin/protocol/constants.js | 12 +++++++ lib/bcoin/protocol/framer.js | 32 +++++++++++++++--- lib/bcoin/script.js | 3 -- lib/bcoin/tx.js | 47 +++++++++++++++++--------- lib/bcoin/uri.js | 38 ++++++++++++++++++++- 9 files changed, 166 insertions(+), 71 deletions(-) diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 6d55e3a9..68e639d1 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -125,13 +125,24 @@ Block.prototype._getSize = function _getSize() { */ Block.prototype.getVirtualSize = function getVirtualSize(force) { + var scale = constants.WITNESS_SCALE_FACTOR; + return (this.getCost() + scale - 1) / scale | 0; +}; + +/** + * Calculate block cost. + * @param {Boolean?} force - If true, always recalculate. + * @returns {Number} cost + */ + +Block.prototype.getCost = function getCost(force) { var size, witnessSize, base; size = this.getSize(force); witnessSize = this.getWitnessSize(force); base = size - witnessSize; - return (base * 4 + witnessSize + 3) / 4 | 0; + return base * (constants.WITNESS_SCALE_FACTOR - 1) + size; }; /** @@ -146,6 +157,18 @@ Block.prototype.getSize = function getSize(force) { return this._size; }; +/** + * Get base block size (without witness). + * @param {Boolean?} force - If true, always recalculate. + * @returns {Number} size + */ + +Block.prototype.getBaseSize = function getBaseSize(force) { + if (force || this._size === 0) + this._getSize(); + return this._size - this._witnessSize; +}; + /** * Get the total size of the witnesses. * @param {Boolean?} force - If true, always recalculate. @@ -202,24 +225,6 @@ Block.prototype.indexOf = function indexOf(hash) { return -1; }; -/** - * Count sigops taking into account the cost vs. witness ops. - * @param {Boolean?} scriptHash - Whether to count redeem script ops. - * @param {Boolean?} accurate - Whether to use accurate counting - * for CHECKMULTISIG(VERIFY). - * @returns {Number} sigops - */ - -Block.prototype.getSigops = function getSigops(scriptHash, accurate) { - var total = 0; - var i; - - for (i = 0; i < this.txs.length; i++) - total += this.txs[i].getSigops(scriptHash, accurate); - - return total; -}; - /** * Calculate merkle root. * @param {String?} enc - Encoding, can be `'hex'` or null. @@ -327,6 +332,7 @@ Block.prototype.__defineGetter__('commitmentHash', function() { Block.prototype._verify = function _verify(ret) { var sigops = 0; + var scale = constants.WITNESS_SCALE_FACTOR; var i, tx, merkle; if (!ret) @@ -337,7 +343,7 @@ Block.prototype._verify = function _verify(ret) { // Size can't be bigger than MAX_BLOCK_SIZE if (this.txs.length > constants.block.MAX_SIZE - || this.getVirtualSize() > constants.block.MAX_SIZE) { + || this.getBaseSize() > constants.block.MAX_SIZE) { ret.reason = 'bad-blk-length'; ret.score = 100; return false; @@ -366,13 +372,13 @@ Block.prototype._verify = function _verify(ret) { return false; // Count legacy sigops (do not count scripthash or witness) - // sigops += tx._getSigops(); - // if (sigops > constants.block.MAX_SIGOPS) { - // return callback(new VerifyError(block, - // 'invalid', - // 'bad-blk-sigops', - // 100)); - // } + sigops += tx.getLegacySigops(); + if (sigops * scale > constants.block.MAX_SIGOPS_COST) { + return callback(new VerifyError(block, + 'invalid', + 'bad-blk-sigops', + 100)); + } } // Check merkle root diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index f2e70544..76984fd5 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -555,6 +555,15 @@ Chain.prototype._verify = function _verify(block, prev, callback) { } } + // Check block cost (different from block size + // check in non-contextual verification). + if (block.getCost() > constants.block.MAX_COST) { + return done(new VerifyError(block, + 'invalid', + 'bad-blk-cost', + 100)); + } + // Get timestamp for tx.isFinal(). ts = (state.lockFlags & constants.flags.MEDIAN_TIME_PAST) !== 0 ? medianTime @@ -819,21 +828,6 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac tx = block.txs[i]; hash = tx.hash('hex'); - // Check for block sigops limits - // Start counting P2SH sigops once block - // timestamps reach March 31st, 2012. - if (flags & constants.flags.VERIFY_P2SH) - sigops += tx.getSigops(true); - else - sigops += tx.getSigops(); - - if (sigops > constants.block.MAX_SIGOPS) { - return callback(new VerifyError(block, - 'invalid', - 'bad-blk-sigops', - 100)); - } - // Coinbases do not have prevouts if (tx.isCoinbase()) continue; @@ -876,6 +870,16 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac } } + // Count sigops (legacy + scripthash? + witness?) + sigops += tx.getSigopsCost(flags); + + if (sigops > constants.block.MAX_SIGOPS_COST) { + return callback(new VerifyError(block, + 'invalid', + 'bad-blk-sigops', + 100)); + } + if (!tx.checkInputs(height, ret)) { return callback(new VerifyError(block, 'invalid', diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index aa5fc06e..c6065448 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -550,6 +550,8 @@ Mempool.prototype.addTX = function addTX(tx, callback, force) { return callback(err); if (!tx.hasCoins()) { + // if (tx.getSize() > 5000) + // return callback(); if (self.totalSize > constants.mempool.MAX_MEMPOOL_SIZE) { return callback(new VerifyError(tx, 'insufficientfee', @@ -700,7 +702,7 @@ Mempool.prototype.verify = function verify(tx, callback) { 0)); } - if (tx.getSigops(true) > constants.tx.MAX_SIGOPS) { + if (tx.getSigopsCost(flags) > constants.tx.MAX_SIGOPS_COST) { return callback(new VerifyError(tx, 'nonstandard', 'bad-txns-too-many-sigops', diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index d6bd6512..b0297ddb 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -460,15 +460,14 @@ MinerBlock.prototype.updateMerkle = function updateMerkle() { */ MinerBlock.prototype.addTX = function addTX(tx) { - var size; + var cost; if (tx.mutable) tx = tx.toTX(); - size = this.block.getVirtualSize(true) + tx.getVirtualSize(); + cost = this.block.getCost(true) + tx.getCost(); - // Deliver me from the block size debate, please - if (size > constants.block.MAX_SIZE) + if (cost > constants.block.MAX_COST) return false; if (this.block.hasTX(tx)) diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 7f3c47c5..4751e980 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -369,6 +369,14 @@ exports.hashType = { exports.hashTypeByVal = utils.revMap(exports.hashType); +/** + * Amount to multiply base/non-witness sizes by. + * @const {Number} + * @default + */ + +exports.WITNESS_SCALE_FACTOR = 4; + /** * Block-related constants. * @enum {Number} @@ -377,7 +385,9 @@ exports.hashTypeByVal = utils.revMap(exports.hashType); exports.block = { MAX_SIZE: 1000000, + MAX_COST: 4000000, MAX_SIGOPS: 1000000 / 50, + MAX_SIGOPS_COST: 80000, MEDIAN_TIMESPAN: 11, BIP16_TIME: 1333238400, SIGHASH_LIMIT: 1300000000 @@ -404,10 +414,12 @@ exports.bip30 = { exports.tx = { MAX_VERSION: 2, MAX_SIZE: 100000, + MAX_COST: 400000, MIN_FEE: 10000, BARE_MULTISIG: true, FREE_THRESHOLD: exports.COIN.muln(144).divn(250), MAX_SIGOPS: exports.block.MAX_SIGOPS / 5, + MAX_SIGOPS_COST: exports.block.MAX_SIGOPS_COST / 5, COINBASE_MATURITY: 100 }; diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index 77e43750..247ee8f6 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -1423,9 +1423,8 @@ Framer.tx.size = function txSize(tx) { */ Framer.block.virtualSize = function blockVirtualSize(block) { - var sizes = Framer.block.sizes(block); - var base = sizes.size - sizes.witnessSize; - return (base * 4 + sizes.witnessSize + 3) / 4 | 0; + var scale = constants.WITNESS_SCALE_FACTOR; + return (Framer.block.cost(block) + scale - 1) / scale | 0; }; /** @@ -1435,9 +1434,34 @@ Framer.block.virtualSize = function blockVirtualSize(block) { */ Framer.tx.virtualSize = function txVirtualSize(tx) { + var scale = constants.WITNESS_SCALE_FACTOR; + return (Framer.tx.cost(tx) + scale - 1) / scale | 0; +}; + +/** + * Calculate block cost. + * @param {NakedBlock|Block} block + * @returns {Number} cost + */ + +Framer.block.cost = function blockCost(block) { + var sizes = Framer.block.sizes(block); + var base = sizes.size - sizes.witnessSize; + var scale = constants.WITNESS_SCALE_FACTOR; + return base * (scale - 1) + sizes.size; +}; + +/** + * Calculate transaction cost. + * @param {NakedTX|TX} tx + * @returns {Number} cost + */ + +Framer.tx.cost = function txCost(tx) { var sizes = Framer.tx.sizes(tx); var base = sizes.size - sizes.witnessSize; - return (base * 4 + sizes.witnessSize + 3) / 4 | 0; + var scale = constants.WITNESS_SCALE_FACTOR; + return base * (scale - 1) + sizes.size; }; return Framer; diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 24d7d465..3ecbe409 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -3437,9 +3437,6 @@ Script.prototype.getSigops = function getSigops(accurate) { var lastOp = -1; var i, op; - if (flags == null) - flags = constants.flags.STANDARD_VERIFY_FLAGS; - for (i = 0; i < this.code.length; i++) { op = this.code[i]; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 922618a7..78569bd3 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -290,6 +290,17 @@ TX.prototype.getRaw = function getRaw() { */ TX.prototype.getVirtualSize = function getVirtualSize() { + var scale = constants.WITNESS_SCALE_FACTOR; + return (this.getCost() + scale - 1) / scale | 0; +}; + +/** + * Calculate the cost of the transaction. + * Note that this is cached. + * @returns {Number} cost + */ + +TX.prototype.getCost = function getCost() { var size, witnessSize, base; this.getRaw(); @@ -298,7 +309,7 @@ TX.prototype.getVirtualSize = function getVirtualSize() { witnessSize = this._witnessSize; base = size - witnessSize; - return (base * 4 + witnessSize + 3) / 4 | 0; + return base * (constants.WITNESS_SCALE_FACTOR - 1) + size; }; /** @@ -886,11 +897,11 @@ TX.prototype.getLegacySigops = function getLegacySigops() { var total = 0; var i; - for (i = 0; i < tx.inputs.length; i++) - total += tx.inputs[i].script.getSigops(false); + for (i = 0; i < this.inputs.length; i++) + total += this.inputs[i].script.getSigops(false); - for (i = 0; i < tx.outputs.length; i++) - total += tx.outputs[i].script.getSigops(false); + for (i = 0; i < this.outputs.length; i++) + total += this.outputs[i].script.getSigops(false); return total; }; @@ -907,8 +918,12 @@ TX.prototype.getScripthashSigops = function getScripthashSigops() { if (this.isCoinbase()) return 0; - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + + if (!input.coin) + continue; + if (input.coin.script.isScripthash()) total += input.coin.script.getSigops(input.script); } @@ -923,7 +938,7 @@ TX.prototype.getScripthashSigops = function getScripthashSigops() { */ TX.prototype.getSigopsCost = function getSigopsCost(flags) { - var cost = this.getLegacySigops() * 4; + var cost = this.getLegacySigops() * constants.WITNESS_SCALE_FACTOR; var input, i; if (flags == null) @@ -933,10 +948,14 @@ TX.prototype.getSigopsCost = function getSigopsCost(flags) { return cost; if (flags & constants.flags.VERIFY_P2SH) - cost += this.getScripthashSigops() * 4; + cost += this.getScripthashSigops() * constants.WITNESS_SCALE_FACTOR; for (i = 0; i < this.inputs.length; i++) { input = this.inputs[i]; + + if (!input.coin) + continue; + cost += Script.getWitnessSigops( input.script, input.coin.script, @@ -989,7 +1008,7 @@ TX.prototype.isSane = function isSane(ret) { return false; } - if (this.getVirtualSize() > constants.block.MAX_SIZE) { + if (this.getSize() > constants.block.MAX_SIZE) { ret.reason = 'bad-txns-oversize'; ret.score = 100; return false; @@ -1079,7 +1098,7 @@ TX.prototype.isStandard = function isStandard(flags, ret) { return false; } - if (this.getVirtualSize() > constants.tx.MAX_SIZE) { + if (this.getCost() > constants.tx.MAX_COST) { ret.reason = 'tx-size'; return false; } @@ -1162,11 +1181,7 @@ TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { if ((flags & constants.flags.VERIFY_P2SH) && input.coin.script.isScripthash()) { - // Not accurate: - // Failsafe to avoid getting dos'd in case we ever - // call hasStandardInputs before isStandard. - if (!input.script.isPushOnly()) - return false; + assert(input.script.isPushOnly()); stack = new Stack(); diff --git a/lib/bcoin/uri.js b/lib/bcoin/uri.js index c9a124ce..e71c07b2 100644 --- a/lib/bcoin/uri.js +++ b/lib/bcoin/uri.js @@ -7,6 +7,23 @@ var url = require('url'); var querystring = require('querystring'); +var utils = require('./utils'); + +/** + * @typedef {Object} ParsedURI + * @property {Base58Address} address + * @property {BN?} amount? - Amount in satoshis. + * @property {String?} label + * @property {String?} message + * @property {String?} request - Payment request URL. + */ + +/** + * Parse a bitcoin URI. + * @param {String} uri - Bitcoin URI. + * @returns {ParsedURI} + * @throws on non-bitcoin uri + */ exports.parse = function parse(uri) { var data = url.parse(uri); @@ -24,6 +41,12 @@ exports.parse = function parse(uri) { }; }; +/** + * Test whether an object is a bitcoin URI. + * @param {String} uri + * @returns {Boolean} + */ + exports.validate = function validate(uri) { try { exports.parse(uri); @@ -33,6 +56,14 @@ exports.validate = function validate(uri) { } }; +/** + * Encode data as a bitcoin URI. + * @param {ParsedURI|Base58Address} data/address + * @param {BN?} amount + * @returns {String} URI + * @throws when no address provided + */ + exports.stringify = function stringify(address, amount) { var query = {}; var data = address; @@ -57,5 +88,10 @@ exports.stringify = function stringify(address, amount) { if (data.request) query.r = data.request; - return uri + querystring.stringify(query); + query = querystring.stringify(query); + + if (query.length === 0) + return uri; + + return uri + '?' + query; };