#3936·fhevm

fix(js-sdk): two range checks validate the wrong bound (assertIsUint128, toClearValueType)

Author: erkancamliCreated Sep 14, 2026Updated Sep 14, 2026

Describe the issue

Two range checks in sdk/js-sdk do not check the range they say they check. Both are one-symbol slips, both contradict a sibling function a few lines away, and both are invisible to the current tests because the negative cases are still rejected by other means.

1. assertIsUint128 validates against the uint256 bound

src/core/base/uint.ts:

export function assertIsUint128(value: unknown, options: ...): asserts value is Uint128 {
  if (!isUint256(value)) {          // isUint128 exists and is correct
    throw new InvalidTypeError({ ..., expectedType: 'uint128' }, options);
  }
}

assertIsUint8, 16, 32, 64 and 256 each call their matching isUintN. Only the 128 case reaches for the 256 guard while still reporting expectedType: 'uint128', so asUint128 returns a Uint128-branded value that is not a uint128.

asUint128(2n ** 200n)          // returns 2n ** 200n, no throw
asUint128(MAX_UINT128 + 1n)    // returns it, no throw
asUint128(-1n)                 // throws, which is why this is easy to miss

2. toClearValueType indexes the bound table with the FHE type name

src/core/handle/FheType.ts, both the euint8/16/32 and the euint64/128/256 arms:

assertIsUint(value, { ...options, max: MAX_UINT_FOR_TYPE[fheTypeName] });

MAX_UINT_FOR_TYPE is keyed by clear type names (uint8, uint16, …), never by FHE type names, so MAX_UINT_FOR_TYPE['euint8'] is undefined, isUint(value, undefined) applies no upper bound, and the check is dead. asClearValueType, forty lines above in the same file, gets it right:

max: MAX_UINT_FOR_TYPE[typeNameFromFheTypeName(fheTypeName)],

So the two functions disagree on the same input:

asClearValueType('euint8', 999)   // throws InvalidTypeError
toClearValueType('euint8', 999)   // returns 999
toClearValueType('euint64', 2n ** 200n)   // returns 2n ** 200n

This contradicts the function's own doc comment, which ends with @throws If value is not the expected JS type or cannot be coerced.

Context

toClearValueType is on the public decryption path: src/core/kms/PublicDecryptionProof-p.ts maps the decoded orderedAbiEncodedClearValues through it, per handle type. To be accurate rather than dramatic: a value that is out of range for its handle type is caught later by createClearValue / asClearValueType, so today the visible effect is a less specific error further downstream rather than a wrong value reaching the caller. The layer that is supposed to catch it simply does not.

assertIsUint128 has no in-tree caller outside asUint128, so that one is latent for now.

Steps to Reproduce or Propose

cd sdk/js-sdk && npm install
npx vitest run --config src/vitest.config.ts

Baseline on main at fa40f7a: 36 files, 910 tests, all green, because nothing exercises the upper bound of either function.

Adding two cases makes the gap visible:

// src/core/base/uint.test.ts
expect(() => assertIsUint128(MAX_UINT128 + 1n, {})).toThrow(InvalidTypeError);

// src/core/handle/FheType.test.ts
expect(() => toClearValueType('euint8', 256)).toThrow(InvalidTypeError);
expect(() => toClearValueType('euint64', 2n ** 64n)).toThrow(InvalidTypeError);
before: 2 failed | 283 passed   (AssertionError: expected function to throw an error, but it didn't)
after:  36 files, 915 tests, all passing

The fix is isUint256 to isUint128 in one place, and wrapping the two MAX_UINT_FOR_TYPE[...] lookups in typeNameFromFheTypeName(...) so they match asClearValueType. I have that patch with the five tests above, verified red then green locally.

Since tfhe-rs states that contributions start from an issue rather than a pull request, I am raising this here first. Happy to open a PR against fhevm with the fix and the tests if you want it, or to split it into two issues if you would rather track them separately.