adding some signature methods for script interpreting

This commit is contained in:
Manuel Araoz 2014-12-12 19:21:37 -03:00
parent 3de71f8558
commit c888c3baa7
6 changed files with 242 additions and 21 deletions

View File

@ -10,8 +10,7 @@ var Signature = function Signature(r, s) {
r: r, r: r,
s: s s: s
}); });
} } else if (r) {
else if (r) {
var obj = r; var obj = r;
this.set(obj); this.set(obj);
} }
@ -132,13 +131,17 @@ Signature.prototype.toCompact = function(i, compressed) {
if (!(i === 0 || i === 1 || i === 2 || i === 3)) if (!(i === 0 || i === 1 || i === 2 || i === 3))
throw new Error('i must be equal to 0, 1, 2, or 3'); throw new Error('i must be equal to 0, 1, 2, or 3');
var val = i + 27 + 4; var val = i + 27 + 4;
if (compressed === false) if (compressed === false)
val = val - 4; val = val - 4;
var b1 = new Buffer([val]); var b1 = new Buffer([val]);
var b2 = this.r.toBuffer({size: 32}); var b2 = this.r.toBuffer({
var b3 = this.s.toBuffer({size: 32}); size: 32
});
var b3 = this.s.toBuffer({
size: 32
});
return Buffer.concat([b1, b2, b3]); return Buffer.concat([b1, b2, b3]);
}; };
@ -168,6 +171,115 @@ Signature.prototype.toString = function() {
return buf.toString('hex'); 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_ALL = 0x01;
Signature.SIGHASH_NONE = 0x02; Signature.SIGHASH_NONE = 0x02;
Signature.SIGHASH_SINGLE = 0x03; Signature.SIGHASH_SINGLE = 0x03;

View File

@ -264,11 +264,11 @@ Script.prototype.isPublicKeyIn = function() {
* @returns true if this is a p2sh output script * @returns true if this is a p2sh output script
*/ */
Script.prototype.isScriptHashOut = function() { Script.prototype.isScriptHashOut = function() {
return this.chunks.length === 3 && var buf = this.toBuffer();
this.chunks[0].opcodenum === Opcode.OP_HASH160 && return (buf.length === 23 &&
this.chunks[1].buf && buf[0] === Opcode.OP_HASH160 &&
this.chunks[1].buf.length === 20 && buf[1] === 0x14 &&
this.chunks[2].opcodenum === Opcode.OP_EQUAL; buf[22] === Opcode.OP_EQUAL);
}; };
/** /**
@ -669,4 +669,61 @@ Script.fromAddress = function(address) {
throw new errors.Script.UnrecognizedAddress(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; module.exports = Script;

View File

@ -210,7 +210,7 @@ ScriptInterpreter.prototype.step = function() {
this.pc++; this.pc++;
var opcodenum = chunk.opcodenum; var opcodenum = chunk.opcodenum;
if (_.isUndefined(opcodenum)) { if (_.isUndefined(opcodenum)) {
this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; this.errstr = 'SCRIPT_ERR_UNDEFINED_OPCODE';
return false; return false;
} }
if (chunk.buf && chunk.buf.length > ScriptInterpreter.MAX_SCRIPT_ELEMENT_SIZE) { if (chunk.buf && chunk.buf.length > ScriptInterpreter.MAX_SCRIPT_ELEMENT_SIZE) {
@ -257,7 +257,6 @@ ScriptInterpreter.prototype.step = function() {
this.stack.push(chunk.buf); this.stack.push(chunk.buf);
} }
} else if (fExec || (Opcode.OP_IF <= opcodenum && opcodenum <= Opcode.OP_ENDIF)) { } else if (fExec || (Opcode.OP_IF <= opcodenum && opcodenum <= Opcode.OP_ENDIF)) {
console.log('STEP!' + JSON.stringify(chunk));
switch (opcodenum) { switch (opcodenum) {
// Push value // Push value
case Opcode.OP_1NEGATE: case Opcode.OP_1NEGATE:
@ -902,9 +901,9 @@ ScriptInterpreter.prototype.step = function() {
// stack.push_back(fSuccess ? vchTrue : vchFalse); // stack.push_back(fSuccess ? vchTrue : vchFalse);
this.stack.push(fSuccess ? ScriptInterpreter.true : ScriptInterpreter.false); this.stack.push(fSuccess ? ScriptInterpreter.true : ScriptInterpreter.false);
if (opcodenum === Opcode.OP_CHECKSIGVERIFY) { if (opcodenum === Opcode.OP_CHECKSIGVERIFY) {
if (fSuccess) if (fSuccess) {
this.stack.pop(); this.stack.pop();
else { } else {
this.errstr = 'SCRIPT_ERR_CHECKSIGVERIFY'; this.errstr = 'SCRIPT_ERR_CHECKSIGVERIFY';
return false; return false;
} }
@ -962,7 +961,7 @@ ScriptInterpreter.prototype.step = function() {
// Drop the signatures, since there's no way for a signature to sign itself // Drop the signatures, since there's no way for a signature to sign itself
for (var k = 0; k < nSigsCount; k++) { for (var k = 0; k < nSigsCount; k++) {
var bufSig = this.stack[this.stack.length - isig - k]; var bufSig = this.stack[this.stack.length - isig - k];
subscript.findAndDelete(Script().writeBuffer(bufSig)); subscript.findAndDelete(Script().add(bufSig));
} }
var fSuccess = true; var fSuccess = true;
@ -1064,8 +1063,10 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin,
return false; return false;
} }
if (!this.evaluate()) // evaluate scriptSig
if (!this.evaluate()) {
return false; return false;
}
if (flags & ScriptInterpreter.SCRIPT_VERIFY_P2SH) if (flags & ScriptInterpreter.SCRIPT_VERIFY_P2SH)
var stackCopy = this.stack.slice(); var stackCopy = this.stack.slice();
@ -1080,11 +1081,11 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin,
flags: flags flags: flags
}); });
// evaluate scriptPubkey
if (!this.evaluate()) if (!this.evaluate())
return false; return false;
if (this.stack.length === 0) { if (this.stack.length === 0) {
console.log('stack 0');
this.errstr = 'SCRIPT_ERR_EVAL_FALSE'; this.errstr = 'SCRIPT_ERR_EVAL_FALSE';
return false; return false;
} }
@ -1109,19 +1110,20 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin,
if (stackCopy.length === 0) if (stackCopy.length === 0)
throw new Error('internal error - stack copy empty'); throw new Error('internal error - stack copy empty');
var pubkeySerialized = stackCopy[stackCopy.length - 1]; var redeemScriptSerialized = stackCopy[stackCopy.length - 1];
var scriptPubkey2 = Script.fromBuffer(pubkeySerialized); var redeemScript = Script.fromBuffer(redeemScriptSerialized);
stackCopy.pop(); stackCopy.pop();
this.initialize(); this.initialize();
this.set({ this.set({
script: scriptPubkey2, script: redeemScript,
stack: stackCopy, stack: stackCopy,
tx: tx, tx: tx,
nin: nin, nin: nin,
flags: flags flags: flags
}); });
// evaluate redeemScript
if (!this.evaluate()) if (!this.evaluate())
// serror is set // serror is set
return false; return false;

View File

@ -151,7 +151,6 @@ describe('Signature', function() {
}); });
describe('#toString', function() { describe('#toString', function() {
it('should convert this signature in to hex DER', function() { it('should convert this signature in to hex DER', function() {
var r = BN('63173831029936981022572627018246571655303050627048489594159321588908385378810'); var r = BN('63173831029936981022572627018246571655303050627048489594159321588908385378810');
var s = BN('4331694221846364448463828256391194279133231453999942381442030409253074198130'); var s = BN('4331694221846364448463828256391194279133231453999942381442030409253074198130');
@ -162,7 +161,41 @@ describe('Signature', function() {
var hex = sig.toString(); var hex = sig.toString();
hex.should.equal('30450221008bab1f0a2ff2f9cb8992173d8ad73c229d31ea8e10b0f4d4ae1a0d8ed76021fa02200993a6ec81755b9111762fc2cf8e3ede73047515622792110867d12654275e72'); 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);
});
}); });
}); });

View File

@ -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');
});
});
}); });

View File

@ -243,6 +243,7 @@ describe('ScriptInterpreter', function() {
var spendtx = Transaction(); var spendtx = Transaction();
var interp = ScriptInterpreter(); var interp = ScriptInterpreter();
console.log(scriptSig.toString() + ' ' + scriptPubkey.toString());
var verified = interp.verify(scriptSig, scriptPubkey, spendtx, 0, flags); var verified = interp.verify(scriptSig, scriptPubkey, spendtx, 0, flags);
console.log(interp.errstr); console.log(interp.errstr);
verified.should.equal(true); verified.should.equal(true);