From c888c3baa703bc98ff728378e601f79c8a7f5399 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Fri, 12 Dec 2014 19:21:37 -0300 Subject: [PATCH] adding some signature methods for script interpreting --- lib/crypto/signature.js | 122 +++++++++++++++++++++++++++++++++++-- lib/script.js | 67 ++++++++++++++++++-- lib/script_interpreter.js | 22 ++++--- test/crypto/signature.js | 35 ++++++++++- test/script.js | 16 +++++ test/script_interpreter.js | 1 + 6 files changed, 242 insertions(+), 21 deletions(-) diff --git a/lib/crypto/signature.js b/lib/crypto/signature.js index e18267e..142bd06 100644 --- a/lib/crypto/signature.js +++ b/lib/crypto/signature.js @@ -10,8 +10,7 @@ var Signature = function Signature(r, s) { r: r, s: s }); - } - else if (r) { + } else if (r) { var obj = r; this.set(obj); } @@ -132,13 +131,17 @@ Signature.prototype.toCompact = function(i, compressed) { if (!(i === 0 || i === 1 || i === 2 || i === 3)) throw new Error('i must be equal to 0, 1, 2, or 3'); - + var val = i + 27 + 4; if (compressed === false) val = val - 4; var b1 = new Buffer([val]); - var b2 = this.r.toBuffer({size: 32}); - var b3 = this.s.toBuffer({size: 32}); + var b2 = this.r.toBuffer({ + size: 32 + }); + var b3 = this.s.toBuffer({ + size: 32 + }); return Buffer.concat([b1, b2, b3]); }; @@ -168,6 +171,115 @@ Signature.prototype.toString = function() { return buf.toString('hex'); }; +/** + * This function is translated from bitcoind's IsDERSignature and is used in + * the script interpreter. This "DER" format actually includes an extra byte, + * the nhashtype, at the end. It is really the tx format, not DER format. + * + * A canonical signature exists of: [30] [total len] [02] [len R] [R] [02] [len S] [S] [hashtype] + * Where R and S are not negative (their first byte has its highest bit not set), and not + * excessively padded (do not start with a 0 byte, unless an otherwise negative number follows, + * in which case a single 0 byte is necessary and even required). + * + * See https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623 + */ +Signature.isTxDER = function(buf) { + if (buf.length < 9) { + // Non-canonical signature: too short + return false; + } + if (buf.length > 73) { + // Non-canonical signature: too long + return false; + } + if (buf[0] !== 0x30) { + // Non-canonical signature: wrong type + return false; + } + if (buf[1] !== buf.length - 3) { + // Non-canonical signature: wrong length marker + return false; + } + var nLenR = buf[3]; + if (5 + nLenR >= buf.length) { + // Non-canonical signature: S length misplaced + return false; + } + var nLenS = buf[5 + nLenR]; + if ((nLenR + nLenS + 7) !== buf.length) { + // Non-canonical signature: R+S length mismatch + return false; + } + + var R = buf.slice(4); + if (buf[4 - 2] !== 0x02) { + // Non-canonical signature: R value type mismatch + return false; + } + if (nLenR === 0) { + // Non-canonical signature: R length is zero + return false; + } + if (R[0] & 0x80) { + // Non-canonical signature: R value negative + return false; + } + if (nLenR > 1 && (R[0] === 0x00) && !(R[1] & 0x80)) { + // Non-canonical signature: R value excessively padded + return false; + } + + var S = buf.slice(6 + nLenR); + if (buf[6 + nLenR - 2] !== 0x02) { + // Non-canonical signature: S value type mismatch + return false; + } + if (nLenS === 0) { + // Non-canonical signature: S length is zero + return false; + } + if (S[0] & 0x80) { + // Non-canonical signature: S value negative + return false; + } + if (nLenS > 1 && (S[0] === 0x00) && !(S[1] & 0x80)) { + // Non-canonical signature: S value excessively padded + return false; + } + return true; +}; + +/** + * Compares to bitcoind's IsLowDERSignature + * See also ECDSA signature algorithm which enforces this. + * See also BIP 62, "low S values in signatures" + */ +Signature.prototype.hasLowS = function() { + if (this.s.lt(1) || + this.s.gt(BN('7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0'))) { + return false; + } + return true; +}; + +/** + * @returns true if the nhashtype is exactly equal to one of the standard options or combinations thereof. + * Translated from bitcoind's IsDefinedHashtypeSignature + */ +Signature.prototype.hasDefinedHashtype = function() { + if (this.nhashtype < Signature.SIGHASH_ALL || this.nhashtype > Signature.SIGHASH_SINGLE) { + return false; + } + return true; +}; + +Signature.prototype.toTxFormat = function() { + var derbuf = this.toDER(); + var buf = new Buffer(1); + buf.writeUInt8(this.nhashtype, 0); + return Buffer.concat([derbuf, buf]); +}; + Signature.SIGHASH_ALL = 0x01; Signature.SIGHASH_NONE = 0x02; Signature.SIGHASH_SINGLE = 0x03; diff --git a/lib/script.js b/lib/script.js index 6403e43..4b374eb 100644 --- a/lib/script.js +++ b/lib/script.js @@ -264,11 +264,11 @@ Script.prototype.isPublicKeyIn = function() { * @returns true if this is a p2sh output script */ Script.prototype.isScriptHashOut = function() { - return this.chunks.length === 3 && - this.chunks[0].opcodenum === Opcode.OP_HASH160 && - this.chunks[1].buf && - this.chunks[1].buf.length === 20 && - this.chunks[2].opcodenum === Opcode.OP_EQUAL; + var buf = this.toBuffer(); + return (buf.length === 23 && + buf[0] === Opcode.OP_HASH160 && + buf[1] === 0x14 && + buf[22] === Opcode.OP_EQUAL); }; /** @@ -669,4 +669,61 @@ Script.fromAddress = function(address) { throw new errors.Script.UnrecognizedAddress(address); }; +/** + * Analagous to bitcoind's FindAndDelete. Find and delete equivalent chunks, + * typically used with push data chunks. Note that this will find and delete + * not just the same data, but the same data with the same push data op as + * produced by default. i.e., if a pushdata in a tx does not use the minimal + * pushdata op, then when you try to remove the data it is pushing, it will not + * be removed, because they do not use the same pushdata op. + */ +Script.prototype.findAndDelete = function(script) { + var buf = script.toBuffer(); + var hex = buf.toString('hex'); + for (var i = 0; i < this.chunks.length; i++) { + var script2 = Script({ + chunks: [this.chunks[i]] + }); + var buf2 = script2.toBuffer(); + var hex2 = buf2.toString('hex'); + if (hex === hex2) { + this.chunks.splice(i, 1); + } + } + return this; +}; + +/** + * @returns true if the chunk {i} is the smallest way to push that particular data. + * Comes from bitcoind's script interpreter CheckMinimalPush function + */ +Script.prototype.checkMinimalPush = function(i) { + var chunk = this.chunks[i]; + var buf = chunk.buf; + var opcodenum = chunk.opcodenum; + if (!buf) { + return true; + } + if (buf.length === 0) { + // Could have used OP_0. + return opcodenum === Opcode.OP_0; + } else if (buf.length === 1 && buf[0] >= 1 && buf[0] <= 16) { + // Could have used OP_1 .. OP_16. + return opcodenum === Opcode.OP_1 + (buf[0] - 1); + } else if (buf.length === 1 && buf[0] === 0x81) { + // Could have used OP_1NEGATE + return opcodenum === Opcode.OP_1NEGATE; + } else if (buf.length <= 75) { + // Could have used a direct push (opcode indicating number of bytes pushed + those bytes). + return opcodenum === buf.length; + } else if (buf.length <= 255) { + // Could have used OP_PUSHDATA. + return opcodenum === Opcode.OP_PUSHDATA1; + } else if (buf.length <= 65535) { + // Could have used OP_PUSHDATA2. + return opcodenum === Opcode.OP_PUSHDATA2; + } + return true; +}; + module.exports = Script; diff --git a/lib/script_interpreter.js b/lib/script_interpreter.js index 0868593..db2ede8 100644 --- a/lib/script_interpreter.js +++ b/lib/script_interpreter.js @@ -210,7 +210,7 @@ ScriptInterpreter.prototype.step = function() { this.pc++; var opcodenum = chunk.opcodenum; if (_.isUndefined(opcodenum)) { - this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; + this.errstr = 'SCRIPT_ERR_UNDEFINED_OPCODE'; return false; } if (chunk.buf && chunk.buf.length > ScriptInterpreter.MAX_SCRIPT_ELEMENT_SIZE) { @@ -257,7 +257,6 @@ ScriptInterpreter.prototype.step = function() { this.stack.push(chunk.buf); } } else if (fExec || (Opcode.OP_IF <= opcodenum && opcodenum <= Opcode.OP_ENDIF)) { - console.log('STEP!' + JSON.stringify(chunk)); switch (opcodenum) { // Push value case Opcode.OP_1NEGATE: @@ -902,9 +901,9 @@ ScriptInterpreter.prototype.step = function() { // stack.push_back(fSuccess ? vchTrue : vchFalse); this.stack.push(fSuccess ? ScriptInterpreter.true : ScriptInterpreter.false); if (opcodenum === Opcode.OP_CHECKSIGVERIFY) { - if (fSuccess) + if (fSuccess) { this.stack.pop(); - else { + } else { this.errstr = 'SCRIPT_ERR_CHECKSIGVERIFY'; return false; } @@ -962,7 +961,7 @@ ScriptInterpreter.prototype.step = function() { // Drop the signatures, since there's no way for a signature to sign itself for (var k = 0; k < nSigsCount; k++) { var bufSig = this.stack[this.stack.length - isig - k]; - subscript.findAndDelete(Script().writeBuffer(bufSig)); + subscript.findAndDelete(Script().add(bufSig)); } var fSuccess = true; @@ -1064,8 +1063,10 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, return false; } - if (!this.evaluate()) + // evaluate scriptSig + if (!this.evaluate()) { return false; + } if (flags & ScriptInterpreter.SCRIPT_VERIFY_P2SH) var stackCopy = this.stack.slice(); @@ -1080,11 +1081,11 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, flags: flags }); + // evaluate scriptPubkey if (!this.evaluate()) return false; if (this.stack.length === 0) { - console.log('stack 0'); this.errstr = 'SCRIPT_ERR_EVAL_FALSE'; return false; } @@ -1109,19 +1110,20 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin, if (stackCopy.length === 0) throw new Error('internal error - stack copy empty'); - var pubkeySerialized = stackCopy[stackCopy.length - 1]; - var scriptPubkey2 = Script.fromBuffer(pubkeySerialized); + var redeemScriptSerialized = stackCopy[stackCopy.length - 1]; + var redeemScript = Script.fromBuffer(redeemScriptSerialized); stackCopy.pop(); this.initialize(); this.set({ - script: scriptPubkey2, + script: redeemScript, stack: stackCopy, tx: tx, nin: nin, flags: flags }); + // evaluate redeemScript if (!this.evaluate()) // serror is set return false; diff --git a/test/crypto/signature.js b/test/crypto/signature.js index 4774082..43b3f31 100644 --- a/test/crypto/signature.js +++ b/test/crypto/signature.js @@ -151,7 +151,6 @@ describe('Signature', function() { }); describe('#toString', function() { - it('should convert this signature in to hex DER', function() { var r = BN('63173831029936981022572627018246571655303050627048489594159321588908385378810'); var s = BN('4331694221846364448463828256391194279133231453999942381442030409253074198130'); @@ -162,7 +161,41 @@ describe('Signature', function() { var hex = sig.toString(); hex.should.equal('30450221008bab1f0a2ff2f9cb8992173d8ad73c229d31ea8e10b0f4d4ae1a0d8ed76021fa02200993a6ec81755b9111762fc2cf8e3ede73047515622792110867d12654275e72'); }); + }); + + describe('@isTxDER', function() { + it('should know this is a DER signature', function() { + var sighex = '3042021e17cfe77536c3fb0526bd1a72d7a8e0973f463add210be14063c8a9c37632022061bfa677f825ded82ba0863fb0c46ca1388dd3e647f6a93c038168b59d131a5101'; + var sigbuf = new Buffer(sighex, 'hex'); + Signature.isTxDER(sigbuf).should.equal(true); + }); + + it('should know this is not a DER signature', function() { + //for more extensive tests, see the script interpreter + var sighex = '3042021e17cfe77536c3fb0526bd1a72d7a8e0973f463add210be14063c8a9c37632022061bfa677f825ded82ba0863fb0c46ca1388dd3e647f6a93c038168b59d131a5101'; + var sigbuf = new Buffer(sighex, 'hex'); + sigbuf[0] = 0x31; + Signature.isTxDER(sigbuf).should.equal(false); + }); + }); + describe('#hasLowS', function() { + it('should detect high and low S', function() { + var r = BN('63173831029936981022572627018246571655303050627048489594159321588908385378810'); + var s = BN('4331694221846364448463828256391194279133231453999942381442030409253074198130'); + var s2 = BN('7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B2000'); + var sig = new Signature({ + r: r, + s: s + }); + var sig2 = new Signature({ + r: r, + s: s2 + }); + sig2.hasLowS().should.equal(true); + sig.hasLowS().should.equal(false); + + }); }); }); diff --git a/test/script.js b/test/script.js index 8a6ae26..bf739f2 100644 --- a/test/script.js +++ b/test/script.js @@ -524,4 +524,20 @@ describe('Script', function() { }); }); + + describe('#findAndDelete', function() { + it('should find and delete this buffer', function() { + Script('OP_RETURN 2 0xf0f0') + .findAndDelete(Script('2 0xf0f0')) + .toString() + .should.equal('OP_RETURN'); + }); + it('should do nothing', function() { + Script('OP_RETURN 2 0xf0f0') + .findAndDelete(Script('2 0xffff')) + .toString() + .should.equal('OP_RETURN 2 0xf0f0'); + }); + }); + }); diff --git a/test/script_interpreter.js b/test/script_interpreter.js index 73d5dcf..e3a72bd 100644 --- a/test/script_interpreter.js +++ b/test/script_interpreter.js @@ -243,6 +243,7 @@ describe('ScriptInterpreter', function() { var spendtx = Transaction(); var interp = ScriptInterpreter(); + console.log(scriptSig.toString() + ' ' + scriptPubkey.toString()); var verified = interp.verify(scriptSig, scriptPubkey, spendtx, 0, flags); console.log(interp.errstr); verified.should.equal(true);