Skip to content

fix(serializer): encode and decode VLQ unsigned ints over the full 32-bit range - #222

Open
Kshot3000 wants to merge 1 commit into
fleet-sdk:masterfrom
Kshot3000:fix/vlq-uint-range
Open

Kshot3000 wants to merge 1 commit into
fleet-sdk:masterfrom
Kshot3000:fix/vlq-uint-range

Conversation

@Kshot3000

Copy link
Copy Markdown

Hi, I'm Kyle Cox (@Kshot9000), an independent developer building on Ergo. I found this while auditing the serializer's number codecs against the reference implementation (scorex-util's VLQWriter/VLQReader, which sigmastate uses for putUInt/getUInt).

The bug

writeVLQ / readVLQ in packages/serializer/src/coders/vlq.ts did their math with JS bitwise operators (>>, <<, |), which truncate operands to signed 32 bits. Everything below 2^31 works, which is why the existing tests (vectors up to 2^28, randoms up to 10^8) never caught it — but at and above 2^31 the codec silently corrupts values:

call before reference behaviour
writeVLQ(2147483648) (2^31) writes [0x00] — the value 0 writes [0x80, 0x80, 0x80, 0x80, 0x08]
writeVLQ(4294967295) (2^32 − 1) writes [0x7f] — the value 127 writes [0xff, 0xff, 0xff, 0xff, 0x0f]
readVLQ([0xff, 0xff, 0xff, 0xff, 0x0f]) returns −1 returns 4294967295
readVLQ of the 2^32 encoding returns 0 getUInt rejects it (out of unsigned int range)
writeVLQ(4294967296) (2^32) writes a wrong in-range value putUInt rejects it

These functions back SigmaByteWriter.writeUInt / SigmaByteReader.readUInt, which serialize box creation heights, the ErgoTree size prefix, AVL tree key/value lengths, and every array length and count prefix — so any UInt in the upper half of its range was corrupted on write and misread on parse, with no error either way.

The fix

  • writeVLQ: shift with division (% 128 / Math.floor(val / 128)) instead of bitwise operators, and throw a RangeError for values above MAX_U32 (new constant in numRanges.ts), matching putUInt's require(x >= 0 && x <= 0xFFFFFFFF).
  • readVLQ: accumulate with multiplication (* 2 ** shift) instead of <<, and throw a RangeError when the decoded value exceeds MAX_U32, matching getUInt.
  • Encodings below 2^31 produce byte-identical output to before; readBigVLQ/writeBigVLQ (the 64-bit path) already used bigint math and are untouched.

Tests

Tests-first: the new cases fail on the old code (7 failures) and pass with the fix.

  • New encode/decode vectors at the boundary: 2^31 − 1, 2^31, and 2^32 − 1, with the reference byte encodings.
  • Encode of 2^32 and decode of the 2^32 encoding now throw RangeError.
  • The round-trip property test's range is widened from 10^8 to the full 0 … 4294967295.
  • Full monorepo suite: 1140 tests passing (66 files); serializer package 381/381; biome check clean on the changed files.

Changeset included (@fleet-sdk/serializer: patch). Happy to adjust anything — thanks for maintaining Fleet.

— Kyle

@changeset-bot

changeset-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 973cc07

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@fleet-sdk/serializer Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

1 participant