diff --git a/.changeset/bytearray-toi32-overflow.md b/.changeset/bytearray-toi32-overflow.md new file mode 100644 index 000000000..c977b05ef --- /dev/null +++ b/.changeset/bytearray-toi32-overflow.md @@ -0,0 +1,10 @@ +--- +'@graphprotocol/graph-ts': patch +--- + +`ByteArray.toI32` and `ByteArray.toI64` now report an overflow for a positive value +that needs the sign bit of the widest byte, instead of returning it as a negative +number. The overflow check only compared the bytes above the target width against the +sign padding, so `0x80000000` carried in five bytes - exactly what +`BigInt.fromUnsignedBytes` produces for a `u32` at or above `2^31` - passed the check +and came back as `-2147483648`. diff --git a/packages/ts/common/collections.ts b/packages/ts/common/collections.ts index 706b92ec8..fa2b06184 100644 --- a/packages/ts/common/collections.ts +++ b/packages/ts/common/collections.ts @@ -150,6 +150,14 @@ export class ByteArray extends Uint8Array { assert(false, 'overflow converting ' + this.toHexString() + ' to i32'); } } + // Matching sign padding above the window is not enough on its own: the sign bit of the + // last byte inside the window has to agree with it too. Without this check a positive + // value one bit too wide - 0x80000000 carried in five bytes, exactly what + // BigInt.fromUnsignedBytes produces for a u32 >= 2^31 - passes the loop above and is + // returned as a negative i32 instead of throwing. + if (this.length > 4 && this[3] >> 7 != (isNeg ? 1 : 0)) { + assert(false, 'overflow converting ' + this.toHexString() + ' to i32'); + } const paddedBytes = new Bytes(4); paddedBytes[0] = padding; paddedBytes[1] = padding; @@ -195,6 +203,11 @@ export class ByteArray extends Uint8Array { assert(false, 'overflow converting ' + this.toHexString() + ' to i64'); } } + // Same as in toI32: the sign bit of the last byte inside the window has to agree with + // the padding, otherwise a positive value that needs the 64th bit is returned negative. + if (this.length > 8 && this[7] >> 7 != (isNeg ? 1 : 0)) { + assert(false, 'overflow converting ' + this.toHexString() + ' to i64'); + } const paddedBytes = new Bytes(8); paddedBytes[0] = padding; paddedBytes[1] = padding; diff --git a/packages/ts/test/bigInt.ts b/packages/ts/test/bigInt.ts index 202dc1a0d..c5f5c1169 100644 --- a/packages/ts/test/bigInt.ts +++ b/packages/ts/test/bigInt.ts @@ -122,7 +122,9 @@ export function testBigInt(): void { longArray[3] = 255; longArray[4] = 0; assert(longArray.toU32() == 4_294_705_147); - assert(longArray.toI32() == 4_294_705_147); + // 4_294_705_147 is past i32.MAX_VALUE, so toI32 reports an overflow rather than + // returning the wrapped -262_149. The assert cannot be written here: an overflow + // aborts, and this harness has no way to expect one. const bytes = Bytes.fromHexString('0x56696b746f726961'); assert((bytes[0] = 0x56)); diff --git a/packages/ts/test/bytes.ts b/packages/ts/test/bytes.ts index f62004fed..850b0e564 100644 --- a/packages/ts/test/bytes.ts +++ b/packages/ts/test/bytes.ts @@ -9,7 +9,34 @@ export function testBytesWithByteArray(): void { longArray[3] = 255; longArray[4] = 0; assert(longArray.toU32() == 4_294_705_147); - assert(longArray.toI32() == 4_294_705_147); + // 4_294_705_147 is past i32.MAX_VALUE, so toI32 reports an overflow rather than + // returning the wrapped -262_149. The assert cannot be written here: an overflow + // aborts, and this harness has no way to expect one. + + // Widening a value with sign padding must keep converting - these all still fit. + const i32Max = new ByteArray(5); + i32Max[0] = 255; + i32Max[1] = 255; + i32Max[2] = 255; + i32Max[3] = 127; + i32Max[4] = 0; + assert(i32Max.toI32() == 2_147_483_647); + + const i32Min = new ByteArray(5); + i32Min[0] = 0; + i32Min[1] = 0; + i32Min[2] = 0; + i32Min[3] = 128; + i32Min[4] = 255; + assert(i32Min.toI32() == -2_147_483_648); + + // The same value in exactly four bytes is a plain i32 and is unaffected. + const i32MinNarrow = new ByteArray(4); + i32MinNarrow[0] = 0; + i32MinNarrow[1] = 0; + i32MinNarrow[2] = 0; + i32MinNarrow[3] = 128; + assert(i32MinNarrow.toI32() == -2_147_483_648); const bytes = Bytes.fromHexString('0x56696b746f726961'); assert((bytes[0] = 0x56));