From 6ca03ffc6b477c64d0cd8ce475ec84d2cd87d29b Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 14 Jan 2025 21:12:16 +0000 Subject: [PATCH] WIP --- yarn-project/foundation/src/abi/decoder.ts | 9 +- .../foundation/src/abi/encoder.test.ts | 75 +---------------- yarn-project/foundation/src/abi/encoder.ts | 22 +---- yarn-project/foundation/src/abi/u128.test.ts | 83 +++++++++++++++++++ yarn-project/foundation/src/abi/u128.ts | 43 ++++++++++ 5 files changed, 139 insertions(+), 93 deletions(-) create mode 100644 yarn-project/foundation/src/abi/u128.test.ts create mode 100644 yarn-project/foundation/src/abi/u128.ts diff --git a/yarn-project/foundation/src/abi/decoder.ts b/yarn-project/foundation/src/abi/decoder.ts index d94f490509ec..2d1ec6a4637d 100644 --- a/yarn-project/foundation/src/abi/decoder.ts +++ b/yarn-project/foundation/src/abi/decoder.ts @@ -1,7 +1,8 @@ import { AztecAddress } from '../aztec-address/index.js'; import { type Fr } from '../fields/index.js'; import { type ABIParameter, type ABIVariable, type AbiType } from './abi.js'; -import { isAztecAddressStruct, parseSignedInt } from './utils.js'; +import { U128 } from './u128.js'; +import { isAztecAddressStruct, isU128Struct, parseSignedInt } from './utils.js'; /** * The type of our decoded ABI. @@ -43,6 +44,12 @@ class AbiDecoder { return array; } case 'struct': { + if (isU128Struct(abiType)) { + const lo = this.decodeNext({ kind: 'field' }) as bigint; + const hi = this.decodeNext({ kind: 'field' }) as bigint; + return U128.fromU64sLE(lo, hi).toInteger(); + } + const struct: { [key: string]: AbiDecoded } = {}; if (isAztecAddressStruct(abiType)) { return new AztecAddress(this.getNextField().toBuffer()); diff --git a/yarn-project/foundation/src/abi/encoder.test.ts b/yarn-project/foundation/src/abi/encoder.test.ts index c72e74f5b1a9..0b901d030efa 100644 --- a/yarn-project/foundation/src/abi/encoder.test.ts +++ b/yarn-project/foundation/src/abi/encoder.test.ts @@ -4,7 +4,7 @@ import { Point } from '../fields/point.js'; import { jsonParseWithSchema, jsonStringify } from '../json-rpc/convert.js'; import { schemas } from '../schemas/schemas.js'; import { type FunctionAbi, FunctionType } from './abi.js'; -import { convertToU128Limbs, encodeArguments } from './encoder.js'; +import { encodeArguments } from './encoder.js'; describe('abi/encoder', () => { it('serializes fields as fields', () => { @@ -235,77 +235,4 @@ describe('abi/encoder', () => { expect(() => encodeArguments(testFunctionAbi, args)).toThrow(/Invalid hex-encoded string/); }); - - describe('convertToU128Limbs', () => { - it('converts 0 correctly', () => { - const result = convertToU128Limbs(0); - expect(result.lo).toBe(0n); - expect(result.hi).toBe(0n); - }); - - it('converts maximum 128-bit value correctly', () => { - const maxValue = 2n ** 128n - 1n; - const result = convertToU128Limbs(maxValue); - expect(result.lo).toBe(BigInt('0xFFFFFFFFFFFFFFFF')); - expect(result.hi).toBe(BigInt('0xFFFFFFFFFFFFFFFF')); - }); - - it('converts value with only low bits set', () => { - const value = BigInt('0xABCDEF0123456789'); - const result = convertToU128Limbs(value); - expect(result.lo).toBe(BigInt('0xABCDEF0123456789')); - expect(result.hi).toBe(0n); - }); - - it('converts value with both high and low bits set', () => { - const hi = BigInt('0xFEDCBA9876543210'); - const lo = BigInt('0x1234567890ABCDEF'); - const value = (hi << 64n) | lo; - const result = convertToU128Limbs(value); - expect(result.lo).toBe(lo); - expect(result.hi).toBe(hi); - }); - - it('converts number type inputs correctly', () => { - const result = convertToU128Limbs(12345); - expect(result.lo).toBe(12345n); - expect(result.hi).toBe(0n); - }); - - it('handles edge cases near power of 2 boundaries', () => { - // Test just below 2^64 - const nearMax64 = 2n ** 64n - 1n; - const result1 = convertToU128Limbs(nearMax64); - expect(result1.lo).toBe(BigInt('0xFFFFFFFFFFFFFFFF')); - expect(result1.hi).toBe(0n); - - // Test just above 2^64 - const justAbove64 = 2n ** 64n + 1n; - const result2 = convertToU128Limbs(justAbove64); - expect(result2.lo).toBe(1n); - expect(result2.hi).toBe(1n); - }); - - describe('error cases', () => { - it('throws error for negative values', () => { - expect(() => convertToU128Limbs(-1)).toThrow( - 'Value -1 is not within 128 bits and hence cannot be converted to U128 limbs.', - ); - }); - - it('throws error for values >= 2^128', () => { - const tooLarge = 2n ** 128n; - expect(() => convertToU128Limbs(tooLarge)).toThrow( - `Value ${tooLarge} is not within 128 bits and hence cannot be converted to U128 limbs.`, - ); - }); - - it('throws error for maximum safe integer edge case', () => { - const maxSafe = 2n ** 128n + 1n; - expect(() => convertToU128Limbs(maxSafe)).toThrow( - `Value ${maxSafe} is not within 128 bits and hence cannot be converted to U128 limbs.`, - ); - }); - }); - }); }); diff --git a/yarn-project/foundation/src/abi/encoder.ts b/yarn-project/foundation/src/abi/encoder.ts index efc5d14d9f9a..6173663f6226 100644 --- a/yarn-project/foundation/src/abi/encoder.ts +++ b/yarn-project/foundation/src/abi/encoder.ts @@ -1,5 +1,6 @@ import { Fr } from '../fields/index.js'; import { type AbiType, type FunctionAbi } from './abi.js'; +import { U128 } from './u128.js'; import { isAddressStruct, isFunctionSelectorStruct, isU128Struct, isWrappedFieldStruct } from './utils.js'; /** @@ -108,9 +109,9 @@ class ArgumentEncoder { if (isU128Struct(abiType)) { // U128 struct has low and high limbs - so we first convert the value to the 2 limbs and then we encode them // --> this results in the limbs being added to the final flat array as [..., lo, hi, ...]. - const { lo, hi } = convertToU128Limbs(arg); - this.encodeArgument({ kind: 'field' }, lo, `${name}.lo`); - this.encodeArgument({ kind: 'field' }, hi, `${name}.hi`); + const value = new U128(arg); + this.encodeArgument({ kind: 'field' }, value.lo, `${name}.lo`); + this.encodeArgument({ kind: 'field' }, value.hi, `${name}.hi`); break; } if (isWrappedFieldStruct(abiType)) { @@ -166,18 +167,3 @@ export function encodeArguments(abi: FunctionAbi, args: any[]) { export function countArgumentsSize(abi: FunctionAbi) { return abi.parameters.reduce((acc, parameter) => acc + ArgumentEncoder.typeSize(parameter.type), 0); } - -export function convertToU128Limbs(value: bigint | number): { lo: bigint; hi: bigint } { - if (typeof value === 'number') { - value = BigInt(value); - } - - // Check value is within 128 bits - if (value < 0 || value >= 2n ** 128n) { - throw new Error(`Value ${value} is not within 128 bits and hence cannot be converted to U128 limbs.`); - } - - const lo = value & BigInt('0xFFFFFFFFFFFFFFFF'); - const hi = value >> BigInt(64); - return { lo, hi }; -} diff --git a/yarn-project/foundation/src/abi/u128.test.ts b/yarn-project/foundation/src/abi/u128.test.ts new file mode 100644 index 000000000000..0c18baba722e --- /dev/null +++ b/yarn-project/foundation/src/abi/u128.test.ts @@ -0,0 +1,83 @@ +import { U128 } from './u128.js'; + +describe('U128', () => { + describe('constructor', () => { + it('accepts valid number inputs', () => { + const small = new U128(42); + expect(small.toInteger()).toBe(42n); + + const large = new U128(Number.MAX_SAFE_INTEGER); + expect(large.toInteger()).toBe(BigInt(Number.MAX_SAFE_INTEGER)); + }); + + it('accepts valid bigint inputs', () => { + const small = new U128(42n); + expect(small.toInteger()).toBe(42n); + + const max = new U128(2n ** 128n - 1n); + expect(max.toInteger()).toBe(2n ** 128n - 1n); + }); + + it('throws for negative values', () => { + expect(() => new U128(-1)).toThrow('Value -1 is not within 128 bits'); + expect(() => new U128(-1n)).toThrow('Value -1 is not within 128 bits'); + }); + + it('throws for values >= 2^128', () => { + const tooLarge = 2n ** 128n; + expect(() => new U128(tooLarge)).toThrow(`Value ${tooLarge} is not within 128 bits`); + }); + }); + + describe('fromU64sLE', () => { + it('correctly combines valid limbs', () => { + const lo = 0xdeadbeefn; + const hi = 0xcafebaben; + const combined = U128.fromU64sLE(lo, hi); + + expect(combined.lo).toBe(lo); + expect(combined.hi).toBe(hi); + expect(combined.toInteger()).toBe((hi << 64n) | lo); + }); + + it('accepts maximum valid limb values', () => { + const maxLimb = 2n ** 64n - 1n; + const value = U128.fromU64sLE(maxLimb, maxLimb); + + expect(value.lo).toBe(maxLimb); + expect(value.hi).toBe(maxLimb); + expect(value.toInteger()).toBe(2n ** 128n - 1n); + }); + + it('throws for invalid lower limb', () => { + const invalid = 2n ** 64n; + expect(() => U128.fromU64sLE(invalid, 0n)).toThrow(`Lower limb ${invalid} is not within valid range`); + + expect(() => U128.fromU64sLE(-1n, 0n)).toThrow('Lower limb -1 is not within valid range'); + }); + + it('throws for invalid higher limb', () => { + const invalid = 2n ** 64n; + expect(() => U128.fromU64sLE(0n, invalid)).toThrow(`Higher limb ${invalid} is not within valid range`); + + expect(() => U128.fromU64sLE(0n, -1n)).toThrow('Higher limb -1 is not within valid range'); + }); + }); + + describe('getters', () => { + it('correctly extracts lo and hi components', () => { + const testCases = [ + { lo: 0xdeadbeefn, hi: 0xcafebaben }, + { lo: 0n, hi: 1n }, + { lo: 1n, hi: 0n }, + { lo: 2n ** 64n - 1n, hi: 2n ** 64n - 1n }, + ]; + + for (const { lo, hi } of testCases) { + const value = U128.fromU64sLE(lo, hi); + expect(value.lo).toBe(lo); + expect(value.hi).toBe(hi); + } + }); + }); +}); diff --git a/yarn-project/foundation/src/abi/u128.ts b/yarn-project/foundation/src/abi/u128.ts new file mode 100644 index 000000000000..4a0431bac074 --- /dev/null +++ b/yarn-project/foundation/src/abi/u128.ts @@ -0,0 +1,43 @@ +// A typescript version of noir::std::U128 +export class U128 { + private readonly value: bigint; + + constructor(value: bigint | number) { + if (typeof value === 'number') { + value = BigInt(value); + } + + // Check value is within 128 bits + if (value < 0n || value >= 2n ** 128n) { + throw new Error(`Value ${value} is not within 128 bits and hence cannot be converted to U128.`); + } + + this.value = value; + } + + static fromU64sLE(lo: bigint, hi: bigint): U128 { + // Validate limbs are within valid ranges + if (lo < 0n || lo >= 2n ** 64n) { + throw new Error(`Lower limb ${lo} is not within valid range (0 to 2^64-1)`); + } + if (hi < 0n || hi >= 2n ** 64n) { + throw new Error(`Higher limb ${hi} is not within valid range (0 to 2^64-1)`); + } + + // Combine limbs into full value and create new instance + const value = (hi << 64n) | lo; + return new U128(value); + } + + get lo(): bigint { + return this.value & 0xffffffffffffffffn; + } + + get hi(): bigint { + return this.value >> 64n; + } + + toInteger(): bigint { + return this.value; + } +}