From 772025a4a1835ac1a95e4bf2fcc2cd596c3101b9 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 16 Aug 2017 15:33:44 -0700 Subject: [PATCH] script: a better scriptnum implementation. --- lib/script/scriptnum.js | 466 +++++++++++----------------------------- 1 file changed, 130 insertions(+), 336 deletions(-) diff --git a/lib/script/scriptnum.js b/lib/script/scriptnum.js index 2498d4cc..33415186 100644 --- a/lib/script/scriptnum.js +++ b/lib/script/scriptnum.js @@ -7,385 +7,179 @@ 'use strict'; const assert = require('assert'); -const ScriptError = require('./common').ScriptError; +const {I64} = require('../utils/int64'); +const {ScriptError} = require('./common'); const EMPTY_ARRAY = Buffer.alloc(0); -/** - * ScriptNum - * @alias module:script.ScriptNum - * @constructor - * @ignore - * @param {Number} value - */ - -function ScriptNum(value) { +function ScriptNum(num, base, limit) { if (!(this instanceof ScriptNum)) - return new ScriptNum(value); + return new ScriptNum(num, base, limit); - assert(!value || value <= 0xffffffffffff, 'Number exceeds 2^48-1.'); - - this.value = value || 0; + I64.call(this, num, base, limit); } -ScriptNum.prototype.clone = function clone() { - return new ScriptNum(this.value); +Object.setPrototypeOf(ScriptNum, I64); +Object.setPrototypeOf(ScriptNum.prototype, I64.prototype); + +ScriptNum.prototype.toInt = function toInt() { + if (this.lt(I64.UINT32_MIN)) + return I64.LONG_MIN; + + if (this.gt(I64.UINT32_MAX)) + return I64.LONG_MAX; + + return this.lo; }; -ScriptNum.prototype.add = function add(num) { - return this.clone().iadd(num); -}; +ScriptNum.prototype.toRaw = function toRaw() { + let num = this; -ScriptNum.prototype.sub = function sub(num) { - return this.clone().isub(num); -}; + // Zeroes are always empty arrays. + if (num.isZero()) + return EMPTY_ARRAY; -ScriptNum.prototype.mul = function mul(num) { - return this.clone().imul(num); -}; - -ScriptNum.prototype.div = function div(num) { - return this.clone().idiv(num); -}; - -ScriptNum.prototype.iadd = function iadd(num) { - return this.iaddn(num.value); -}; - -ScriptNum.prototype.isub = function isub(num) { - return this.isubn(num.value); -}; - -ScriptNum.prototype.imul = function imul(num) { - return this.imuln(num.value); -}; - -ScriptNum.prototype.idiv = function idiv(num) { - return this.idivn(num.value); -}; - -ScriptNum.prototype.addn = function addn(value) { - return this.clone().iaddn(value); -}; - -ScriptNum.prototype.subn = function subn(value) { - return this.clone().isubn(value); -}; - -ScriptNum.prototype.muln = function muln(value) { - return this.clone().imuln(value); -}; - -ScriptNum.prototype.divn = function divn(value) { - return this.clone().idivn(value); -}; - -ScriptNum.prototype.ushln = function ushln(value) { - return this.clone().iushln(value); -}; - -ScriptNum.prototype.ushrn = function ushrn(value) { - return this.clone().iushrn(value); -}; - -ScriptNum.prototype.iaddn = function iaddn(value) { - this.value += value; - return this; -}; - -ScriptNum.prototype.isubn = function isubn(value) { - this.value -= value; - return this; -}; - -ScriptNum.prototype.imuln = function imuln(value) { - this.value *= value; - return this; -}; - -ScriptNum.prototype.idivn = function idivn(value) { - this.value = Math.floor(this.value / value); - return this; -}; - -ScriptNum.prototype.iushln = function iushln(value) { - this.value *= Math.pow(2, value); - return this; -}; - -ScriptNum.prototype.iushrn = function iushrn(value) { - this.value = Math.floor(this.value / Math.pow(2, value)); - return this; -}; - -ScriptNum.prototype.cmp = function cmp(num) { - return this.cmpn(num.value); -}; - -ScriptNum.prototype.cmpn = function cmpn(value) { - if (this.value === value) - return 0; - return this.value < value ? -1 : 1; -}; - -ScriptNum.prototype.neg = function neg() { - return this.clone().ineg(); -}; - -ScriptNum.prototype.ineg = function ineg() { - this.value = -this.value; - return this; -}; - -ScriptNum.prototype.toNumber = function toNumber() { - return this.value; -}; - -ScriptNum.prototype.toString = function toString(base) { - if (!base) - base = 10; - - if (base === 10 || base === 'dec') - return this.value.toString(10); - - if (base === 16 || base === 'hex') { - let str = this.value.toString(16); - if (str.length % 2 !== 0) - str = '0' + str; - return str; + // Need to append sign bit. + let neg = false; + if (num.isNeg()) { + num = num.neg(); + neg = true; } - throw new Error(`Base ${base} not supported.`); -}; + // Calculate size. + const size = num.byteLength(); -ScriptNum.prototype.toJSON = function toJSON() { - return this.toString(16); -}; + let offset = 0; -ScriptNum.prototype.fromString = function fromString(str, base) { - if (!base) - base = 10; + if (num.testn((size * 8) - 1)) + offset = 1; - let negative = false; + // Write number. + const data = Buffer.allocUnsafe(size + offset); - if (str[0] === '-') { - assert(str.length > 1, 'Non-numeric string passed.'); - str = str.substring(1); - negative = true; + switch (size) { + case 8: + data[7] = (num.hi >>> 24) & 0xff; + case 7: + data[6] = (num.hi >> 16) & 0xff; + case 6: + data[5] = (num.hi >> 8) & 0xff; + case 5: + data[4] = num.hi & 0xff; + case 4: + data[3] = (num.lo >>> 24) & 0xff; + case 3: + data[2] = (num.lo >> 16) & 0xff; + case 2: + data[1] = (num.lo >> 8) & 0xff; + case 1: + data[0] = num.lo & 0xff; + } + + // Append sign bit. + if (data[size - 1] & 0x80) { + assert(offset === 1); + assert(data.length === size + offset); + data[size] = neg ? 0x80 : 0; + } else if (neg) { + assert(offset === 0); + assert(data.length === size); + data[size - 1] |= 0x80; } else { - assert(str.length > 0, 'Non-numeric string passed.'); + assert(offset === 0); + assert(data.length === size); } - this.value = 0; - - if (base === 10 || base === 'dec') { - let nonzero = 0; - for (let i = 0; i < str.length; i++) { - let ch = str[i]; - - if (nonzero === 0 && ch === '0') - continue; - - if (!(ch >= '0' && ch <= '9')) - throw new Error('Parse error.'); - - ch = ch.charCodeAt(0) - 48; - - nonzero++; - assert(nonzero <= 15, 'Number exceeds 2^48-1.'); - - this.value *= 10; - this.value += ch; - } - - if (negative) - this.value = -this.value; - - return this; - } - - if (base === 16 || base === 'hex') { - let nonzero = 0; - for (let i = 0; i < str.length; i++) { - let ch = str[i]; - - if (nonzero === 0 && ch === '0') - continue; - - if (ch >= '0' && ch <= '9') { - ch = ch.charCodeAt(0); - ch -= 48; - } else if (ch >= 'a' && ch <= 'f') { - ch = ch.charCodeAt(0); - ch -= 87; - } else if (ch >= 'A' && ch <= 'F') { - ch = ch.charCodeAt(0); - ch -= 55; - } else { - throw new Error('Parse error.'); - } - - nonzero++; - assert(nonzero <= 12, 'Number exceeds 2^48-1.'); - - this.value *= 16; - this.value += ch; - } - - if (negative) - this.value = -this.value; - - return this; - } - - throw new Error(`Base ${base} not supported.`); + return data; }; -ScriptNum.fromString = function fromString(str, base) { - return new ScriptNum(0).fromString(str, base); +ScriptNum.prototype.fromRaw = function fromRaw(data) { + assert(Buffer.isBuffer(data)); + + // Empty arrays are always zero. + if (data.length === 0) + return this; + + // Read number (9 bytes max). + switch (data.length) { + case 9: + // Note: this shift overflows to + // zero in modern bitcoin core. + this.lo |= data[8]; + case 8: + this.hi |= data[7] << 24; + case 7: + this.hi |= data[6] << 16; + case 6: + this.hi |= data[5] << 8; + case 5: + this.hi |= data[4]; + case 4: + this.lo |= data[3] << 24; + case 3: + this.lo |= data[2] << 16; + case 2: + this.lo |= data[1] << 8; + case 1: + this.lo |= data[0]; + } + + // Remove high bit and flip sign. + if (data[data.length - 1] & 0x80) { + this.setn((data.length * 8) - 1, 0); + this.ineg(); + } + + return this; }; -ScriptNum.prototype.fromRaw = function fromRaw(data, minimal, limit) { +ScriptNum.prototype.decode = function decode(data, minimal, limit) { + assert(Buffer.isBuffer(data)); + if (minimal == null) minimal = true; if (limit == null) limit = 4; - // We can't handle more than 6 bytes. - assert(limit <= 6, 'Number exceeds 48 bits.'); + // We can't handle more than 9 bytes. + assert(limit >= 4 && limit <= 9, 'Bad script number size limit.'); - // Max size is 4 bytes by default, 6 bytes max. + // Max size is 4 bytes by default, 9 bytes max. if (data.length > limit) throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.'); - // Empty arrays are always zero. - if (data.length === 0) { - this.value = 0; - return this; - } - // Ensure minimal serialization. - if (minimal) { - if ((data[data.length - 1] & 0x7f) === 0) { - if (data.length === 1 || !(data[data.length - 2] & 0x80)) { - throw new ScriptError( - 'UNKNOWN_ERROR', - 'Non-minimally encoded Script number.'); - } - } - } + if (minimal && !ScriptNum.isMinimal(data)) + throw new ScriptError('UNKNOWN_ERROR', 'Non-minimal script number.'); - this.value = 0; - - // Read number (6 bytes max). - switch (data.length) { - case 6: - this.value += data[5] * 0x10000000000; - case 5: - this.value += data[4] * 0x100000000; - case 4: - this.value += data[3] * 0x1000000; - case 3: - this.value += data[2] * 0x10000; - case 2: - this.value += data[1] * 0x100; - case 1: - this.value += data[0]; - } - - // Remove high bit and flip sign. - if (data[data.length - 1] & 0x80) { - switch (data.length) { - case 1: - case 2: - case 3: - case 4: - this.value &= ~(0x80 << (8 * (data.length - 1))); - break; - case 5: - this.value -= 0x8000000000; - break; - case 6: - this.value -= 0x800000000000; - break; - } - this.value = -this.value; - } - - return this; + return this.fromRaw(data); }; -ScriptNum.fromRaw = function fromRaw(data, minimal, limit) { - return new ScriptNum(0).fromRaw(data, minimal, limit); +ScriptNum.prototype.inspect = function inspect() { + return ``; }; -ScriptNum.prototype.toRaw = function toRaw() { - let value = this.value; +ScriptNum.isMinimal = function isMinimal(data) { + assert(Buffer.isBuffer(data)); - // Zeroes are always empty arrays. - if (value === 0) - return EMPTY_ARRAY; + if (data.length === 0) + return true; - // Need to append sign bit. - let negative = false; - if (value < 0) { - negative = true; - value = -value; + if ((data[data.length - 1] & 0x7f) === 0) { + if (data.length === 1) + return false; + + if (!(data[data.length - 2] & 0x80)) + return false; } - // Gauge buffer size. - let offset, size; - if (value <= 0xff) { - offset = (value & 0x80) ? 1 : 0; - size = 1; - } else if (value <= 0xffff) { - offset = (value & 0x8000) ? 1 : 0; - size = 2; - } else if (value <= 0xffffff) { - offset = (value & 0x800000) ? 1 : 0; - size = 3; - } else if (value <= 0xffffffff) { - offset = (value & 0x80000000) ? 1 : 0; - size = 4; - } else if (value <= 0xffffffffff) { - offset = value >= 0x8000000000 ? 1 : 0; - size = 5; - } else if (value <= 0xffffffffffff) { - offset = value >= 0x800000000000 ? 1 : 0; - size = 6; - } else { - throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.'); - } - - // Write number. - const data = Buffer.allocUnsafe(size + offset); - - switch (size) { - case 6: - data[5] = (value / 0x10000000000 | 0) & 0xff; - case 5: - data[4] = (value / 0x100000000 | 0) & 0xff; - case 4: - data[3] = (value >>> 24) & 0xff; - case 3: - data[2] = (value >> 16) & 0xff; - case 2: - data[1] = (value >> 8) & 0xff; - case 1: - data[0] = value & 0xff; - } - - // Append sign bit. - if (data[size - 1] & 0x80) - data[size] = negative ? 0x80 : 0; - else if (negative) - data[size - 1] |= 0x80; - - return data; + return true; }; -/* - * Expose - */ +ScriptNum.decode = function decode(data, minimal, limit) { + return new ScriptNum().decode(data, minimal, limit); +}; -module.exports = ScriptNum; +ScriptNum.isScriptNum = function isScriptNum(obj) { + return obj instanceof ScriptNum; +};