diff --git a/packages/noir-ethereum-api/src/arrays.ts b/packages/noir-ethereum-api/src/arrays.ts index 39cae0854..4aab750b0 100644 --- a/packages/noir-ethereum-api/src/arrays.ts +++ b/packages/noir-ethereum-api/src/arrays.ts @@ -1,4 +1,5 @@ import { assert } from './assert.js'; +import { incHexByte } from './utils/string.js'; type PaddingDirection = 'left' | 'right'; @@ -18,8 +19,3 @@ export const alterArray = function (array: readonly string[]): string[] { assert(array.length > 0, 'Array should not be empty'); return [incHexByte(array[0]), ...array.slice(1)]; }; - -function incHexByte(hexByte: string): string { - const newByte = ((parseInt(hexByte) + 1) % 256).toString(16); - return '0x' + newByte; -} diff --git a/packages/noir-ethereum-api/src/ethereum/mockClient.ts b/packages/noir-ethereum-api/src/ethereum/mockClient.ts index 4338f4cb2..c7d4ce85d 100644 --- a/packages/noir-ethereum-api/src/ethereum/mockClient.ts +++ b/packages/noir-ethereum-api/src/ethereum/mockClient.ts @@ -4,13 +4,17 @@ import { assert } from '../assert.js'; import { readObject } from '../utils/file.js'; import { mock } from '../utils/mock.js'; import isEqual from 'lodash.isequal'; +import { identity } from '../utils/function.js'; -export async function createMockClient(filePath: string): Promise { +export async function createMockClient( + filePath: string, + resultModifier: (call: Call) => Call = identity +): Promise { const savedCalls = await readObject(filePath); return mock(isEthereumApiMethod, (method: string, args: object): object => { const call: Call | undefined = savedCalls.find((it) => it.method === method && isEqual(it.arguments, args)); assert(!!call, `call not found for: ${method}(${args})`); - return call.result; + return resultModifier(call).result; }); } diff --git a/packages/noir-ethereum-api/src/noir/oracles/oracles.ts b/packages/noir-ethereum-api/src/noir/oracles/oracles.ts index 13d9c00bd..5c641b1fc 100644 --- a/packages/noir-ethereum-api/src/noir/oracles/oracles.ts +++ b/packages/noir-ethereum-api/src/noir/oracles/oracles.ts @@ -2,6 +2,7 @@ import { type ForeignCallOutput } from '@noir-lang/noir_js'; import { createDefaultClient } from '../../ethereum/client.js'; import { getAccountOracle } from './accountOracles.js'; import { type PublicClient } from 'viem'; +import { getHeaderOracle } from './headerOracle.js'; export type NoirArguments = string[][]; @@ -22,4 +23,9 @@ export const createOracles = return await fn(client, args); }; -export const defaultOracles = createOracles(createDefaultClient())({ getAccountOracle }); +export const defaultOraclesMap = { + get_account: getAccountOracle, + get_header: getHeaderOracle +}; + +export const defaultOracles = createOracles(createDefaultClient())(defaultOraclesMap); diff --git a/packages/noir-ethereum-api/src/utils/function.ts b/packages/noir-ethereum-api/src/utils/function.ts new file mode 100644 index 000000000..8aa070f5f --- /dev/null +++ b/packages/noir-ethereum-api/src/utils/function.ts @@ -0,0 +1 @@ +export const identity = (it: T): T => it; diff --git a/packages/noir-ethereum-api/src/utils/object.ts b/packages/noir-ethereum-api/src/utils/object.ts new file mode 100644 index 000000000..01cea569a --- /dev/null +++ b/packages/noir-ethereum-api/src/utils/object.ts @@ -0,0 +1,8 @@ +export function updateNestedField(obj: T, pathArray: string[], updater: (value: V) => V): void { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + pathArray.reduce((acc: any, key: string, i: number) => { + if (acc[key] === undefined) acc[key] = {}; + if (i === pathArray.length - 1) acc[key] = updater(acc[key]); + return acc[key]; + }, obj); +} diff --git a/packages/noir-ethereum-api/src/utils/string.ts b/packages/noir-ethereum-api/src/utils/string.ts new file mode 100644 index 000000000..66867e1cc --- /dev/null +++ b/packages/noir-ethereum-api/src/utils/string.ts @@ -0,0 +1,8 @@ +export function incHexStr(hexStr: string): string { + return '0x' + (BigInt(hexStr) + 1n).toString(16); +} + +export function incHexByte(hexByte: string): string { + const newByte = ((parseInt(hexByte) + 1) % 256).toString(16); + return '0x' + newByte; +} diff --git a/packages/noir-ethereum-api/test/e2e.test.ts b/packages/noir-ethereum-api/test/e2e.test.ts index 66b695e6d..521620315 100644 --- a/packages/noir-ethereum-api/test/e2e.test.ts +++ b/packages/noir-ethereum-api/test/e2e.test.ts @@ -1,15 +1,15 @@ -import { describe, expect, it } from 'vitest'; -import { generateAndVerifyStorageProof, type MainInputs } from '../src/main.js'; -import { encodeAddress } from '../src/noir/encode.js'; -import { createOracles, type Oracles } from '../src/noir/oracles/oracles.js'; -import accountWithProofJSON from './fixtures/accountWithProof.json'; -import { AccountWithProof, expectCircuitFail, type FieldsOfType, serializeAccountWithProof } from './helpers.js'; -import { blockHeaders } from './fixtures/blockHeader.json'; -import { encodeBlockHeaderPartial } from '../src/noir/oracles/headerOracle.js'; -import { type BlockHeader } from '../src/ethereum/blockHeader.js'; +import { PublicClient } from 'viem'; +import { beforeAll, describe, expect, it } from 'vitest'; import { alterArray } from '../src/arrays.js'; +import { incHexStr } from '../src/utils/string.js'; import { createMockClient } from '../src/ethereum/mockClient.js'; +import { Call } from '../src/ethereum/recordingClient.js'; +import { generateAndVerifyStorageProof, type MainInputs } from '../src/main.js'; +import { encodeAddress } from '../src/noir/encode.js'; +import { createOracles, defaultOraclesMap, type Oracles } from '../src/noir/oracles/oracles.js'; import { ADDRESS } from './ethereum/recordingClient.test.js'; +import { expectCircuitFail } from './helpers.js'; +import { updateNestedField } from '../src/utils/object.js'; const defaultTestCircuitInputParams: MainInputs = { block_no: 14194126, @@ -20,33 +20,35 @@ const defaultTestCircuitInputParams: MainInputs = { '0xda', '0xc4', '0x8e', '0x2b', '0x4'] }; +function alterCall(callName: string, path: string[]) { + return (call: Call): Call => { + if (call.method === callName) { + updateNestedField(call, path, incHexStr); + } + return call; + }; +} describe( 'e2e', - async () => { - async function oracles(accountWithProof: AccountWithProof = accountWithProofJSON): Promise { - return createOracles(await createMockClient('./test/fixtures/mockClientData.json'))({ - get_account: async () => serializeAccountWithProof(accountWithProof), - get_header: async () => encodeBlockHeaderPartial(blockHeaders[1].header as BlockHeader) - }); - } + () => { + let client: PublicClient; + let oracles: Oracles; + + beforeAll(async () => { + client = await createMockClient('./test/fixtures/mockClientData.json'); + oracles = createOracles(client)(defaultOraclesMap); + }); it('proof successes', async () => { - expect(await generateAndVerifyStorageProof(defaultTestCircuitInputParams, await oracles())).toEqual(true); + expect(await generateAndVerifyStorageProof(defaultTestCircuitInputParams, oracles)).toEqual(true); }); - const arrayKeys: Array> = ['key', 'value', 'proof']; - arrayKeys.forEach((arrayField) => { - it(`proof fails: invalid field: ${arrayField}`, async () => { - await expectCircuitFail( - generateAndVerifyStorageProof( - defaultTestCircuitInputParams, - await oracles({ - ...accountWithProofJSON, - [arrayField]: alterArray(accountWithProofJSON[arrayField]) - }) - ) - ); - }); + it('proof fails: invalid proof', async () => { + const resultModifier = alterCall('getProof', ['result', 'accountProof', '0']); + const oracles = createOracles(await createMockClient('./test/fixtures/mockClientData.json', resultModifier))( + defaultOraclesMap + ); + await expectCircuitFail(generateAndVerifyStorageProof(defaultTestCircuitInputParams, oracles)); }); it('proof fails: invalid state root', async () => { @@ -54,7 +56,7 @@ describe( ...defaultTestCircuitInputParams, state_root: alterArray(defaultTestCircuitInputParams.state_root) }; - await expectCircuitFail(generateAndVerifyStorageProof(inputParams, await oracles())); + await expectCircuitFail(generateAndVerifyStorageProof(inputParams, oracles)); }); }, { diff --git a/packages/noir-ethereum-api/test/utils/object.test.ts b/packages/noir-ethereum-api/test/utils/object.test.ts new file mode 100644 index 000000000..3b1c2443a --- /dev/null +++ b/packages/noir-ethereum-api/test/utils/object.test.ts @@ -0,0 +1,17 @@ +import { updateNestedField } from '../../src/utils/object.js'; +import { describe, it, expect } from 'vitest'; + +describe('updateNestedField', () => { + it('existing key', () => { + const object = { a: [{ bar: { c: 3 } }] }; + updateNestedField(object, ['a', '0', 'bar', 'c'], (x: number) => x + 1); + expect(object.a[0].bar.c).to.eq(4); + }); + + it('non-existing key', () => { + const object = { a: [{ bar: { c: 3 } }] }; + updateNestedField(object, ['x', '0', 'y', 'z'], () => 5); + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + expect((object as any).x[0]?.y?.z).to.eq(5); + }); +}); diff --git a/packages/noir-ethereum-api/test/utils/string.test.ts b/packages/noir-ethereum-api/test/utils/string.test.ts new file mode 100644 index 000000000..51049cee7 --- /dev/null +++ b/packages/noir-ethereum-api/test/utils/string.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { incHexByte, incHexStr } from '../../src/utils/string.js'; + +describe('incHexStr', () => { + it('zero', () => { + expect(incHexStr('0x0')).toEqual('0x1'); + }); + + it('small', () => { + expect(incHexStr('0x10')).toEqual('0x11'); + }); + + it('big', () => { + expect(incHexStr('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1')).toEqual( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2' + ); + }); +}); + +describe('incHexByte', () => { + it('zero', () => { + expect(incHexByte('0x0')).toEqual('0x1'); + }); + + it('mid size', () => { + expect(incHexByte('0xaa')).toEqual('0xab'); + expect(incHexByte('0xAA')).toEqual('0xab'); + }); + + it('oveflow', () => { + expect(incHexByte('0xFF')).toEqual('0x0'); + }); +});