diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 0bf40344..876ade68 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -599,7 +599,7 @@ Stack.prototype.toalt = function toalt() { Stack.prototype.fromalt = function fromalt() { if (this.alt.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_FROMALTSTACK); + throw new ScriptError('INVALID_ALTSTACK_OPERATION', opcodes.OP_FROMALTSTACK); this.push(this.alt.pop()); }; @@ -1009,30 +1009,6 @@ Script.prototype.getSubscript = function getSubscript(lastSep) { return new Script(code); }; -/** - * Execute the script and return false on script execution errors. - * @param {Stack} stack - Script execution stack. - * @param {Number?} flags - Script standard flags. - * @param {TX?} tx - Transaction being verified. - * @param {Number?} index - Index of input being verified. - * @param {Number?} version - Signature hash version (0=legacy, 1=segwit). - * @returns {Boolean} Whether the execution was successful. - */ - -Script.prototype.execute = function execute(stack, flags, tx, index, version) { - try { - return this.interpret(stack, flags, tx, index, version); - } catch (e) { - if (e.type === 'ScriptError') { - bcoin.debug('Script error: %s.', e.message); - } else { - bcoin.debug('Script interpreter threw:'); - bcoin.debug(e.stack + ''); - } - return false; - } -}; - /** * Execute and interpret the script. * @param {Stack} stack - Script execution stack. @@ -1044,7 +1020,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { * @returns {Boolean} Whether the execution was successful. */ -Script.prototype.interpret = function interpret(stack, flags, tx, index, version) { +Script.prototype.execute = function execute(stack, flags, tx, index, version) { var ip = 0; var lastSep = -1; var opCount = 0; @@ -1064,7 +1040,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version stack.negate = 0; if (this.getSize() > constants.script.MAX_SIZE) - throw new ScriptError('SCRIPT_SIZE', null, -1); + throw new ScriptError('SCRIPT_SIZE'); for (ip = 0; ip < this.code.length; ip++) { op = this.code[ip]; @@ -1200,7 +1176,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version } if (!tx) - throw new ScriptError('NO_TX', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); @@ -1226,7 +1202,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version } if (!tx) - throw new ScriptError('NO_TX', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); @@ -1355,7 +1331,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version res = utils.equals(stack.pop(), stack.pop()); if (op === opcodes.OP_EQUALVERIFY) { if (!res) - throw new ScriptError('VERIFY', op, ip); + throw new ScriptError('EQUALVERIFY', op, ip); } else { stack.push(res ? STACK_TRUE : STACK_FALSE); } @@ -1466,12 +1442,12 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version break; case opcodes.OP_LSHIFT: if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) - throw new ScriptError('BAD_SHIFT', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); n = n1.ushln(n2.toNumber()); break; case opcodes.OP_RSHIFT: if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) - throw new ScriptError('BAD_SHIFT', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); n = n1.ushrn(n2.toNumber()); break; case opcodes.OP_BOOLAND: @@ -1570,7 +1546,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version case opcodes.OP_CHECKSIGVERIFY: case opcodes.OP_CHECKSIG: { if (!tx) - throw new ScriptError('NO_TX', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); @@ -1582,11 +1558,8 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version if (version === 0) subscript.removeData(sig); - if (!Script.isValidSignature(sig, flags)) - throw new ScriptError('SIG_DER_OR_HASHTYPE', op, ip); - - if (!Script.isValidKey(key, flags)) - throw new ScriptError('PUBKEYTYPE', op, ip); + Script.validateSignature(sig, flags); + Script.validateKey(key, flags); type = sig[sig.length - 1]; @@ -1605,7 +1578,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version case opcodes.OP_CHECKMULTISIGVERIFY: case opcodes.OP_CHECKMULTISIG: { if (!tx) - throw new ScriptError('NO_TX', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); i = 1; if (stack.length < i) @@ -1653,11 +1626,8 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version sig = stack.top(-isig); key = stack.top(-ikey); - if (!Script.isValidSignature(sig, flags)) - throw new ScriptError('SIG_DER_OR_HASHTYPE', op, ip); - - if (!Script.isValidKey(key, flags)) - throw new ScriptError('PUBKEYTYPE', op, ip); + Script.validateSignature(sig, flags); + Script.validateKey(key, flags); type = sig[sig.length - 1]; hash = tx.signatureHash(index, subscript, type, version); @@ -1682,7 +1652,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version if (flags & constants.flags.VERIFY_NULLDUMMY) { if (!Script.isDummy(stack.top(-1))) - throw new ScriptError('VERIFY_NULLDUMMY', op, ip); + throw new ScriptError('SIG_NULLDUMMY', op, ip); } stack.pop(); @@ -1713,7 +1683,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version v2 = Script.num(stack.pop(), flags).toNumber(); // begin v1 = stack.pop(); // string if (v2 < 0 || v3 < v2) - throw new ScriptError('STRING_OUT_OF_RANGE', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'String out of range.'); if (v2 > v1.length) v2 = v1.length; if (v3 > v1.length) @@ -1728,7 +1698,7 @@ Script.prototype.interpret = function interpret(stack, flags, tx, index, version v2 = Script.num(stack.pop(), flags).toNumber(); // size v1 = stack.pop(); // string if (v2 < 0) - throw new ScriptError('STRING_OUT_OF_RANGE', op, ip); + throw new ScriptError('UNKNOWN_ERROR', 'String size out of range.'); if (v2 > v1.length) v2 = v1.length; if (op === opcodes.OP_LEFT) @@ -1905,7 +1875,7 @@ Script.num = function num(value, flags, size) { size = 4; if (value.length > size) - throw new ScriptError('Script number overflow.'); + throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.'); if ((flags & constants.flags.VERIFY_MINIMALDATA) && value.length > 0) { // If the low bits on the last byte are unset, @@ -1917,8 +1887,11 @@ Script.num = function num(value, flags, size) { // to avoid negative zero (also avoids positive // zero). if (!(value[value.length - 1] & 0x7f)) { - if (value.length === 1 || !(value[value.length - 2] & 0x80)) - throw new ScriptError('Non-minimally encoded Script number.'); + if (value.length === 1 || !(value[value.length - 2] & 0x80)) { + throw new ScriptError( + 'UNKNOWN_ERROR', + 'Non-minimally encoded Script number.'); + } } } @@ -3232,20 +3205,19 @@ Script.isZero = function isZero(op) { * @param {Buffer} key * @param {VerifyFlags?} flags * @returns {Boolean} + * @throws {ScriptError} */ -Script.isValidKey = function isValidKey(key, flags) { +Script.validateKey = function validateKey(key, flags) { if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; if (!Script.isPush(key)) - return false; + throw new ScriptError('BAD_OPCODE'); if (flags & constants.flags.VERIFY_STRICTENC) { - if (!Script.isKeyEncoding(key)) { - bcoin.debug('Script failed key encoding test.'); - return false; - } + if (!Script.isKeyEncoding(key)) + throw new ScriptError('PUBKEYTYPE'); } return true; @@ -3286,14 +3258,15 @@ Script.isKeyEncoding = function isKeyEncoding(key) { * @param {Buffer} sig * @param {VerifyFlags?} flags * @returns {Boolean} + * @throws {ScriptError} */ -Script.isValidSignature = function isValidSignature(sig, flags) { +Script.validateSignature = function validateSignature(sig, flags) { if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; if (!Script.isPush(sig)) - return false; + throw new ScriptError('BAD_OPCODE'); // Allow empty sigs if (sig.length === 0) @@ -3302,24 +3275,18 @@ Script.isValidSignature = function isValidSignature(sig, flags) { if ((flags & constants.flags.VERIFY_DERSIG) || (flags & constants.flags.VERIFY_LOW_S) || (flags & constants.flags.VERIFY_STRICTENC)) { - if (!Script.isSignatureEncoding(sig)) { - bcoin.debug('Script does not have a proper signature encoding.'); - return false; - } + if (!Script.isSignatureEncoding(sig)) + throw new ScriptError('SIG_DER'); } if (flags & constants.flags.VERIFY_LOW_S) { - if (!Script.isLowDER(sig)) { - bcoin.debug('Script does not have a low DER.'); - return false; - } + if (!Script.isLowDER(sig)) + throw new ScriptError('SIG_HIGH_S'); } if (flags & constants.flags.VERIFY_STRICTENC) { - if (!Script.isHashType(sig)) { - bcoin.debug('Script does not have a valid hash type.'); - return false; - } + if (!Script.isHashType(sig)) + throw new ScriptError('SIG_HASHTYPE'); } return true; @@ -3809,10 +3776,11 @@ Script.fromSymbolic = function fromSymbolic(items) { * @param {Number} i * @param {VerifyFlags} flags * @returns {Boolean} + * @throws {ScriptError} */ Script.verify = function verify(input, witness, output, tx, i, flags) { - var copy, res, raw, redeem, hadWitness; + var copy, raw, redeem, hadWitness; var stack = new Stack(); if (flags == null) @@ -3820,36 +3788,32 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { if (flags & constants.flags.VERIFY_SIGPUSHONLY) { if (!input.isPushOnly()) - return false; + throw new ScriptError('SIG_PUSHONLY'); } // Execute the input script - res = input.execute(stack, flags, tx, i, 0); - - if (!res) - return false; + input.execute(stack, flags, tx, i, 0); // Copy the stack for P2SH if (flags & constants.flags.VERIFY_P2SH) copy = stack.clone(); // Execute the previous output script - res = output.execute(stack, flags, tx, i, 0); + output.execute(stack, flags, tx, i, 0); // Verify the script did not fail as well as the stack values - if (!res || stack.length === 0 || !Script.bool(stack.pop())) - return false; + if (stack.length === 0 || !Script.bool(stack.pop())) + throw new ScriptError('EVAL_FALSE'); if ((flags & constants.flags.VERIFY_WITNESS) && output.isWitnessProgram()) { hadWitness = true; // Input script must be empty. if (input.code.length !== 0) - return false; + throw new ScriptError('WITNESS_MALLEATED'); // Verify the program in the output script - if (!Script.verifyProgram(witness, output, flags, tx, i)) - return false; + Script.verifyProgram(witness, output, flags, tx, i); // Force a cleanstack stack.length = 0; @@ -3859,40 +3823,35 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { if ((flags & constants.flags.VERIFY_P2SH) && output.isScripthash()) { // P2SH can only have push ops in the scriptSig if (!input.isPushOnly()) - return false; + throw new ScriptError('SIG_PUSHONLY'); // Reset the stack stack = copy; // Stack should not be empty at this point if (stack.length === 0) - return false; + throw new ScriptError('EVAL_FALSE'); // Grab the real redeem script raw = stack.pop(); - - if (!Script.isPush(raw)) - return false; - redeem = new Script(raw); // Execute the redeem script - res = redeem.execute(stack, flags, tx, i, 0); + redeem.execute(stack, flags, tx, i, 0); // Verify the script did not fail as well as the stack values - if (!res || stack.length === 0 || !Script.bool(stack.pop())) - return false; + if (stack.length === 0 || !Script.bool(stack.pop())) + throw new ScriptError('EVAL_FALSE'); if ((flags & constants.flags.VERIFY_WITNESS) && redeem.isWitnessProgram()) { hadWitness = true; // Input script must be exactly one push of the redeem script. if (!(input.code.length === 1 && utils.equals(input.code[0], raw))) - return false; + throw new ScriptError('WITNESS_MALLEATED'); // Verify the program in the redeem script - if (!Script.verifyProgram(witness, redeem, flags, tx, i)) - return false; + Script.verifyProgram(witness, redeem, flags, tx, i); // Force a cleanstack stack.length = 0; @@ -3904,14 +3863,14 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { assert((flags & constants.flags.VERIFY_P2SH) !== 0); // assert((flags & constants.flags.VERIFY_WITNESS) !== 0); if (stack.length !== 0) - return false; + throw new ScriptError('CLEANSTACK'); } // If we had a witness but no witness program, fail. if (flags & constants.flags.VERIFY_WITNESS) { assert((flags & constants.flags.VERIFY_P2SH) !== 0); if (!hadWitness && witness.length > 0) - return false; + throw new ScriptError('WITNESS_UNEXPECTED'); } return true; @@ -3926,23 +3885,38 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { * @param {VerifyFlags} flags * @param {TX} tx * @param {Number} i + * @throws {ScriptError} */ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, i) { - var program, witnessScript, redeem, stack, j, res; + var program = output.getWitnessProgram(); + var stack = witness.toStack(); + var witnessScript, redeem, j; + assert(program, 'verifyProgram called on non-witness-program.'); assert((flags & constants.flags.VERIFY_WITNESS) !== 0); - assert(output.isWitnessProgram()); - program = output.getWitnessProgram(); + if (program.version === 0) { + if (program.data.length === 32) { + if (stack.length === 0) + throw new ScriptError('WITNESS_PROGRAM_WITNESS_EMPTY'); - // Failure on version=0 (bad program data length) - if (!program.type) { - bcoin.debug('Malformed witness program.'); - return false; - } + witnessScript = stack.pop(); - if (program.version > 0) { + if (!utils.equals(utils.sha256(witnessScript), program.data)) + throw new ScriptError('WITNESS_PROGRAM_MISMATCH'); + + redeem = new Script(witnessScript); + } else if (program.data.length === 20) { + if (stack.length !== 2) + throw new ScriptError('WITNESS_PROGRAM_MISMATCH'); + + redeem = Script.createPubkeyhash(program.data); + } else { + // Failure on version=0 (bad program data length) + throw new ScriptError('WITNESS_PROGRAM_WRONG_LENGTH'); + } + } else { bcoin.debug('Unknown witness program version: %s', program.version); // Anyone can spend (we can return true here // if we want to always relay these transactions). @@ -3952,45 +3926,24 @@ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, i) { // succeed in a block, but fail in the mempool // due to VERIFY_CLEANSTACK. if (flags & constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) - return false; + throw new ScriptError('DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM'); return true; } - stack = witness.toStack(); - - if (program.type === 'witnesspubkeyhash') { - if (stack.length !== 2) - return false; - - redeem = Script.createPubkeyhash(program.data); - } else if (program.type === 'witnessscripthash') { - if (stack.length === 0) - return false; - - witnessScript = stack.pop(); - - if (!utils.equals(utils.sha256(witnessScript), program.data)) - return false; - - redeem = new Script(witnessScript); - } else { - assert(false); - } - for (j = 0; j < stack.length; j++) { if (stack.get(j).length > constants.script.MAX_PUSH) - return false; + throw new ScriptError('PUSH_SIZE'); } - res = redeem.execute(stack, flags, tx, i, 1); + redeem.execute(stack, flags, tx, i, 1); // Verify the script did not fail as well as the stack values - if (!res || stack.length === 0 || !Script.bool(stack.pop())) - return false; + if (stack.length === 0 || !Script.bool(stack.pop())) + throw new ScriptError('EVAL_FALSE'); // Witnesses always require cleanstack if (stack.length !== 0) - return false; + throw new ScriptError('EVAL_FALSE'); return true; }; @@ -4310,25 +4263,31 @@ function ScriptError(code, op, ip) { this.type = 'ScriptError'; this.code = code; - if (Buffer.isBuffer(op)) - op = 'PUSHDATA[' + op.length + ']'; + if (typeof op !== 'string') { + if (Buffer.isBuffer(op)) + op = 'PUSHDATA[' + op.length + ']'; - if (op || ip != null) { - code += '('; - if (op) { - op = constants.opcodesByVal[op] || op; - code += 'op=' + op; + if (op || ip != null) { + code += ' ('; + if (op) { + op = constants.opcodesByVal[op] || op; + code += 'op=' + op; + if (ip != null) + code += ', '; + } if (ip != null) - code += ', '; + code += 'ip=' + ip; + code += ')'; } - if (ip != null) - code += 'ip=' + ip; - code + ')'; - } - this.message = code; - this.op = op; - this.ip = ip; + this.message = code; + this.op = op || ''; + this.ip = ip != null ? ip : -1; + } else { + this.message = op; + this.op = ''; + this.ip = -1; + } } utils.inherits(ScriptError, Error); diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 0d84638e..e118aac2 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -515,7 +515,7 @@ TX.prototype.signatureHashV1 = function signatureHashV1(index, prev, type) { */ TX.prototype.verify = function verify(index, force, flags) { - var i, input, res; + var i, input; // Valid if included in block if (!force && this.ts !== 0) @@ -544,17 +544,23 @@ TX.prototype.verify = function verify(index, force, flags) { return false; } - res = Script.verify( - input.script, - input.witness, - input.coin.script, - this, - i, - flags - ); - - if (!res) + try { + Script.verify( + input.script, + input.witness, + input.coin.script, + this, + i, + flags + ); + } catch (e) { + if (e.type === 'ScriptError') { + bcoin.debug('Script verification error: %s', e.message); + } else { + bcoin.debug('Script interpreter threw: %s', e.stack + ''); + } return false; + } } return true; @@ -1133,7 +1139,7 @@ TX.prototype.isStandard = function isStandard(flags, ret) { TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { var maxSigops = constants.script.MAX_SCRIPTHASH_SIGOPS; var VERIFY_NONE = constants.flags.VERIFY_NONE; - var i, input, stack, res, redeem; + var i, input, stack, redeem; if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; @@ -1160,10 +1166,11 @@ TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { stack = new Stack(); - res = input.script.execute(stack, VERIFY_NONE, this, i, 0); - - if (!res) + try { + input.script.execute(stack, VERIFY_NONE, this, i, 0); + } catch (e) { return false; + } if (stack.length === 0) return false; diff --git a/test/script-test.js b/test/script-test.js index 4ac330cc..9731186e 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -254,11 +254,20 @@ describe('Script', function() { delete input.raw; delete output.raw; } - var res = Script.verify(input, witness, output, tx, 0, flags); - if (expected === 'OK') - assert.ok(res); - else - assert.ok(!res); + var err, res; + try { + res = Script.verify(input, witness, output, tx, 0, flags); + } catch (e) { + err = e; + } + if (expected !== 'OK') { + assert(!res); + assert(err); + assert.equal(err.code, expected); + return; + } + utils.assert.noError(err); + assert(res); }); }); });