Repository: bitcoinjs/bitcoinjs-lib
Pull request #516 (2016-01-04 15:03:52)

script_number: add impl/tests
@jprichardson in working on the spend example for #511 (which I now regret merging without it), I've realised we didn't have a `CScriptNum` equivalent for script numbers encoded in a stack.


Patch:
diff --git a/src/script.js b/src/script.js
index 6f9b21ee..24783e57 100644
--- a/src/script.js
+++ b/src/script.js
@@ -372,6 +372,8 @@ module.exports = {
   fromASM: fromASM,
   toASM: toASM,
 
+  number: require('./script_number'),
+
   isCanonicalPubKey: isCanonicalPubKey,
   isCanonicalSignature: isCanonicalSignature,
   isDefinedHashType: isDefinedHashType,
diff --git a/src/script_number.js b/src/script_number.js
new file mode 100644
index 00000000..2c1a0ce9
--- /dev/null
+++ b/src/script_number.js
@@ -0,0 +1,66 @@
+function decode (buffer, maxLength, minimal) {
+  maxLength = maxLength || 4
+  minimal = minimal === undefined ? true : minimal
+
+  var length = buffer.length
+  if (length === 0) return 0
+  if (length > maxLength) throw new TypeError('Script number overflow')
+  if (minimal) {
+    if ((buffer[length - 1] & 0x7f) === 0) {
+      if (length <= 1 || (buffer[length - 2] & 0x80) === 0) throw new Error('Non-minimally encoded script number')
+    }
+  }
+
+  // 40-bit
+  if (length === 5) {
+    var a = buffer.readUInt32LE(0)
+    var b = buffer.readUInt8(4)
+
+    if (b & 0x80) return -((b & ~0x80) * 0x100000000 + a)
+    return b * 0x100000000 + a
+  }
+
+  var result = 0
+
+  // 32-bit / 24-bit / 16-bit / 8-bit
+  for (var i = 0; i < length; ++i) {
+    result |= buffer[i] << (8 * i)
+  }
+
+  if (buffer[length - 1] & 0x80) return -(result & ~(0x80 << (8 * (length - 1))))
+  return result
+}
+
+function scriptNumSize (i) {
+  return i > 0x7fffffff ? 5
+  : i > 0x7fffff ? 4
+  : i > 0x7fff ? 3
+  : i > 0x7f ? 2
+  : i > 0x00 ? 1
+  : 0
+}
+
+function encode (number) {
+  var value = Math.abs(number)
+  var size = scriptNumSize(value)
+  var buffer = new Buffer(size)
+  var negative = number < 0
+
+  for (var i = 0; i < size; ++i) {
+    buffer.writeUInt8(value & 0xff, i)
+    value >>= 8
+  }
+
+  if (buffer[size - 1] & 0x80) {
+    buffer.writeUInt8(negative ? 0x80 : 0x00, size - 1)
+  } else if (negative) {
+    buffer[size - 1] |= 0x80
+  }
+
+  return buffer
+}
+
+module.exports = {
+  decode: decode,
+  encode: encode
+}


Test patch:
diff --git a/test/fixtures/script_number.json b/test/fixtures/script_number.json
new file mode 100644
index 00000000..f934740d
--- /dev/null
+++ b/test/fixtures/script_number.json
@@ -0,0 +1,225 @@
+[
+  {
+    "hex": "",
+    "number": 0
+  },
+  {
+    "hex": "01",
+    "number": 1
+  },
+  {
+    "hex": "02",
+    "number": 2
+  },
+  {
+    "hex": "03",
+    "number": 3
+  },
+  {
+    "hex": "7e",
+    "number": 126
+  },
+  {
+    "hex": "7f",
+    "number": 127
+  },
+  {
+    "hex": "8000",
+    "number": 128
+  },
+  {
+    "hex": "8100",
+    "number": 129
+  },
+  {
+    "hex": "8200",
+    "number": 130
+  },
+  {
+    "hex": "ff00",
+    "number": 255
+  },
+  {
+    "hex": "fe7f",
+    "number": 32766
+  },
+  {
+    "hex": "ff7f",
+    "number": 32767
+  },
+  {
+    "hex": "008000",
+    "number": 32768
+  },
+  {
+    "hex": "ffff00",
+    "number": 65535
+  },
+  {
+    "hex": "018000",
+    "number": 32769
+  },
+  {
+    "hex": "028000",
+    "number": 32770
+  },
+  {
+    "hex": "ffffff00",
+    "number": 16777215
+  },
+  {
+    "hex": "feff7f",
+    "number": 8388606
+  },
+  {
+    "hex": "ffff7f",
+    "number": 8388607
+  },
+  {
+    "hex": "00008000",
+    "number": 8388608
+  },
+  {
+    "hex": "01008000",
+    "number": 8388609
+  },
+  {
+    "hex": "02008000",
+    "number": 8388610
+  },
+  {
+    "hex": "feffff7f",
+    "number": 2147483646
+  },
+  {
+    "hex": "ffffff7f",
+    "number": 2147483647
+  },
+  {
+    "bytes": 5,
+    "hex": "0000008000",
+    "number": 2147483648
+  },
+  {
+    "bytes": 5,
+    "hex": "0100008000",
+    "number": 2147483649
+  },
+  {
+    "bytes": 5,
+    "hex": "0200008000",
+    "number": 2147483650
+  },
+  {
+    "bytes": 5,
+    "hex": "ffffffff00",
+    "number": 4294967295
+  },
+  {
+    "bytes": 5,
+    "hex": "0200008080",
+    "number": -2147483650
+  },
+  {
+    "bytes": 5,
+    "hex": "0100008080",
+    "number": -2147483649
+  },
+  {
+    "bytes": 5,
+    "hex": "0000008080",
+    "number": -2147483648
+  },
+  {
+    "hex": "ffffffff",
+    "number": -2147483647
+  },
+  {
+    "hex": "feffffff",
+    "number": -2147483646
+  },
+  {
+    "hex": "fdffffff",
+    "number": -2147483645
+  },
+  {
+    "hex": "ffffff80",
+    "number": -16777215
+  },
+  {
+    "hex": "01008080",
+    "number": -8388609
+  },
+  {
+    "hex": "00008080",
+    "number": -8388608
+  },
+  {
+    "hex": "ffffff",
+    "number": -8388607
+  },
+  {
+    "hex": "feffff",
+    "number": -8388606
+  },
+  {
+    "hex": "fdffff",
+    "number": -8388605
+  },
+  {
+    "hex": "ffff80",
+    "number": -65535
+  },
+  {
+    "hex": "018080",
+    "number": -32769
+  },
+  {
+    "hex": "008080",
+    "number": -32768
+  },
+  {
+    "hex": "ffff",
+    "number": -32767
+  },
+  {
+    "hex": "feff",
+    "number": -32766
+  },
+  {
+    "hex": "fdff",
+    "number": -32765
+  },
+  {
+    "hex": "ff80",
+    "number": -255
+  },
+  {
+    "hex": "8180",
+    "number": -129
+  },
+  {
+    "hex": "8080",
+    "number": -128
+  },
+  {
+    "hex": "ff",
+    "number": -127
+  },
+  {
+    "hex": "fe",
+    "number": -126
+  },
+  {
+    "hex": "fd",
+    "number": -125
+  },
+  {
+    "hex": "82",
+    "number": -2
+  },
+  {
+    "hex": "81",
+    "number": -1
+  }
+]
diff --git a/test/script_number.js b/test/script_number.js
new file mode 100644
index 00000000..8200df3a
--- /dev/null
+++ b/test/script_number.js
@@ -0,0 +1,27 @@
+/* global describe, it */
+
+var assert = require('assert')
+var scriptNumber = require('../src/script_number')
+var fixtures = require('./fixtures/script_number.json')
+
+describe('script', function () {
+  describe('decode', function () {
+    fixtures.forEach(function (f) {
+      it(f.hex + ' returns ' + f.number, function () {
+        var actual = scriptNumber.decode(new Buffer(f.hex, 'hex'), f.bytes)
+
+        assert.strictEqual(actual, f.number)
+      })
+    })
+  })
+
+  describe('encode', function () {
+    fixtures.forEach(function (f) {
+      it(f.number + ' returns ' + f.hex, function () {
+        var actual = scriptNumber.encode(f.number)
+
+        assert.strictEqual(actual.toString('hex'), f.hex)
+      })
+    })
+  })
+})