From 4280b993e02c6efaa9ebe3b7fc0932ca51a1af56 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 1 Jul 2015 08:58:34 -0700 Subject: [PATCH] Add CLTV (BIP65) support --- lib/crypto/bn.js | 11 +++- lib/opcode.js | 2 + lib/script/interpreter.js | 101 ++++++++++++++++++++++++++++- lib/transaction/input/input.js | 4 ++ test/data/bitcoind/tx_invalid.json | 9 +++ test/data/bitcoind/tx_valid.json | 42 ++++++++++++ test/opcode.js | 4 +- test/script/interpreter.js | 3 + 8 files changed, 170 insertions(+), 6 deletions(-) diff --git a/lib/crypto/bn.js b/lib/crypto/bn.js index 20b53f8..e88c6da 100644 --- a/lib/crypto/bn.js +++ b/lib/crypto/bn.js @@ -131,10 +131,11 @@ BN.prototype.toSM = function(opts) { * This is analogous to the constructor for CScriptNum in bitcoind. Many ops in * bitcoind's script interpreter use CScriptNum, which is not really a proper * bignum. Instead, an error is thrown if trying to input a number bigger than - * 4 bytes. We copy that behavior here. + * 4 bytes. We copy that behavior here. A third argument, `size`, is provided to + * extend the hard limit of 4 bytes, as some usages require more than 4 bytes. */ -BN.fromScriptNumBuffer = function(buf, fRequireMinimal) { - var nMaxNumSize = 4; +BN.fromScriptNumBuffer = function(buf, fRequireMinimal, size) { + var nMaxNumSize = size || 4; $.checkArgument(buf.length <= nMaxNumSize, new Error('script number overflow')); if (fRequireMinimal && buf.length > 0) { // Check that the number is encoded with the minimum possible @@ -175,6 +176,10 @@ BN.prototype.gt = function(b) { return this.cmp(b) > 0; }; +BN.prototype.gte = function(b) { + return this.cmp(b) >= 0; +}; + BN.prototype.lt = function(b) { return this.cmp(b) < 0; }; diff --git a/lib/opcode.js b/lib/opcode.js index d5e5a93..3218faf 100644 --- a/lib/opcode.js +++ b/lib/opcode.js @@ -195,6 +195,8 @@ Opcode.map = { OP_CHECKMULTISIG: 174, OP_CHECKMULTISIGVERIFY: 175, + OP_CHECKLOCKTIMEVERIFY: 177, + // expansion OP_NOP1: 176, OP_NOP2: 177, diff --git a/lib/script/interpreter.js b/lib/script/interpreter.js index 2226c16..3041c6d 100644 --- a/lib/script/interpreter.js +++ b/lib/script/interpreter.js @@ -184,6 +184,9 @@ Interpreter.false = new Buffer([]); Interpreter.MAX_SCRIPT_ELEMENT_SIZE = 520; +Interpreter.LOCKTIME_THRESHOLD = 500000000; +Interpreter.LOCKTIME_THRESHOLD_BN = new BN(500000000); + // flags taken from bitcoind // bitcoind commit: b5d1b1092998bc95313856d535c632ea5a8f9104 Interpreter.SCRIPT_VERIFY_NONE = 0; @@ -226,6 +229,9 @@ Interpreter.SCRIPT_VERIFY_MINIMALDATA = (1 << 6); // executed, e.g. within an unexecuted IF ENDIF block, are *not* rejected. Interpreter.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS = (1 << 7); +// CLTV See BIP65 for details. +Interpreter.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY = (1 << 9); + Interpreter.castToBool = function(buf) { for (var i = 0; i < buf.length; i++) { if (buf[i] !== 0) { @@ -311,6 +317,52 @@ Interpreter.prototype.evaluate = function() { return true; }; +/** + * Checks a locktime parameter with the transaction's locktime + * + * @param {BN} nLockTime the locktime read from the script + * @return {boolean} true if the transaction's locktime is less than or equal to + * the transaction's locktime + */ +Interpreter.prototype.checkLockTime = function(nLockTime) { + + // There are two times of nLockTime: lock-by-blockheight + // and lock-by-blocktime, distinguished by whether + // nLockTime < LOCKTIME_THRESHOLD. + // + // We want to compare apples to apples, so fail the script + // unless the type of nLockTime being tested is the same as + // the nLockTime in the transaction. + if (!( + (this.tx.nLockTime < Interpreter.LOCKTIME_THRESHOLD && nLockTime.lt(Interpreter.LOCKTIME_THRESHOLD_BN)) || + (this.tx.nLockTime >= Interpreter.LOCKTIME_THRESHOLD && nLockTime.gte(Interpreter.LOCKTIME_THRESHOLD_BN)) + )) { + return false; + } + + // Now that we know we're comparing apples-to-apples, the + // comparison is a simple numeric one. + if (nLockTime.gt(new BN(this.tx.nLockTime))) { + return false; + } + + // Finally the nLockTime feature can be disabled and thus + // CHECKLOCKTIMEVERIFY bypassed if every txin has been + // finalized by setting nSequence to maxint. The + // transaction would be allowed into the blockchain, making + // the opcode ineffective. + // + // Testing if this vin is not final is sufficient to + // prevent this condition. Alternatively we could test all + // inputs, but testing just this input minimizes the data + // required to prove correct CHECKLOCKTIMEVERIFY execution. + if (!this.tx.inputs[this.nin].isFinal()) { + return false; + } + + return true; +} + /** * Based on the inner loop of bitcoind's EvalScript function * bitcoind commit: b5d1b1092998bc95313856d535c632ea5a8f9104 @@ -414,8 +466,55 @@ Interpreter.prototype.step = function() { case Opcode.OP_NOP: break; - case Opcode.OP_NOP1: case Opcode.OP_NOP2: + case Opcode.OP_CHECKLOCKTIMEVERIFY: + + if (!(this.flags & Interpreter.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY)) { + // not enabled; treat as a NOP2 + if (this.flags & Interpreter.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) { + this.errstr = 'SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS'; + return false; + } + break; + } + + if (this.stack.length < 1) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } + + // Note that elsewhere numeric opcodes are limited to + // operands in the range -2**31+1 to 2**31-1, however it is + // legal for opcodes to produce results exceeding that + // range. This limitation is implemented by CScriptNum's + // default 4-byte limit. + // + // If we kept to that limit we'd have a year 2038 problem, + // even though the nLockTime field in transactions + // themselves is uint32 which only becomes meaningless + // after the year 2106. + // + // Thus as a special case we tell CScriptNum to accept up + // to 5-byte bignums, which are good until 2**39-1, well + // beyond the 2**32-1 limit of the nLockTime field itself. + var nLockTime = BN.fromScriptNumBuffer(this.stack[this.stack.length - 1], fRequireMinimal, 5); + + // In the rare event that the argument may be < 0 due to + // some arithmetic being done first, you can always use + // 0 MAX CHECKLOCKTIMEVERIFY. + if (nLockTime.lt(new BN(0))) { + this.errstr = 'SCRIPT_ERR_NEGATIVE_LOCKTIME'; + return false; + } + + // Actually compare the specified lock time with the transaction. + if (!this.checkLockTime(nLockTime)) { + this.errstr = 'SCRIPT_ERR_UNSATISFIED_LOCKTIME'; + return false; + } + break; + + case Opcode.OP_NOP1: case Opcode.OP_NOP3: case Opcode.OP_NOP4: case Opcode.OP_NOP5: diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index 841ed18..afa169e 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -155,6 +155,10 @@ Input.prototype.isFullySigned = function() { throw new errors.AbstractMethodInvoked('Input#isFullySigned'); }; +Input.prototype.isFinal = function() { + return this.sequenceNumber !== 4294967295; +}; + Input.prototype.addSignature = function() { throw new errors.AbstractMethodInvoked('Input#addSignature'); }; diff --git a/test/data/bitcoind/tx_invalid.json b/test/data/bitcoind/tx_invalid.json index 638a705..3f48737 100644 --- a/test/data/bitcoind/tx_invalid.json +++ b/test/data/bitcoind/tx_invalid.json @@ -103,5 +103,14 @@ [[["ad503f72c18df5801ee64d76090afe4c607fb2b822e9b7b63c5826c50e22fc3b", 0, "0x21 0x027c3a97665bf283a102a587a62a30a0c102d4d3b141015e2cae6f64e2543113e5 CHECKSIG NOT"]], "01000000013bfc220ec526583cb6b7e922b8b27f604cfe0a09764de61e80f58dc1723f50ad0000000000ffffffff0101000000000000002321027c3a97665bf283a102a587a62a30a0c102d4d3b141015e2cae6f64e2543113e5ac00000000", "P2SH"], + +["Failure due to failing CHECKLOCKTIMEVERIFY in scriptSig"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "1"]], +"01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000000000000", "P2SH,CHECKLOCKTIMEVERIFY"], + +["Failure due to failing CHECKLOCKTIMEVERIFY in redeemScript"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "HASH160 0x14 0xc5b93064159b3b2d6ab506a41b1f50463771b988 EQUAL"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000000000000", "P2SH,CHECKLOCKTIMEVERIFY"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/test/data/bitcoind/tx_valid.json b/test/data/bitcoind/tx_valid.json index aa8e5ca..438ca9c 100644 --- a/test/data/bitcoind/tx_valid.json +++ b/test/data/bitcoind/tx_valid.json @@ -177,6 +177,48 @@ ["ceafe58e0f6e7d67c0409fbbf673c84c166e3c5d3c24af58f7175b18df3bb3db", 1, "2 0x48 0x3045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 3 CHECKMULTISIG"]], "0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000", "P2SH"], +["CHECKLOCKTIMEVERIFY tests"], + +["By-height locks, with argument == 0 and == tx nLockTime"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP2 1"]], +"010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKLOCKTIMEVERIFY"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "499999999 NOP2 1"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d", "P2SH,CHECKLOCKTIMEVERIFY"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP2 1"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d", "P2SH,CHECKLOCKTIMEVERIFY"], + +["By-time locks, with argument just beyond tx nLockTime (but within numerical boundries)"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "500000000 NOP2 1"]], +"01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", "P2SH,CHECKLOCKTIMEVERIFY"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP2 1"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff", "P2SH,CHECKLOCKTIMEVERIFY"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "500000000 NOP2 1"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff", "P2SH,CHECKLOCKTIMEVERIFY"], + +["Any non-maxint nSequence is fine"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP2 1"]], +"010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", "P2SH,CHECKLOCKTIMEVERIFY"], + +["The argument can be calculated rather than created directly by a PUSHDATA"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "499999999 1ADD NOP2 1"]], +"01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", "P2SH,CHECKLOCKTIMEVERIFY"], + +["Perhaps even by an ADD producing a 5-byte result that is out of bounds for other opcodes"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483647 2147483647 ADD NOP2 1"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff", "P2SH,CHECKLOCKTIMEVERIFY"], + +["5 byte non-minimally-encoded arguments are valid"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x05 0x0000000000 NOP2 1"]], +"010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKLOCKTIMEVERIFY"], + +["Valid CHECKLOCKTIMEVERIFY in scriptSig"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "1"]], +"01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000", "P2SH,CHECKLOCKTIMEVERIFY"], + +["Valid CHECKLOCKTIMEVERIFY in redeemScript"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "HASH160 0x14 0xc5b93064159b3b2d6ab506a41b1f50463771b988 EQUAL"]], +"0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000", "P2SH,CHECKLOCKTIMEVERIFY"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/test/opcode.js b/test/opcode.js index c060989..6471e13 100644 --- a/test/opcode.js +++ b/test/opcode.js @@ -85,8 +85,8 @@ describe('Opcode', function() { }); describe('@map', function() { - it('should have a map containing 116 elements', function() { - _.size(Opcode.map).should.equal(116); + it('should have a map containing 117 elements', function() { + _.size(Opcode.map).should.equal(117); }); }); diff --git a/test/script/interpreter.js b/test/script/interpreter.js index e5712d6..ea7a328 100644 --- a/test/script/interpreter.js +++ b/test/script/interpreter.js @@ -182,6 +182,9 @@ describe('Interpreter', function() { if (flagstr.indexOf('DISCOURAGE_UPGRADABLE_NOPS') !== -1) { flags = flags | Interpreter.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS; } + if (flagstr.indexOf('CHECKLOCKTIMEVERIFY') !== -1) { + flags = flags | Interpreter.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY; + } return flags; };