Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan committed Jan 14, 2025
1 parent 26c50f7 commit 6ca03ff
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 93 deletions.
9 changes: 8 additions & 1 deletion yarn-project/foundation/src/abi/decoder.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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());
Expand Down
75 changes: 1 addition & 74 deletions yarn-project/foundation/src/abi/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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.`,
);
});
});
});
});
22 changes: 4 additions & 18 deletions yarn-project/foundation/src/abi/encoder.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 };
}
83 changes: 83 additions & 0 deletions yarn-project/foundation/src/abi/u128.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
43 changes: 43 additions & 0 deletions yarn-project/foundation/src/abi/u128.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 6ca03ff

Please sign in to comment.