diff --git a/lib/script/opcode.js b/lib/script/opcode.js index 9ec15ef42..afdf96aaf 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -212,13 +212,13 @@ class Opcode { toNum(minimal, limit) { if (this.value === opcodes.OP_0) - return ScriptNum.fromInt(0); + return ScriptNum.fromBigInt(0n); if (this.value === opcodes.OP_1NEGATE) - return ScriptNum.fromInt(-1); + return ScriptNum.fromBigInt(-1n); if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16) - return ScriptNum.fromInt(this.value - 0x50); + return ScriptNum.fromBigInt(BigInt(this.value - 0x50)); if (!this.data) return null; diff --git a/lib/script/script.js b/lib/script/script.js index 06bad4ccb..b6a5fd088 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -897,29 +897,30 @@ class Script extends bio.Struct { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let num = stack.getNum(-1, minimal, 4); + let binum = stack.getNum(-1, minimal, 4).toBigInt(); let cmp; switch (op.value) { case opcodes.OP_1ADD: - num.iaddn(1); + binum = bigInt64(binum + 1n); break; case opcodes.OP_1SUB: - num.isubn(1); + binum = bigInt64(binum - 1n); break; case opcodes.OP_NEGATE: - num.ineg(); + binum = bigInt64(-binum); break; case opcodes.OP_ABS: - num.iabs(); + if (binum < 0n) + binum = bigInt64(-binum); break; case opcodes.OP_NOT: - cmp = num.isZero(); - num = ScriptNum.fromBool(cmp); + cmp = binum === 0n; + binum = cmp ? 1n : 0n; break; case opcodes.OP_0NOTEQUAL: - cmp = !num.isZero(); - num = ScriptNum.fromBool(cmp); + cmp = binum !== 0n; + binum = BigInt(cmp); break; default: assert(false, 'Fatal script error.'); @@ -927,7 +928,7 @@ class Script extends bio.Struct { } stack.pop(); - stack.pushNum(num); + stack.pushNum(ScriptNum.fromBigInt(binum)); break; } @@ -947,59 +948,59 @@ class Script extends bio.Struct { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const n1 = stack.getNum(-2, minimal, 4); - const n2 = stack.getNum(-1, minimal, 4); + const bn1 = stack.getNum(-2, minimal, 4).toBigInt(); + const bn2 = stack.getNum(-1, minimal, 4).toBigInt(); - let num, cmp; + let binum, cmp; switch (op.value) { case opcodes.OP_ADD: - num = n1.iadd(n2); + binum = bigInt64(bn1 + bn2); break; case opcodes.OP_SUB: - num = n1.isub(n2); + binum = bigInt64(bn1 - bn2); break; case opcodes.OP_BOOLAND: - cmp = n1.toBool() && n2.toBool(); - num = ScriptNum.fromBool(cmp); + cmp = bn1 !== 0n && bn2 !== 0n; + binum = BigInt(cmp); break; case opcodes.OP_BOOLOR: - cmp = n1.toBool() || n2.toBool(); - num = ScriptNum.fromBool(cmp); + cmp = bn1 !== 0n || bn2 !== 0n; + binum = BigInt(cmp); break; case opcodes.OP_NUMEQUAL: - cmp = n1.eq(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 === bn2; + binum = BigInt(cmp); break; case opcodes.OP_NUMEQUALVERIFY: - cmp = n1.eq(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 === bn2; + binum = BigInt(cmp); break; case opcodes.OP_NUMNOTEQUAL: - cmp = !n1.eq(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 !== bn2; + binum = BigInt(cmp); break; case opcodes.OP_LESSTHAN: - cmp = n1.lt(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 < bn2; + binum = BigInt(cmp); break; case opcodes.OP_GREATERTHAN: - cmp = n1.gt(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 > bn2; + binum = BigInt(cmp); break; case opcodes.OP_LESSTHANOREQUAL: - cmp = n1.lte(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 <= bn2; + binum = BigInt(cmp); break; case opcodes.OP_GREATERTHANOREQUAL: - cmp = n1.gte(n2); - num = ScriptNum.fromBool(cmp); + cmp = bn1 >= bn2; + binum = BigInt(cmp); break; case opcodes.OP_MIN: - num = ScriptNum.min(n1, n2); + binum = bn1 < bn2 ? bn1 : bn2; break; case opcodes.OP_MAX: - num = ScriptNum.max(n1, n2); + binum = bn1 > bn2 ? bn1 : bn2; break; default: assert(false, 'Fatal script error.'); @@ -1008,7 +1009,7 @@ class Script extends bio.Struct { stack.pop(); stack.pop(); - stack.pushNum(num); + stack.pushNum(ScriptNum.fromBigInt(binum)); if (op.value === opcodes.OP_NUMEQUALVERIFY) { if (!stack.getBool(-1)) @@ -1022,11 +1023,11 @@ class Script extends bio.Struct { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const n1 = stack.getNum(-3, minimal, 4); - const n2 = stack.getNum(-2, minimal, 4); - const n3 = stack.getNum(-1, minimal, 4); + const n1 = stack.getNum(-3, minimal, 4).toBigInt(); + const n2 = stack.getNum(-2, minimal, 4).toBigInt(); + const n3 = stack.getNum(-1, minimal, 4).toBigInt(); - const val = n2.lte(n1) && n1.lt(n3); + const val = n2 <= n1 && n1 < n3; stack.pop(); stack.pop(); @@ -2457,6 +2458,16 @@ function checksig(msg, sig, key) { return secp256k1.verify(msg, sig.slice(0, -1), key); } +/** + * Make sure number is int64. + * @param {BigInt} value + * @returns {BigInt} + */ + +function bigInt64(value) { + return BigInt.asIntN(64, value); +} + /* * Expose */ diff --git a/lib/script/scriptnum.js b/lib/script/scriptnum.js index 2c91dd6cb..73a3d3e2d 100644 --- a/lib/script/scriptnum.js +++ b/lib/script/scriptnum.js @@ -7,34 +7,81 @@ 'use strict'; const assert = require('bsert'); -const {I64} = require('n64'); const ScriptError = require('./scripterror'); +/* eslint valid-typeof: "off" */ + +/** + * @typedef {10|16|8|2} FastBase + */ + /* * Constants */ const EMPTY_ARRAY = Buffer.alloc(0); +const INT32N_MAX = 0x7fffffffn; +const INT32N_MIN = -0x80000000n; + +const INT32_MAX = 0x7fffffff; +const INT32_MIN = -0x80000000; + /** * Script Number - * @see https://github.com/chjj/n64 * @alias module:script.ScriptNum - * @property {Number} hi - * @property {Number} lo - * @property {Number} sign */ -class ScriptNum extends I64 { +class ScriptNum { + /** @type {bigint} */ + value; + /** * Create a script number. * @constructor - * @param {(Number|String|Buffer|Object)?} num - * @param {(String|Number)?} base + * @param {BigInt} [num=0n] + */ + + constructor(num = 0n) { + assert(typeof num === 'bigint'); + this.value = num; + } + + /** + * is Negative + * @returns {Boolean} + */ + + isNeg() { + return this.value < 0n; + } + + /** + * Get double value. + * @returns {Number} */ - constructor(num, base) { - super(num, base); + toDouble() { + return Number(this.value); + } + + /** + * Get Number. + * Alias to toDouble. + * @returns {Number} + */ + + toNumber() { + return this.toDouble(); + } + + /** + * Get value. + * @returns {BigInt} + */ + + toBigInt() { + return this.value; } /** @@ -43,40 +90,108 @@ class ScriptNum extends I64 { */ getInt() { - if (this.lt(I64.INT32_MIN)) - return I64.LONG_MIN; + if (this.value < INT32N_MIN) + return INT32_MIN; - if (this.gt(I64.INT32_MAX)) - return I64.LONG_MAX; + if (this.value > INT32N_MAX) + return INT32_MAX; return this.toInt(); } + /** + * Cast to int32. + * NOTE: limits are enforced by getInt. + * @private + * @returns {Number} + */ + + toInt() { + return Number(this.value); + } + + /** + * Cast to bool. + * @returns {Boolean} + */ + + toBool() { + return this.value !== 0n; + } + + /** + * Create ScriptNum from BigInt. + * @param {BigInt} num + * @returns {ScriptNum} + */ + + fromBigInt(num) { + assert(typeof num === 'bigint'); + this.value = num; + return this; + } + + /** + * Create ScriptNum from number. + * @param {Number} num + * @returns {ScriptNum} + */ + + fromNumber(num) { + assert(typeof num === 'number'); + this.value = BigInt(num); + return this; + } + + /** + * Create ScriptNum from string. + * @param {String} str + * @param {Number} [base=10] + * @returns {ScriptNum} + */ + + fromString(str, base = 10) { + assert(typeof str === 'string'); + + this.value = fromStringFast(str, base); + + return this; + } + + /** + * Stringify. + * @param {Number} [base=10] + * @returns {String} + */ + + toString(base = 10) { + return this.value.toString(base); + } + /** * Serialize script number. * @returns {Buffer} */ encode() { - let num = this; - - // Zeroes are always empty arrays. - if (num.isZero()) + if (this.value === 0n) return EMPTY_ARRAY; // Need to append sign bit. let neg = false; - if (num.isNeg()) { - num = num.neg(); + let absval = this.value; + + if (absval < 0n) { + absval = bigInt64(-this.value); neg = true; } // Calculate size. - const size = num.byteLength(); + const size = byteLength(absval); let offset = 0; - if (num.testn((size * 8) - 1)) + if (testBit(absval, (size * 8) - 1)) offset = 1; // Write number. @@ -84,21 +199,21 @@ class ScriptNum extends I64 { switch (size) { case 8: - data[7] = (num.hi >>> 24) & 0xff; + data[7] = Number((absval >> 56n) & 0xffn); case 7: - data[6] = (num.hi >> 16) & 0xff; + data[6] = Number((absval >> 48n) & 0xffn); case 6: - data[5] = (num.hi >> 8) & 0xff; + data[5] = Number((absval >> 40n) & 0xffn); case 5: - data[4] = num.hi & 0xff; + data[4] = Number((absval >> 32n) & 0xffn); case 4: - data[3] = (num.lo >>> 24) & 0xff; + data[3] = Number((absval >> 24n) & 0xffn); case 3: - data[2] = (num.lo >> 16) & 0xff; + data[2] = Number((absval >> 16n) & 0xffn); case 2: - data[1] = (num.lo >> 8) & 0xff; + data[1] = Number((absval >> 8n) & 0xffn); case 1: - data[0] = num.lo & 0xff; + data[0] = Number(absval & 0xffn); } // Append sign bit. @@ -132,37 +247,41 @@ class ScriptNum extends I64 { if (data.length === 0) return this; + let result = 0n; + // Read number (9 bytes max). switch (data.length) { case 8: - this.hi |= data[7] << 24; + result |= BigInt(data[7]) << 56n; case 7: - this.hi |= data[6] << 16; + result |= BigInt(data[6]) << 48n; case 6: - this.hi |= data[5] << 8; + result |= BigInt(data[5]) << 40n; case 5: - this.hi |= data[4]; + result |= BigInt(data[4]) << 32n; case 4: - this.lo |= data[3] << 24; + result |= BigInt(data[3]) << 24n; case 3: - this.lo |= data[2] << 16; + result |= BigInt(data[2]) << 16n; case 2: - this.lo |= data[1] << 8; + result |= BigInt(data[1]) << 8n; case 1: - this.lo |= data[0]; + result |= BigInt(data[0]); break; default: for (let i = 0; i < data.length; i++) - this.orb(i, data[i]); + result |= BigInt(data[i]) << BigInt(8 * i); break; } // Remove high bit and flip sign. if (data[data.length - 1] & 0x80) { - this.setn((data.length * 8) - 1, 0); - this.ineg(); + result = setBit(result, (data.length * 8) - 1, 0); + result = -result; } + this.value = result; + return this; } @@ -220,6 +339,36 @@ class ScriptNum extends I64 { return true; } + /** + * Create ScriptNum from string. + * @param {String} str + * @param {Number} [base=10] + * @returns {ScriptNum} + */ + + static fromString(str, base = 10) { + return new this().fromString(str, base); + } + + /** + * Create ScriptNum from number. + * @param {Number} num + */ + + static fromNumber(num) { + return new this().fromNumber(num); + } + + /** + * Create ScriptNum from bigint. + * @param {BigInt} num + * @returns {ScriptNum} + */ + + static fromBigInt(num) { + return new this().fromBigInt(num); + } + /** * Decode and verify script number. * @param {Buffer} data @@ -243,6 +392,110 @@ class ScriptNum extends I64 { } } +/* + * Helpers + */ + +/** + * Calculate byte length for a bigint. + * @param {BigInt} num + * @returns {Number} + */ + +function byteLength(num) { + if (num === 0n) + return 0; + + if (num < 0n) + num = -num; + + return Math.ceil(num.toString(2).length / 8); +} + +/** + * Test whether a bigint has a bit set. + * @param {BigInt} num + * @param {Number} bit must be between 0 and 63. + * @returns {Boolean} + */ + +function testBit(num, bit) { + bit &= 63; + + return (num >> BigInt(bit)) & 1n; +} + +/** + * Set specific bit on a bigint. + * @param {BigInt} num + * @param {Number} bit must be between 0 and 63. + * @param {Number} value + * @returns {BigInt} + */ + +function setBit(num, bit, value) { + bit &= 63; + + if (value === 0) + return num & ~(1n << BigInt(bit)); + + return num | (1n << BigInt(bit)); +} + +/** + * Get bigint from string + * @param {String} str + * @param {FastBase} base + * @returns {BigInt} + */ + +function fromStringFast(str, base = 10) { + let neg = false; + let num; + + if (str.length > 0 && str[0] === '-') { + neg = true; + str = str.substring(1); + } + + switch (base) { + case 2: + str = '0b' + str; + break; + case 8: + str = '0o' + str; + break; + case 16: + str = '0x' + str; + break; + case 10: + break; + default: + throw new Error('Invalid base.'); + } + + try { + num = BigInt(str); + } catch (e) { + throw new Error('Invalid string.'); + } + + if (neg) + num = -num; + + return num; +} + +/** + * Make sure number is int64. + * @param {BigInt} value + * @returns {BigInt} + */ + +function bigInt64(value) { + return BigInt.asIntN(64, value); +} + /* * Expose */ diff --git a/package-lock.json b/package-lock.json index 5493a5e4d..bdef06c06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "bval": "~0.1.8", "bweb": "~0.2.0", "goosig": "~0.10.0", - "n64": "~0.2.10", "urkel": "~1.0.3" }, "bin": { @@ -446,14 +445,6 @@ "node": ">=8.0.0" } }, - "node_modules/n64": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/n64/-/n64-0.2.10.tgz", - "integrity": "sha512-uH9geV4+roR1tohsrrqSOLCJ9Mh1iFcDI+9vUuydDlDxUS1UCAWUfuGb06p3dj3flzywquJNrGsQ7lHP8+4RVQ==", - "engines": { - "node": ">=2.0.0" - } - }, "node_modules/unbound": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/unbound/-/unbound-0.4.3.tgz", diff --git a/package.json b/package.json index adc20a891..2af7f8cef 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "bval": "~0.1.8", "bweb": "~0.2.0", "goosig": "~0.10.0", - "n64": "~0.2.10", "urkel": "~1.0.3" }, "devDependencies": { diff --git a/test/script-test.js b/test/script-test.js index 9f436b143..fb24f6c05 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -360,6 +360,11 @@ describe('Script', function() { (1 << 24) - 1, (1 << 31), 1500, -1500 ]; + const serializationVectorBI = [ + 1n - 1n << 32n, + 1n << 40n + ]; + it('should serialize script numbers correctly', () => { for (const [num, bytes] of sn2bytesVector) { const sn = ScriptNum.fromNumber(num); @@ -370,6 +375,16 @@ describe('Script', function() { } }); + it('should serialize script numbers correctly (BigInt)', () => { + for (const [num, bytes] of sn2bytesVector) { + const sn = ScriptNum.fromBigInt(BigInt(num)); + const numBytes = sn.encode(); + const testBuffer = Buffer.from(bytes); + + assert.bufferEqual(numBytes, testBuffer); + } + }); + it('should serialize/deserialize script numbers correctly', () => { for (const num of serializationVector) { const encoded = ScriptNum.fromNumber(num).encode(); @@ -378,6 +393,20 @@ describe('Script', function() { assert.strictEqual(num, final); } }); + + it('should serialize/deserialize script numbers correctly (BigInt)', () => { + const all = [ + ...serializationVector.map(n => BigInt(n)), + ...serializationVectorBI + ]; + + for (const num of all) { + const encoded = ScriptNum.fromBigInt(num).encode(); + const final = ScriptNum.decode(encoded).toBigInt(); + + assert.strictEqual(num, final); + } + }); }); describe('Script - mathops', function () {