diff --git a/lib/script/interpreter.js b/lib/script/interpreter.js index 4c0d79954..97116486d 100644 --- a/lib/script/interpreter.js +++ b/lib/script/interpreter.js @@ -9,6 +9,7 @@ var Hash = require('../crypto/hash'); var Signature = require('../crypto/signature'); var PublicKey = require('../publickey'); + /** * Bitcoin transactions contain scripts. Each input has a script called the * scriptSig, and each output has a script called the scriptPubkey. To validate @@ -402,6 +403,92 @@ Interpreter.prototype.checkPubkeyEncoding = function(buf) { return true; }; + + +/** + * + * Check the buffer is minimally encoded (see https://github.com/bitcoincashorg/spec/blob/master/may-2018-reenabled-opcodes.md#op_bin2num) + * + * + */ + +Interpreter._isMinimallyEncoded = function(buf) { + if (buf.length > Interpreter.MAX_SCRIPT_ELEMENT_SIZE ) { + return false; + } + + if (buf.length > 0) { + // Check that the number is encoded with the minimum possible number + // of bytes. + // + // If the most-significant-byte - excluding the sign bit - is zero + // then we're not minimal. Note how this test also rejects the + // negative-zero encoding, 0x80. + if ((buf[buf.length-1] & 0x7f) == 0) { + // One exception: if there's more than one byte and the most + // significant bit of the second-most-significant-byte is set it + // would conflict with the sign bit. An example of this case is + // +-255, which encode to 0xff00 and 0xff80 respectively. + // (big-endian). + if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) == 0) { + return false; + } + } + } + return true; +}; + +/** + * + * minimally encode the buffer content + * + * @param {number} nMaxNumSize (max allowed size) + */ +Interpreter._minimallyEncode = function(buf) { + if (buf.length == 0) { + return buf; + } + + // If the last byte is not 0x00 or 0x80, we are minimally encoded. + var last = buf[buf.length - 1]; + if (last & 0x7f) { + return buf; + } + + // If the script is one byte long, then we have a zero, which encodes as an + // empty array. + if (buf.length == 1) { + return Buffer.from(''); + } + + // If the next byte has it sign bit set, then we are minimaly encoded. + if (buf[buf.length - 2] & 0x80) { + return buf; + } + + // We are not minimally encoded, we need to figure out how much to trim. + for (var i = buf.length - 1; i > 0; i--) { + // We found a non zero byte, time to encode. + if (buf[i - 1] != 0) { + if (buf[i - 1] & 0x80) { + // We found a byte with it sign bit set so we need one more + // byte. + buf[i++] = last; + } else { + // the sign bit is clear, we can use it. + buf[i - 1] |= last; + } + + return buf.slice(0,i); + } + } + + // If we the whole thing is zeros, then we have a zero. + return Buffer.from(''); +} + + + /** * Based on bitcoind's EvalScript function, with the inner loop moved to * Interpreter.prototype.step() @@ -427,6 +514,7 @@ Interpreter.prototype.evaluate = function() { return false; } } catch (e) { + this.errstr = 'SCRIPT_ERR_UNKNOWN_ERROR: ' + e; return false; } @@ -567,8 +655,6 @@ Interpreter.prototype.step = function() { case Opcode.OP_CAT: case Opcode.OP_SPLIT: - case Opcode.OP_NUM2BIN: - case Opcode.OP_BIN2NUM: case Opcode.OP_DIV: case Opcode.OP_MOD: @@ -578,6 +664,8 @@ Interpreter.prototype.step = function() { case Opcode.OP_AND: case Opcode.OP_OR: case Opcode.OP_XOR: + case Opcode.OP_BIN2NUM: + case Opcode.OP_NUM2BIN: // Opcodes that have been reenabled. if ((self.flags & Interpreter.SCRIPT_ENABLE_MONOLITH_OPCODES) == 0) { return true; @@ -1592,6 +1680,88 @@ Interpreter.prototype.step = function() { } break; + // + // Conversion operations + // + case Opcode.OP_NUM2BIN: { + + // (in -- out) + if (this.stack.length < 2) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } + + var size = BN.fromScriptNumBuffer(stacktop(-1), fRequireMinimal).toNumber(); + if (size > Interpreter.MAX_SCRIPT_ELEMENT_SIZE) { + this.errstr = 'SCRIPT_ERR_PUSH_SIZE'; + return false; + } + + this.stack.pop(); + var rawnum = stacktop(-1); + + // Try to see if we can fit that number in the number of + // byte requested. + rawnum=Interpreter._minimallyEncode(rawnum); + + if (rawnum.length > size) { + // We definitively cannot. + this.errstr = 'SCRIPT_ERR_IMPOSSIBLE_ENCODING'; + return false; + } + + // We already have an element of the right size, we + // don't need to do anything. + if (rawnum.length == size) { + this.stack[this.stack.length-1] = rawnum; + break; + } + + var signbit = 0x00; + if (rawnum.length > 0) { + signbit = rawnum[rawnum.length - 1] & 0x80; + rawnum[rawnum.length - 1] &= 0x7f; + } + + var num = Buffer.alloc(size); + rawnum.copy(num,0); + + var l = rawnum.length - 1; + while (l++ < size - 2) { + num[l]=0x00; + } + + num[l]=signbit; + + this.stack[this.stack.length-1] = num; + } + break; + + + + case Opcode.OP_BIN2NUM: { + // (in -- out) + if (this.stack.length < 1) { + this.errstr = 'SCRIPT_ERR_INVALID_STACK_OPERATION'; + return false; + } + + buf1 = stacktop(-1); + buf2 = Interpreter._minimallyEncode(buf1); + + this.stack[this.stack.length - 1] = buf2; + + // The resulting number must be a valid number. + if (!Interpreter._isMinimallyEncoded(buf2)) { + this.errstr = 'SCRIPT_ERR_INVALID_NUMBER_RANGE'; + return false; + } + } + break; + + + + default: this.errstr = 'SCRIPT_ERR_BAD_OPCODE'; return false; diff --git a/lib/script/script.js b/lib/script/script.js index ad6b62a7d..966039ec5 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -689,6 +689,7 @@ Script.prototype._addBuffer = function(buf, prepend) { return this; }; + Script.prototype.removeCodeseparators = function() { var chunks = []; for (var i = 0; i < this.chunks.length; i++) { diff --git a/lib/util/buffer.js b/lib/util/buffer.js index 3c126199c..852801bb1 100644 --- a/lib/util/buffer.js +++ b/lib/util/buffer.js @@ -170,7 +170,7 @@ module.exports = { hexToBuffer: function hexToBuffer(string) { assert(js.isHexa(string)); return new buffer.Buffer(string, 'hex'); - } + }, }; module.exports.NULL_HASH = module.exports.fill(Buffer.alloc(32), 0); diff --git a/test/script/interpreter.js b/test/script/interpreter.js index 9b056ca53..4f34af98f 100644 --- a/test/script/interpreter.js +++ b/test/script/interpreter.js @@ -285,7 +285,7 @@ describe('Interpreter', function() { // Temporary, until all reenable opcodes are implemented // - var pendingToImplement = ['NUM2BIN', 'BIN2NUM', 'CAT','SPLIT', 'DIV', 'MOD']; + var pendingToImplement = ['CAT','SPLIT', 'DIV', 'MOD']; function isPendingToImplement(str) { for(var i in pendingToImplement) { if (fullScriptString.indexOf(pendingToImplement[i])!=-1) {