Skip to content

fix: clear surplus words in notn so result stays below 2 ** width - #319

Merged
fanatid merged 1 commit into
indutny:masterfrom
spokodev:fix/notn-clear-surplus-words
Jun 26, 2026
Merged

fanatid merged 1 commit into
indutny:masterfrom
spokodev:fix/notn-clear-surplus-words

Conversation

@spokodev

Copy link
Copy Markdown
Contributor

When width is smaller than the bit length of the value, notn(width) returns a number larger than 2 ** width.

Repro

new BN("5991996425021").notn(15).toString()
// actual   "5991949171906"
// expected "31938"

new BN("4503599627370497").notn(26).toString()
// actual   "4503599694479358"
// expected "67108862"

Checked against the native BigInt oracle for the same operation:

(~BigInt("5991996425021") & ((1n << 15n) - 1n)).toString() // "31938"

Contract

notn(width) is the w-bit bitwise complement: the result must be < 2 ** width. Single-word values are fine; the bug only shows when the value occupies more 26-bit words than width requires.

Root cause

inotn computes bytesNeeded = Math.ceil(width / 26), inverts the words covering [0, bytesNeeded) plus the residue word, but never clears the words above that range. _expand only grows this.length, it never shrinks, so when the value already has more words than the width needs, those surplus high words are left untouched and survive _strip().

Existing .notn() tests only use width >= bitLength(value), so width < bitLength was never exercised.

Fix

Zero this.words[j] for every j from the first index past the requested width up to this.length, before stripping.

Tests

Added a case to test/binary-test.js .notn() that checks multi-word values with width < bitLength against the BigInt oracle. It fails on master and passes with the fix. Full suite stays green (175 passing, lint clean).

inotn inverts the words covering the requested width but never clears the
words above it. When the value occupies more 26-bit words than the width
requires, those high words survive _strip(), so the result exceeds 2 ** width.

  new BN('5991996425021').notn(15).toString()
  // was '5991949171906', expected '31938'

Zero the words from the first index past the width up to this.length before
stripping, matching the w-bit complement contract that the result is < 2 ** width.
@fanatid
fanatid merged commit dc3429f into indutny:master Jun 26, 2026
2 checks passed
fanatid pushed a commit that referenced this pull request Jun 26, 2026
…th (#319)

inotn inverts the words covering the requested width but never clears the
words above it. When the value occupies more 26-bit words than the width
requires, those high words survive _strip(), so the result exceeds 2 ** width.

  new BN('5991996425021').notn(15).toString()
  // was '5991949171906', expected '31938'

Zero the words from the first index past the width up to this.length before
stripping, matching the w-bit complement contract that the result is < 2 ** width.
@fanatid

fanatid commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks! published in 4.12.4 and 5.2.4

@spokodev
spokodev deleted the fix/notn-clear-surplus-words branch July 8, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants