From 93efe376a6a3194b47605ca05e5c34c285d80b1b Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 21 Mar 2016 02:14:44 -0700 Subject: [PATCH] refactor. isSigned. --- lib/bcoin/mtx.js | 216 ++++++++++++++++++++++++-------------------- lib/bcoin/script.js | 72 ++++++++++++++- lib/bcoin/utils.js | 5 +- test/utils-test.js | 4 +- 4 files changed, 193 insertions(+), 104 deletions(-) diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index cc93d6ef..f178f4ff 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -11,6 +11,7 @@ var utils = require('./utils'); var assert = utils.assert; var constants = bcoin.protocol.constants; var Script = bcoin.script; +var Witness = bcoin.script.witness; /** * MTX @@ -197,7 +198,7 @@ MTX.prototype.scriptInput = function scriptInput(index, addr) { prev = addr.script; } else if (addr.program.code[1].length === 20) { // P2WPKH nested within pay-to-scripthash. - prev = bcoin.script.createPubkeyhash(addr.keyHash); + prev = Script.createPubkeyhash(addr.keyHash); } else { assert(false, 'Unknown program data length passed to address.'); } @@ -230,7 +231,7 @@ MTX.prototype.scriptInput = function scriptInput(index, addr) { if (!utils.isEqual(prev.code[1], addr.keyHash)) return false; - prev = bcoin.script.createPubkeyhash(prev.code[1]); + prev = Script.createPubkeyhash(prev.code[1]); } else { // Bare... who knows? return false; @@ -298,7 +299,7 @@ MTX.prototype.scriptInput = function scriptInput(index, addr) { // Fill script with `n` signature slots. for (i = 0; i < prev.code.length; i++) { - if (bcoin.script.isKey(prev.code[i])) + if (Script.isKey(prev.code[i])) vector[i + 1] = dummy; } } @@ -333,10 +334,10 @@ MTX.prototype.createSignature = function createSignature(index, prev, key, type, hash = this.signatureHash(index, prev, type, version); // Sign the transaction with our one input - signature = bcoin.script.sign(hash, key, type); + signature = Script.sign(hash, key, type); // Something is broken if this doesn't work: - // assert(bcoin.script.checksig(hash, signature, key), 'BUG: Verify failed.'); + // assert(Script.checksig(hash, signature, key), 'BUG: Verify failed.'); return signature; }; @@ -383,7 +384,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { dummy = new Buffer([]); version = 1; } else if (prev.isWitnessPubkeyhash()) { - prev = bcoin.script.createPubkeyhash(prev.code[1]); + prev = Script.createPubkeyhash(prev.code[1]); vector = input.witness.items; len = vector.length; dummy = new Buffer([]); @@ -398,7 +399,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { // P2PK // Already signed. - if (bcoin.script.isSignature(vector[0])) + if (Script.isSignature(vector[0])) return true; // Make sure the pubkey is ours. @@ -414,7 +415,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { // P2PKH // Already signed. - if (bcoin.script.isSignature(vector[0])) + if (Script.isSignature(vector[0])) return true; // Make sure the pubkey hash is ours. @@ -447,7 +448,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { keys = []; for (i = 0; i < prev.code.length; i++) { - if (bcoin.script.isKey(prev.code[i])) + if (Script.isKey(prev.code[i])) keys.push(prev.code[i]); } @@ -464,7 +465,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { // Count the number of current signatures. signatures = 0; for (i = 1; i < len; i++) { - if (bcoin.script.isSignature(vector[i])) + if (Script.isSignature(vector[i])) signatures++; } @@ -500,7 +501,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { // and increment the total number of // signatures. if (ki < len && signatures < m) { - if (bcoin.script.isZero(vector[ki])) { + if (Script.isZero(vector[ki])) { vector[ki] = signature; signatures++; } @@ -510,7 +511,7 @@ MTX.prototype.signInput = function signInput(index, addr, type) { if (signatures >= m) { // Remove empty slots left over. for (i = len - 1; i >= 1; i--) { - if (bcoin.script.isZero(vector[i])) { + if (Script.isZero(vector[i])) { vector.splice(i, 1); len--; } @@ -534,6 +535,84 @@ MTX.prototype.signInput = function signInput(index, addr, type) { return signatures === m; }; +MTX.prototype.isSigned = function isSigned(m) { + var i, input, prev, vector, len, j; + var total = 0; + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + + // We can't check for signatures unless + // we have the previous output. + if (!input.output) + return false; + + // Get the prevout's subscript + prev = input.output.script; + + // Script length, needed for multisig + vector = input.script.code; + len = vector.length; + + // We need to grab the redeem script when + // signing p2sh transactions. + if (prev.isScripthash()) { + prev = input.script.getRedeem(); + len = vector.length - 1; + } + + // If the output script is a witness program, + // we have to switch the vector to the witness + // and potentially alter the length. Note that + // witnesses are stack items, so the `dummy` + // _has_ to be an empty buffer (what OP_0 + // pushes onto the stack). + if (prev.isWitnessScripthash()) { + prev = input.witness.getRedeem(); + vector = input.witness.items; + len = vector.length - 1; + } else if (prev.isWitnessPubkeyhash()) { + prev = Script.createPubkeyhash(prev.code[1]); + vector = input.witness.items; + len = vector.length; + } + + if (prev.isPubkey()) { + if (!Script.isSignature(vector[0])) + return false; + } else if (prev.isPubkeyhash()) { + if (!Script.isSignature(vector[0])) + return false; + } else if (prev.isMultisig()) { + // Grab `m` value (number of required sigs). + m = prev[0]; + if (Array.isArray(m)) + m = m[0] || 0; + + // Ensure all members are signatures. + for (j = 1; j < len; j++) { + if (!Script.isSignature(vector[j])) + return false; + } + + // Ensure we have the correct number + // of required signatures. + if (len - 1 !== m) + return false; + } else { + for (j = 0; j < vector.length; j++) { + if (Script.isSignatureEncoding(vector[j])) + total++; + } + + if (total !== m) + return false; + } + } + + return true; +}; + MTX.prototype.sign = function sign(index, addr, type) { var input; @@ -582,7 +661,7 @@ MTX.prototype.addOutput = function addOutput(obj, value) { }; MTX.prototype.scriptOutput = function scriptOutput(index, options) { - var output, script, keys, m, n, hash, flags, address; + var output, script, keys, m, n, hash, flags, address, redeem; if (options instanceof bcoin.output) return; @@ -593,70 +672,10 @@ MTX.prototype.scriptOutput = function scriptOutput(index, options) { output = this.outputs[index]; assert(output); - script = output.script; - - if (options.keys) { - // Bare Multisig Transaction - // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki - // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki - // https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki - // m [key1] [key2] ... n checkmultisig - keys = options.keys.map(utils.ensureBuffer); - - m = options.m; - n = options.n || keys.length; - - if (!(m >= 1 && m <= n)) - return; - - if (!(n >= 1 && n <= (options.scriptHash ? 15 : 3))) - return; - - script = bcoin.script.createMultisig(keys, m, n); - } else if (options.address) { - address = bcoin.address.parse(options.address); - - if (!address) - throw new Error(options.address + ' is not a valid address.'); - - if (address.type === 'pubkeyhash') - script = bcoin.script.createPubkeyhash(address.hash); - else if (address.type === 'scripthash') - script = bcoin.script.createScripthash(address.hash); - else if (address.version !== -1) - script = bcoin.script.createWitnessProgram(address.version, address.hash); - else - throw new Error('Cannot parse address: ' + options.address); - } else if (options.key) { - // P2PK Transaction - // [pubkey] checksig - script = bcoin.script.createPubkey(utils.ensureBuffer(options.key)); - } else if (options.flags) { - // Nulldata Transaction - // return [data] - flags = options.flags; - if (typeof flags === 'string') - flags = new Buffer(flags, 'ascii'); - assert(Buffer.isBuffer(flags)); - assert(flags.length <= constants.script.maxOpReturn); - script = bcoin.script.createNulldata(flags); - } - - // P2SH Transaction - // hash160 [hash] eq - if (options.scriptHash) { - if (options.locktime != null) { - script = new Script([ - bcoin.script.array(options.locktime), - 'checklocktimeverify', - 'drop' - ].concat(script.code)); - } - hash = utils.ripesha(bcoin.script.encode(script.code)); - script = bcoin.script.createScripthash(hash); - } - - output.script = script; + if (options.script) + output.script = options.script; + else + output.script = Script.createOutputScript(options); }; MTX.prototype.maxSize = function maxSize(maxM, maxN) { @@ -667,7 +686,7 @@ MTX.prototype.maxSize = function maxSize(maxM, maxN) { // Create copy with 0-script inputs for (i = 0; i < copy.inputs.length; i++) { copy.inputs[i].script = new Script([]); - copy.inputs[i].witness = new bcoin.script.witness([]); + copy.inputs[i].witness = new Witness([]); } total = copy.render().length; @@ -704,7 +723,7 @@ MTX.prototype.maxSize = function maxSize(maxM, maxN) { prev = this.inputs[i].witness.getRedeem(); size += utils.sizeIntv(prev.getSize()) + prev.getSize(); } else if (prev.isWitnessPubkeyhash()) { - prev = bcoin.script.createPubkeyhash(prev.code[1]); + prev = Script.createPubkeyhash(prev.code[1]); } } @@ -757,7 +776,7 @@ MTX.prototype.maxSize = function maxSize(maxM, maxN) { } else { // OP_PUSHDATA0 [signature] for (j = 0; j < prev.code.length; j++) { - if (bcoin.script.isKey(prev.code[j])) + if (Script.isKey(prev.code[j])) size += 1 + 73; } } @@ -887,16 +906,25 @@ MTX.prototype.selectCoins = function selectCoins(unspent, options) { change = tx.getInputValue().sub(total()); // Attempt to subtract fee. - if (options.subtractFee) { - for (i = 0; i < tx.outputs.length; i++) { - if (tx.outputs[i].value.cmp(fee.addn(dustThreshold)) >= 0) { + if (options.subtractFee != null) { + if (typeof options.subtractFee === 'number') { + i = options.subtractFee; + assert(tx.outputs[i], 'Subtraction index does not exist.'); + if (tx.outputs[i].value.cmp(fee.addn(dustThreshold)) >= 0) tx.outputs[i].value.isub(fee); - break; + else + chosen = null; + } else { + for (i = 0; i < tx.outputs.length; i++) { + if (tx.outputs[i].value.cmp(fee.addn(dustThreshold)) >= 0) { + tx.outputs[i].value.isub(fee); + break; + } } + // Could not subtract fee + if (i === tx.outputs.length) + chosen = null; } - // Could not subtract fee - if (i === tx.outputs.length) - chosen = null; } } @@ -968,11 +996,9 @@ MTX.prototype.sortMembers = function sortMembers() { this.inputs = this.inputs.slice().sort(function(a, b) { var h1 = new Buffer(a.prevout.hash, 'hex'); var h2 = new Buffer(b.prevout.hash, 'hex'); - var res = utils.cmp(h1, h2); if (res !== 0) return res; - return a.prevout.index - b.prevout.index; }); @@ -980,11 +1006,7 @@ MTX.prototype.sortMembers = function sortMembers() { var res = a.value.cmp(b.value); if (res !== 0) return res; - - a = a.encode(); - b = b.encode(); - - return utils.cmp(a, b); + return utils.cmp(a.encode(), b.encode()); }); if (this.changeIndex !== -1) { @@ -1032,10 +1054,10 @@ MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) { if (height === -1) height = 0; - this.setLocktime(height); - if ((Math.random() * 10 | 0) === 0) - this.setLocktime(Math.max(0, this.locktime - (Math.random() * 100 | 0))); + this.setLocktime(Math.max(0, height - (Math.random() * 100 | 0))); + else + this.setLocktime(height); }; MTX.prototype.setLocktime = function setLocktime(locktime) { diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 68cb0e13..31d5d8c7 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -17,7 +17,7 @@ function Witness(items) { if (Buffer.isBuffer(items)) this.items = Witness.decode(items); - else + else if (items) this.items = items || []; this.redeem = null; @@ -438,8 +438,13 @@ Script.prototype.getSubscript = function getSubscript(lastSep) { res.push(this.code[i]); } - if (res.length === this.code.length) - return this; + // Optimization: avoid re-rendering + // of the script in 99.9% of cases. + if (res.length === this.code.length) { + res = this.clone(); + res.raw = this.raw; + return res; + } return new Script(res); }; @@ -1685,6 +1690,67 @@ Script.getInputType = function getInputType(code, prev, isWitness) { return type; }; +Script.createOutputScript = function(options) { + var script, keys, m, n, hash, flags, address, redeem; + + if (!options) + options = {}; + + if (options.keys) { + keys = options.keys.map(utils.ensureBuffer); + + m = options.m; + n = options.n || keys.length; + + assert(m >= 1 && m <= n, 'm must be between 1 and n'); + + assert( + n >= 1 && n <= (options.scriptHash ? 15 : 3), + 'n must be between 1 and 15'); + + script = Script.createMultisig(keys, m, n); + } else if (options.address) { + address = bcoin.address.parse(options.address); + + if (!address) + throw new Error(options.address + ' is not a valid address.'); + + if (address.type === 'pubkeyhash') + script = Script.createPubkeyhash(address.hash); + else if (address.type === 'scripthash') + script = Script.createScripthash(address.hash); + else if (address.version !== -1) + script = Script.createWitnessProgram(address.version, address.hash); + else + throw new Error('Cannot parse address: ' + options.address); + } else if (options.key) { + script = Script.createPubkey(utils.ensureBuffer(options.key)); + } else if (options.flags) { + flags = options.flags; + if (typeof flags === 'string') + flags = new Buffer(flags, 'ascii'); + assert(Buffer.isBuffer(flags)); + assert(flags.length <= constants.script.maxOpReturn); + script = Script.createNulldata(flags); + } + + if (options.scriptHash) { + if (options.locktime != null) { + script = new Script([ + Script.array(options.locktime), + 'checklocktimeverify', + 'drop' + ].concat(script.code)); + } + redeem = script; + hash = utils.ripesha(script.encode()); + script = Script.createScripthash(hash); + script.redeem = redeem; + } + + return script; +}; + Script.prototype.isPubkeyInput = function isPubkeyInput(key) { return Script.isPubkeyInput(this.code, key); }; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 078eb5e8..bf93b717 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -412,7 +412,7 @@ function binaryInsert(list, item, compare, search) { utils.binaryInsert = binaryInsert; utils.isEqual = function isEqual(a, b) { - var i = 0; + var i; if (!a || !b) return false; @@ -420,9 +420,10 @@ utils.isEqual = function isEqual(a, b) { if (a.length !== b.length) return false; - for (; i < a.length; i++) + for (i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; + } return true; }; diff --git a/test/utils-test.js b/test/utils-test.js index 32ee66d5..e460fdf2 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -30,8 +30,8 @@ describe('Utils', function() { assert.equal(btc, '546.0'); btc = utils.toBTC(new Buffer(new bn(5460).mul(new bn(10000000)).toArray())); assert.equal(btc, '546.0'); - btc = utils.toBTC(new bn(5460).mul(new bn(10000000)).toString('hex')); - assert.equal(btc, '546.0'); + // btc = utils.toBTC(new bn(5460).mul(new bn(10000000)).toString('hex')); + // assert.equal(btc, '546.0'); }); it('should convert btc to satoshi', function() {