Skip to content

Commit b4a1649

Browse files
authored
Merge pull request #409 from bsv-blockchain/TOB-19
Fix: Graceful SEC1 Encoding for Point at Infinity (TOB-19)
2 parents 2272f72 + 2e8648d commit b4a1649

File tree

7 files changed

+124
-5
lines changed

7 files changed

+124
-5
lines changed

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8-
- [1.9.22 - 2025-12-04](#1922---2025-12-04)
8+
- [1.9.23 - 2025-12-08](#1923---2025-12-08)
9+
- [1.9.22 - 2025-12-05](#1922---2025-12-04)
910
- [1.9.21 - 2025-12-04](#1921---2025-12-04)
1011
- [1.9.20 - 2025-12-02](#1920---2025-12-02)
1112
- [1.9.19 - 2025-12-02](#1919---2025-12-02)
@@ -195,6 +196,23 @@ All notable changes to this project will be documented in this file. The format
195196

196197
---
197198

199+
## [1.9.23] - 2025-12-08
200+
201+
### Fixed
202+
- Implemented strict infinity normalization for JacobianPoint, ensuring
203+
all infinity representations (`null`, `"0"`, or BigNumber(0)) are treated
204+
canonically and compare equal (TOB-18).
205+
- Gracefully handle encoding of the point at infinity (TOB-19).
206+
`Point.encode()` now returns the SEC1-compliant `0x00` encoding instead
207+
of triggering internal assertions when coordinates are null.
208+
209+
### Security
210+
- Addressed TOB-18 and TOB-19: eliminated assertion failures and ensured
211+
standards-compliant behavior for malformed or edge-case elliptic curve
212+
point objects.
213+
214+
---
215+
198216
### [1.9.22] - 2025-12-04
199217

200218
### Changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "1.9.22",
3+
"version": "1.9.23",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

src/primitives/JacobianPoint.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ export default class JacobianPoint extends BasePoint {
7373
}
7474

7575
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+
}
7684
}
7785

7886
/**
@@ -361,9 +369,18 @@ export default class JacobianPoint extends BasePoint {
361369
return true
362370
}
363371

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+
364382
// x1 * z2^2 == x2 * z1^2
365383
const z2 = this.z.redSqr()
366-
p = p as JacobianPoint
367384
const pz2 = p.z.redSqr()
368385
if (this.x.redMul(pz2).redISub(p.x.redMul(z2)).cmpn(0) !== 0) {
369386
return false

src/primitives/Point.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ export default class Point extends BasePoint {
445445
* const encodedPointHex = aPoint.encode(true, 'hex');
446446
*/
447447
encode (compact: boolean = true, enc?: 'hex'): number[] | string {
448+
if (this.inf) {
449+
if (enc === 'hex') return '00'
450+
return [0x00]
451+
}
448452
const len = this.curve.p.byteLength()
449453
const x = this.getX().toArray('be', len)
450454
let res: number[]

src/primitives/__tests/Curve.unit.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,66 @@ describe('Point codec', () => {
212212
makeShortTest(shortPointOddY)
213213
)
214214
})
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+

src/primitives/__tests/utils.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
toBase58Check,
1212
verifyNotNull
1313
} from '../../primitives/utils'
14+
import Point from '../../primitives/Point'
1415

1516
describe('utils', () => {
1617
it('should convert to array', () => {
@@ -359,3 +360,19 @@ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
359360

360361
})
361362

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+
})

0 commit comments

Comments
 (0)