@bsv/sdk 1.9.22 → 1.9.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.9.22",
3
+ "version": "1.9.24",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -282,13 +282,15 @@ export function toArray (
282
282
  return res
283
283
  }
284
284
 
285
- function htonl (w: number): number {
286
- const res =
287
- (w >>> 24) |
288
- ((w >>> 8) & 0xff00) |
289
- ((w << 8) & 0xff0000) |
290
- ((w & 0xff) << 24)
291
- return res >>> 0
285
+ /**
286
+ * @deprecated
287
+ * This function behaves differently from the standard C `htonl()`.
288
+ * It always performs an unconditional 32-bit byte swap.
289
+ * Use `swapBytes32()` for explicit byte swapping, or `realHtonl()` for
290
+ * standards-compliant host-to-network conversion.
291
+ */
292
+ export function htonl (w: number): number {
293
+ return swapBytes32(w)
292
294
  }
293
295
 
294
296
  function toHex32 (msg: number[], endian?: 'little' | 'big'): string {
@@ -1857,3 +1859,66 @@ export function pbkdf2 (
1857
1859
  const out = pbkdf2Fast(p, s, iterations, keylen)
1858
1860
  return Array.from(out)
1859
1861
  }
1862
+
1863
+ /**
1864
+ * Unconditionally swaps the byte order of a 32-bit unsigned integer.
1865
+ *
1866
+ * This function performs a strict 32-bit byte swap regardless of host
1867
+ * endianness. It is equivalent to the behavior commonly referred to as
1868
+ * `bswap32` in low-level libraries.
1869
+ *
1870
+ * This function is introduced as part of TOB-20 to provide a clearly-named
1871
+ * alternative to `htonl()`, which was previously implemented as an
1872
+ * unconditional byte swap and did not match the semantics of the traditional
1873
+ * C `htonl()` function.
1874
+ *
1875
+ * @param w - A 32-bit unsigned integer.
1876
+ * @returns The value with its byte order reversed.
1877
+ *
1878
+ * @example
1879
+ * swapBytes32(0x11223344) // → 0x44332211
1880
+ */
1881
+ export function swapBytes32 (w: number): number {
1882
+ const res =
1883
+ (w >>> 24) |
1884
+ ((w >>> 8) & 0xff00) |
1885
+ ((w << 8) & 0xff0000) |
1886
+ ((w & 0xff) << 24)
1887
+ return res >>> 0
1888
+ }
1889
+
1890
+ // Detect the host machine's endianness at runtime.
1891
+ //
1892
+ // This is used by `realHtonl()` to determine whether the value must be
1893
+ // byte-swapped or returned unchanged. JavaScript engines on common platforms
1894
+ // are almost always little-endian, but this check is included for correctness.
1895
+ const isLittleEndian = (() => {
1896
+ const b = new ArrayBuffer(4)
1897
+ const a = new Uint32Array(b)
1898
+ const c = new Uint8Array(b)
1899
+ a[0] = 0x01020304
1900
+ return c[0] === 0x04
1901
+ })()
1902
+
1903
+ /**
1904
+ * Converts a 32-bit unsigned integer from host byte order to network byte order.
1905
+ *
1906
+ * Unlike the legacy `htonl()` implementation (which always swapped bytes),
1907
+ * this function behaves like the traditional C `htonl()`:
1908
+ *
1909
+ * - On **little-endian** machines → performs a byte swap.
1910
+ * - On **big-endian** machines → returns the value unchanged.
1911
+ *
1912
+ * This function is provided to resolve TOB-20, which identified that the
1913
+ * previous `htonl()` implementation had a misleading name and did not match
1914
+ * platform-dependent semantics.
1915
+ *
1916
+ * @param w - A 32-bit unsigned integer.
1917
+ * @returns The value converted to network byte order.
1918
+ *
1919
+ * @example
1920
+ * realHtonl(0x11223344) // → 0x44332211 on little-endian systems
1921
+ */
1922
+ export function realHtonl (w: number): number {
1923
+ return isLittleEndian ? swapBytes32(w) : (w >>> 0)
1924
+ }
@@ -73,6 +73,14 @@ export default class JacobianPoint extends BasePoint {
73
73
  }
74
74
 
75
75
  this.zOne = this.z === this.curve.one
76
+
77
+ // --- Canonicalize point at infinity ---
78
+ if (this.isInfinity()) {
79
+ this.x = this.curve.one
80
+ this.y = this.curve.one
81
+ this.z = new BigNumber(0).toRed(this.curve.red)
82
+ this.zOne = false
83
+ }
76
84
  }
77
85
 
78
86
  /**
@@ -361,9 +369,18 @@ export default class JacobianPoint extends BasePoint {
361
369
  return true
362
370
  }
363
371
 
372
+ p = p as JacobianPoint
373
+
374
+ // --- Infinity handling ---
375
+ if (this.isInfinity() && p.isInfinity()) {
376
+ return true
377
+ }
378
+ if (this.isInfinity() !== p.isInfinity()) {
379
+ return false
380
+ }
381
+
364
382
  // x1 * z2^2 == x2 * z1^2
365
383
  const z2 = this.z.redSqr()
366
- p = p as JacobianPoint
367
384
  const pz2 = p.z.redSqr()
368
385
  if (this.x.redMul(pz2).redISub(p.x.redMul(z2)).cmpn(0) !== 0) {
369
386
  return false
@@ -445,6 +445,10 @@ export default class Point extends BasePoint {
445
445
  * const encodedPointHex = aPoint.encode(true, 'hex');
446
446
  */
447
447
  encode (compact: boolean = true, enc?: 'hex'): number[] | string {
448
+ if (this.inf) {
449
+ if (enc === 'hex') return '00'
450
+ return [0x00]
451
+ }
448
452
  const len = this.curve.p.byteLength()
449
453
  const x = this.getX().toArray('be', len)
450
454
  let res: number[]
@@ -212,3 +212,66 @@ describe('Point codec', () => {
212
212
  makeShortTest(shortPointOddY)
213
213
  )
214
214
  })
215
+
216
+ describe('JacobianPoint – Infinity handling and equality (TOB-18)', () => {
217
+ function J(x: any, y: any, z: any) {
218
+ return new JPoint(x, y, z)
219
+ }
220
+
221
+ it('Multiple infinity representations are canonicalized and equal', () => {
222
+ const inf1 = J(null, null, null)
223
+ const inf2 = J('0', '0', '0')
224
+ const inf3 = J(new BigNumber(0), new BigNumber(0), new BigNumber(0))
225
+
226
+ expect(inf1.isInfinity()).toBe(true)
227
+ expect(inf2.isInfinity()).toBe(true)
228
+ expect(inf3.isInfinity()).toBe(true)
229
+
230
+ expect(inf1.eq(inf2)).toBe(true)
231
+ expect(inf2.eq(inf3)).toBe(true)
232
+ expect(inf1.eq(inf3)).toBe(true)
233
+ })
234
+
235
+ it('Infinity must not equal a finite point', () => {
236
+ const inf = J(null, null, null)
237
+
238
+ const good = J(
239
+ new BigNumber('e7789226', 16),
240
+ new BigNumber('4b76b191', 16),
241
+ new BigNumber('cbf8d990', 16)
242
+ )
243
+
244
+ expect(inf.eq(good)).toBe(false)
245
+ expect(good.eq(inf)).toBe(false)
246
+ })
247
+
248
+ it('Infinity equals infinity (canonicalized)', () => {
249
+ const inf1 = J(null, null, null)
250
+ const inf2 = J('0', '0', '0')
251
+
252
+ expect(inf1.eq(inf2)).toBe(true)
253
+ expect(inf2.eq(inf1)).toBe(true)
254
+ })
255
+
256
+ it('Infinity is detected when z is zero in RED form', () => {
257
+ const redZero = new BigNumber(0).toRed(new Curve().red)
258
+ const p = J('1', '2', redZero)
259
+
260
+ expect(p.isInfinity()).toBe(true)
261
+
262
+ const clean = J(null, null, null)
263
+ expect(p.eq(clean)).toBe(true)
264
+ expect(clean.eq(p)).toBe(true)
265
+ })
266
+
267
+ it('eq() must handle mixed canonical and non-canonical infinity cases', () => {
268
+ const canonical = J(null, null, null)
269
+ const messy = J('1', '1', new BigNumber(0))
270
+
271
+ expect(messy.isInfinity()).toBe(true)
272
+ expect(canonical.eq(messy)).toBe(true)
273
+ expect(messy.eq(canonical)).toBe(true)
274
+ })
275
+ })
276
+
277
+
@@ -105,9 +105,9 @@ describe('Hash', function () {
105
105
  describe('BaseHash padding and endianness', () => {
106
106
  it('encodes length in big-endian for SHA1', () => {
107
107
  const sha1 = new (hash as any).SHA1()
108
- ;(sha1 as any).pendingTotal = 12345
109
- const pad = (sha1 as any)._pad() as number[]
110
- const padLength = (sha1 as any).padLength as number
108
+ ;(sha1).pendingTotal = 12345
109
+ const pad = (sha1)._pad() as number[]
110
+ const padLength = (sha1).padLength as number
111
111
  const lengthBytes = pad.slice(-padLength)
112
112
 
113
113
  const totalBits = BigInt(12345) * 8n
@@ -123,9 +123,9 @@ describe('Hash', function () {
123
123
 
124
124
  it('encodes length in little-endian for RIPEMD160', () => {
125
125
  const ripemd = new (hash as any).RIPEMD160()
126
- ;(ripemd as any).pendingTotal = 12345
127
- const pad = (ripemd as any)._pad() as number[]
128
- const padLength = (ripemd as any).padLength as number
126
+ ;(ripemd).pendingTotal = 12345
127
+ const pad = (ripemd)._pad() as number[]
128
+ const padLength = (ripemd).padLength as number
129
129
  const lengthBytes = pad.slice(-padLength)
130
130
 
131
131
  const totalBits = BigInt(12345) * 8n
@@ -141,11 +141,11 @@ describe('Hash', function () {
141
141
 
142
142
  it('throws when message length exceeds maximum encodable bits', () => {
143
143
  const sha1 = new (hash as any).SHA1()
144
- ;(sha1 as any).padLength = 1
145
- ;(sha1 as any).pendingTotal = 40
144
+ ;(sha1).padLength = 1
145
+ ;(sha1).pendingTotal = 40
146
146
 
147
147
  expect(() => {
148
- ;(sha1 as any)._pad()
148
+ ;(sha1)._pad()
149
149
  }).toThrow(new Error('Message too long for this hash function'))
150
150
  })
151
151
  })
@@ -180,7 +180,6 @@ describe('Hash', function () {
180
180
  })
181
181
 
182
182
  describe('Hash strict length validation (TOB-21)', () => {
183
-
184
183
  it('throws when pendingTotal is not a safe integer', () => {
185
184
  const h = new SHA1()
186
185
 
@@ -201,4 +200,57 @@ describe('Hash', function () {
201
200
  }).toThrow('Message too long for this hash function')
202
201
  })
203
202
  })
203
+
204
+ describe('TOB-20 byte-order helper functions', () => {
205
+ const { htonl, swapBytes32, realHtonl } = hash
206
+
207
+ it('swapBytes32 performs a strict 32-bit byte swap', () => {
208
+ expect(swapBytes32(0x11223344)).toBe(0x44332211)
209
+ expect(swapBytes32(0xaabbccdd)).toBe(0xddccbbaa)
210
+ expect(swapBytes32(0x00000000)).toBe(0x00000000)
211
+ expect(swapBytes32(0xffffffff)).toBe(0xffffffff)
212
+ })
213
+
214
+ it('swapBytes32 always returns an unsigned 32-bit integer', () => {
215
+ expect(swapBytes32(-1)).toBe(0xffffffff) // wraps to unsigned
216
+ expect(swapBytes32(0x80000000)).toBe(0x00000080) // MSB becomes LSB
217
+ })
218
+
219
+ it('htonl is now an alias for swapBytes32 (deprecated)', () => {
220
+ expect(htonl(0x11223344)).toBe(swapBytes32(0x11223344))
221
+ expect(htonl(0xaabbccdd)).toBe(swapBytes32(0xaabbccdd))
222
+ })
223
+
224
+ it('realHtonl matches swapBytes32 on little-endian systems', () => {
225
+ // All JS engines used for Node/Jest are little-endian
226
+ expect(realHtonl(0x11223344)).toBe(0x44332211)
227
+ expect(realHtonl(0xaabbccdd)).toBe(0xddccbbaa)
228
+ })
229
+
230
+ it('realHtonl preserves value when system is big-endian (forced simulation)', () => {
231
+ // We simulate the big-endian branch of realHtonl by calling
232
+ // the fallback path directly.
233
+ const forceBigEndianRealHtonl = (w: number) => (w >>> 0)
234
+
235
+ expect(forceBigEndianRealHtonl(0x11223344)).toBe(0x11223344)
236
+ expect(forceBigEndianRealHtonl(0xaabbccdd)).toBe(0xaabbccdd)
237
+ expect(forceBigEndianRealHtonl(0xffffffff)).toBe(0xffffffff >>> 0)
238
+ })
239
+
240
+ it('htonl, swapBytes32, realHtonl never throw for any 32-bit input', () => {
241
+ const inputs = [
242
+ 0, 1, -1,
243
+ 0x7fffffff,
244
+ 0x80000000,
245
+ 0xffffffff,
246
+ 0x12345678
247
+ ]
248
+
249
+ for (const n of inputs) {
250
+ expect(() => htonl(n)).not.toThrow()
251
+ expect(() => swapBytes32(n)).not.toThrow()
252
+ expect(() => realHtonl(n)).not.toThrow()
253
+ }
254
+ })
255
+ })
204
256
  })
@@ -11,6 +11,7 @@ import {
11
11
  toBase58Check,
12
12
  verifyNotNull
13
13
  } from '../../primitives/utils'
14
+ import Point from '../../primitives/Point'
14
15
 
15
16
  describe('utils', () => {
16
17
  it('should convert to array', () => {
@@ -359,3 +360,19 @@ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
359
360
 
360
361
  })
361
362
 
363
+ describe('Point.encode infinity handling', () => {
364
+ it('encodes infinity as 00 (array)', () => {
365
+ const p = new Point(null, null)
366
+ expect(p.encode()).toEqual([0x00])
367
+ })
368
+
369
+ it('encodes infinity as 00 (hex)', () => {
370
+ const p = new Point(null, null)
371
+ expect(p.encode(true, 'hex')).toBe('00')
372
+ })
373
+
374
+ it('does not throw for infinity', () => {
375
+ const p = new Point(null, null)
376
+ expect(() => p.encode()).not.toThrow()
377
+ })
378
+ })