fix(js-sdk): two range checks validate the wrong bound (assertIsUint128, toClearValueType)
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.
Source: zama-ai/fhevm