320 lines
6.2 KiB
JavaScript
320 lines
6.2 KiB
JavaScript
/*!
|
|
* scriptnum.js - script number object for bcoin.
|
|
* Copyright (c) 2017, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const {I64} = require('../utils/int64');
|
|
const {ScriptError} = require('./common');
|
|
|
|
/*
|
|
* Constants
|
|
*/
|
|
|
|
const EMPTY_ARRAY = Buffer.alloc(0);
|
|
|
|
/**
|
|
* Script Number
|
|
* @see https://github.com/chjj/n64
|
|
* @alias module:script.ScriptNum
|
|
* @constructor
|
|
* @param {(Number|String|Buffer|Object)?} num
|
|
* @param {(String|Number)?} base
|
|
* @property {Number} hi
|
|
* @property {Number} lo
|
|
* @property {Number} sign
|
|
*/
|
|
|
|
function ScriptNum(num, base) {
|
|
if (!(this instanceof ScriptNum))
|
|
return new ScriptNum(num, base);
|
|
|
|
I64.call(this, num, base);
|
|
}
|
|
|
|
Object.setPrototypeOf(ScriptNum, I64);
|
|
Object.setPrototypeOf(ScriptNum.prototype, I64.prototype);
|
|
|
|
/**
|
|
* Cast to int32.
|
|
* @returns {Number}
|
|
*/
|
|
|
|
ScriptNum.prototype.getInt = function getInt() {
|
|
if (this.lt(I64.INT32_MIN))
|
|
return I64.LONG_MIN;
|
|
|
|
if (this.gt(I64.INT32_MAX))
|
|
return I64.LONG_MAX;
|
|
|
|
return this.toInt();
|
|
};
|
|
|
|
/**
|
|
* Serialize script number.
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
ScriptNum.prototype.toRaw = function toRaw() {
|
|
let num = this;
|
|
|
|
// Zeroes are always empty arrays.
|
|
if (num.isZero())
|
|
return EMPTY_ARRAY;
|
|
|
|
// Need to append sign bit.
|
|
let neg = false;
|
|
if (num.isNeg()) {
|
|
num = num.neg();
|
|
neg = true;
|
|
}
|
|
|
|
// Calculate size.
|
|
const size = num.byteLength();
|
|
|
|
let offset = 0;
|
|
|
|
if (num.testn((size * 8) - 1))
|
|
offset = 1;
|
|
|
|
// Write number.
|
|
const data = Buffer.allocUnsafe(size + offset);
|
|
|
|
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(offset === 0);
|
|
assert(data.length === size);
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* Instantiate script number from serialized data.
|
|
* @private
|
|
* @param {Buffer} data
|
|
* @returns {ScriptNum}
|
|
*/
|
|
|
|
ScriptNum.prototype.fromRaw = function fromRaw(data) {
|
|
assert(Buffer.isBuffer(data));
|
|
assert(data.length <= 9);
|
|
|
|
// 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;
|
|
};
|
|
|
|
/**
|
|
* Serialize script number.
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
ScriptNum.prototype.encode = function encode() {
|
|
return this.toRaw();
|
|
};
|
|
|
|
/**
|
|
* Decode and verify script number.
|
|
* @private
|
|
* @param {Buffer} data
|
|
* @param {Boolean?} minimal - Require minimal encoding.
|
|
* @param {Number?} limit - Size limit.
|
|
* @returns {ScriptNum}
|
|
*/
|
|
|
|
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 9 bytes.
|
|
assert(limit >= 4 && limit <= 9, 'Bad script number size limit.');
|
|
|
|
// Max size is 4 bytes by default, 9 bytes max.
|
|
if (data.length > limit)
|
|
throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.');
|
|
|
|
// Ensure minimal serialization.
|
|
if (minimal && !ScriptNum.isMinimal(data))
|
|
throw new ScriptError('UNKNOWN_ERROR', 'Non-minimal script number.');
|
|
|
|
return this.fromRaw(data);
|
|
};
|
|
|
|
/**
|
|
* Inspect script number.
|
|
* @returns {String}
|
|
*/
|
|
|
|
ScriptNum.prototype.inspect = function inspect() {
|
|
return `<ScriptNum: ${this.toString(10)}>`;
|
|
};
|
|
|
|
/**
|
|
* Test wether a serialized script
|
|
* number is in its most minimal form.
|
|
* @param {Buffer} data
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
ScriptNum.isMinimal = function isMinimal(data) {
|
|
assert(Buffer.isBuffer(data));
|
|
|
|
if (data.length === 0)
|
|
return true;
|
|
|
|
if ((data[data.length - 1] & 0x7f) === 0) {
|
|
if (data.length === 1)
|
|
return false;
|
|
|
|
if (!(data[data.length - 2] & 0x80))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Encode a script number.
|
|
* @param {Number|ScriptNum|BN} num
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
ScriptNum.encode = function encode(num) {
|
|
assert(num != null);
|
|
|
|
if (ScriptNum.isScriptNum(num))
|
|
return num.encode();
|
|
|
|
if (typeof num === 'number')
|
|
num = ScriptNum.fromNumber(num);
|
|
else if (I64.isN64(num))
|
|
num = ScriptNum.fromObject(num);
|
|
else if (Array.isArray(num.words))
|
|
num = ScriptNum.fromBN(num);
|
|
else
|
|
throw new Error('Object must be encodable.');
|
|
|
|
return num.encode();
|
|
};
|
|
|
|
/**
|
|
* Decode and verify script number.
|
|
* @param {Buffer} data
|
|
* @param {Boolean?} minimal - Require minimal encoding.
|
|
* @param {Number?} limit - Size limit.
|
|
* @returns {ScriptNum}
|
|
*/
|
|
|
|
ScriptNum.decode = function decode(data, minimal, limit) {
|
|
return new ScriptNum().decode(data, minimal, limit);
|
|
};
|
|
|
|
/**
|
|
* Test whether object is encodable.
|
|
* @param {Object} obj
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
ScriptNum.isEncodable = function isEncodable(obj) {
|
|
if (obj == null)
|
|
return false;
|
|
|
|
if (ScriptNum.isScriptNum(obj))
|
|
return true;
|
|
|
|
if (typeof obj === 'number')
|
|
return true;
|
|
|
|
if (I64.isN64(obj))
|
|
return true;
|
|
|
|
if (Array.isArray(obj.words))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Test whether object is a script number.
|
|
* @param {Object} obj
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
ScriptNum.isScriptNum = function isScriptNum(obj) {
|
|
return obj instanceof ScriptNum;
|
|
};
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = ScriptNum;
|