Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-vlq-uints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/serializer": patch
---

Fix VLQ encoding and decoding of unsigned 32-bit integers at and above 2^31. `writeVLQ` and `readVLQ` used JS bitwise operators, which truncate to 32 bits: `writeUInt(2147483648)` serialized as `0`, `writeUInt(4294967295)` as `127`, and the reference encoding of `4294967295` decoded as `-1`. Both now use arithmetic that covers the full unsigned 32-bit range, and values outside it throw a `RangeError`, matching the reference implementation's `putUInt`/`getUInt` bounds. This backs `SigmaByteWriter.writeUInt` / `SigmaByteReader.readUInt`, so box creation heights, ErgoTree sizes, and length prefixes in that range no longer corrupt silently.
2 changes: 2 additions & 0 deletions packages/serializer/src/coders/numRanges.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const MIN_U8 = 0x00;
export const MAX_U8 = 0xff;

export const MAX_U32 = 0xffffffff;

export const MIN_I8 = -0x80;
export const MAX_I8 = 0x7f;

Expand Down
20 changes: 18 additions & 2 deletions packages/serializer/src/coders/vlq.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ describe("VLQ encoding/decoding", () => {
{ uint: 2097151, bytes: Uint8Array.from([0xff, 0xff, 0x7f]) },
{ uint: 2097152, bytes: Uint8Array.from([0x80, 0x80, 0x80, 0x01]) },
{ uint: 268435455, bytes: Uint8Array.from([0xff, 0xff, 0xff, 0x7f]) },
{ uint: 268435456, bytes: Uint8Array.from([0x80, 0x80, 0x80, 0x80, 0x01]) }
{ uint: 268435456, bytes: Uint8Array.from([0x80, 0x80, 0x80, 0x80, 0x01]) },
{ uint: 2147483647, bytes: Uint8Array.from([0xff, 0xff, 0xff, 0xff, 0x07]) },
{ uint: 2147483648, bytes: Uint8Array.from([0x80, 0x80, 0x80, 0x80, 0x08]) },
{ uint: 4294967295, bytes: Uint8Array.from([0xff, 0xff, 0xff, 0xff, 0x0f]) }
];

function toVLQBytes(value: number) {
Expand All @@ -34,10 +37,23 @@ describe("VLQ encoding/decoding", () => {
}).toThrow();
});

it("Should fail trying to encode values above the unsigned 32-bit range", () => {
expect(() => {
toVLQBytes(4294967296);
}).toThrow(RangeError);
});

test.each(testVectors)("Should decode", (tv) => {
expect(readVLQ(new SigmaByteReader(tv.bytes))).toEqual(tv.uint);
});

it("Should fail trying to decode values above the unsigned 32-bit range", () => {
// 2^32, encoded as VLQ
expect(() => {
readVLQ(new SigmaByteReader(Uint8Array.from([0x80, 0x80, 0x80, 0x80, 0x10])));
}).toThrow(RangeError);
});

it("Should decode empty Buffer to 0", () => {
expect(readVLQ(new SigmaByteReader(Uint8Array.from([])))).toEqual(0);
});
Expand All @@ -50,7 +66,7 @@ describe("VLQ encoding/decoding", () => {

it("Should encode/decode radom numbers", () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 100000000 }), (n) => {
fc.property(fc.integer({ min: 0, max: 4294967295 }), (n) => {
expect(readVLQ(new SigmaByteReader(toVLQBytes(n)))).to.be.equal(n);
})
);
Expand Down
18 changes: 15 additions & 3 deletions packages/serializer/src/coders/vlq.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { _0n, _7n, _127n, _128n, ensureBigInt } from "@fleet-sdk/common";
import { MAX_U32 } from "./numRanges";
import type { SigmaByteReader } from "./sigmaByteReader";
import type { SigmaByteWriter } from "./sigmaByteWriter";

Expand All @@ -22,11 +23,16 @@ export function writeVLQ(writer: SigmaByteWriter, value: number): SigmaByteWrite
if (value < 0) {
throw new RangeError("Variable Length Quantity not supported for negative numbers.");
}
if (value > MAX_U32) {
throw new RangeError(`Value ${value} is out of range for an unsigned 32-bit integer`);
}

// Divide instead of shifting: JS bitwise operators truncate to 32 bits, so
// `>>` corrupts every value >= 2^31.
let val = value;
do {
let lower7bits = val & 0x7f;
val >>= 7;
let lower7bits = val % 128;
val = Math.floor(val / 128);
if (val > 0) lower7bits |= 0x80;
writer.write(lower7bits);
} while (val > 0);
Expand All @@ -50,10 +56,16 @@ export function readVLQ(reader: SigmaByteReader): number {

do {
lower7bits = reader.readByte();
value |= (lower7bits & 0x7f) << shift;
// Multiply instead of shifting: JS bitwise operators truncate to 32 bits,
// so `<<` corrupts every value >= 2^31.
value += (lower7bits & 0x7f) * 2 ** shift;
shift += 7;
} while ((lower7bits & 0x80) !== 0);

if (value > MAX_U32) {
throw new RangeError(`Value ${value} is out of range for an unsigned 32-bit integer`);
}

return value;
}

Expand Down