From 7c88435e6aadfcac7e56465e15972892c6315a37 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 1 Nov 2024 20:00:01 -0300 Subject: [PATCH] Fix for parsing coerced schemas --- yarn-project/circuit-types/src/body.ts | 13 ++ .../circuit-types/src/interfaces/archiver.ts | 168 ++++++++++++++++++ yarn-project/circuit-types/src/l2_block.ts | 20 +++ .../circuit-types/src/l2_block_source.ts | 31 ++-- .../src/logs/encrypted_l2_note_log.ts | 11 ++ .../src/logs/extended_unencrypted_l2_log.ts | 19 ++ .../src/logs/get_unencrypted_logs_response.ts | 22 +-- .../circuit-types/src/logs/log_filter.ts | 15 +- yarn-project/circuit-types/src/logs/log_id.ts | 21 +++ .../src/logs/unencrypted_l2_log.ts | 12 ++ .../circuit-types/src/notes/extended_note.ts | 13 ++ .../circuit-types/src/public_data_write.ts | 16 ++ yarn-project/circuit-types/src/tx/tx_hash.ts | 9 +- .../circuit-types/src/tx/tx_receipt.ts | 72 +++++--- yarn-project/circuit-types/src/tx_effect.ts | 13 +- yarn-project/circuits.js/package.json | 3 +- .../src/contract/interfaces/contract_class.ts | 114 +++++++++--- .../src/structs/content_commitment.ts | 25 +++ .../circuits.js/src/structs/gas_fees.ts | 11 ++ .../src/structs/global_variables.ts | 17 ++ .../circuits.js/src/structs/header.ts | 24 +++ .../src/structs/parity/root_parity_input.ts | 2 +- .../src/structs/partial_state_reference.ts | 23 +++ .../src/structs/recursive_proof.ts | 2 +- .../rollup/append_only_tree_snapshot.ts | 15 ++ .../src/structs/state_reference.ts | 14 ++ yarn-project/foundation/package.json | 2 +- yarn-project/foundation/src/abi/abi.ts | 50 +++--- .../foundation/src/aztec-address/index.ts | 4 + .../foundation/src/buffer/buffer32.ts | 5 + yarn-project/foundation/src/fields/fields.ts | 1 + .../src/json-rpc/fixtures/test_state.ts | 15 +- .../json-rpc/server/safe_json_rpc_server.ts | 7 +- .../src/json-rpc/test/integration.test.ts | 18 ++ .../src/json-rpc/test/integration.ts | 4 +- yarn-project/foundation/src/schemas/api.ts | 14 +- yarn-project/foundation/src/schemas/hex.ts | 9 - yarn-project/foundation/src/schemas/index.ts | 2 +- .../foundation/src/schemas/parse.test.ts | 54 ++++++ yarn-project/foundation/src/schemas/parse.ts | 22 +++ .../foundation/src/schemas/schemas.ts | 53 ++++-- yarn-project/foundation/src/schemas/utils.ts | 23 +++ yarn-project/yarn.lock | 1 + 43 files changed, 844 insertions(+), 145 deletions(-) create mode 100644 yarn-project/circuit-types/src/interfaces/archiver.ts delete mode 100644 yarn-project/foundation/src/schemas/hex.ts create mode 100644 yarn-project/foundation/src/schemas/parse.test.ts create mode 100644 yarn-project/foundation/src/schemas/utils.ts diff --git a/yarn-project/circuit-types/src/body.ts b/yarn-project/circuit-types/src/body.ts index dcc41c060b6c..fef5d421bd72 100644 --- a/yarn-project/circuit-types/src/body.ts +++ b/yarn-project/circuit-types/src/body.ts @@ -8,6 +8,7 @@ import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { computeUnbalancedMerkleRoot } from '@aztec/foundation/trees'; import { inspect } from 'util'; +import { z } from 'zod'; export class Body { constructor(public txEffects: TxEffect[]) { @@ -18,6 +19,18 @@ export class Body { }); } + static get schema() { + return z + .object({ + txEffects: z.array(TxEffect.schema), + }) + .transform(({ txEffects }) => new Body(txEffects)); + } + + toJSON() { + return { txEffects: this.txEffects }; + } + /** * Serializes a block body * @returns A serialized L2 block body. diff --git a/yarn-project/circuit-types/src/interfaces/archiver.ts b/yarn-project/circuit-types/src/interfaces/archiver.ts new file mode 100644 index 000000000000..01bb9982fd78 --- /dev/null +++ b/yarn-project/circuit-types/src/interfaces/archiver.ts @@ -0,0 +1,168 @@ +import { + AztecAddress, + ContractClassPublic, + ContractClassPublicSchema, + ContractDataSource, + ContractInstanceWithAddress, + EthAddress, + Fr, + FunctionSelector, + Header, + PublicFunction, + PublicFunctionSchema, +} from '@aztec/circuits.js'; +import { ContractArtifact } from '@aztec/foundation/abi'; +import { ApiSchemaFor, schemas } from '@aztec/foundation/schemas'; + +import { z } from 'zod'; + +import { L2Block } from '../l2_block.js'; +import { L2BlockSource, L2Tips, L2TipsSchema } from '../l2_block_source.js'; +import { EncryptedL2NoteLog } from '../logs/encrypted_l2_note_log.js'; +import { GetUnencryptedLogsResponse, GetUnencryptedLogsResponseSchema } from '../logs/get_unencrypted_logs_response.js'; +import { L2BlockL2Logs } from '../logs/l2_block_l2_logs.js'; +import { L2LogsSource } from '../logs/l2_logs_source.js'; +import { LogFilter, LogFilterSchema } from '../logs/log_filter.js'; +import { FromLogType, LogType } from '../logs/log_type.js'; +import { L1ToL2MessageSource } from '../messaging/l1_to_l2_message_source.js'; +import { TxHash } from '../tx/tx_hash.js'; +import { TxReceipt } from '../tx/tx_receipt.js'; +import { TxEffect } from '../tx_effect.js'; + +export type ArchiverApi = Omit< + L2BlockSource & L2LogsSource & ContractDataSource & L1ToL2MessageSource, + 'start' | 'stop' +>; + +class Foo implements ArchiveSource { + getRollupAddress(): Promise { + throw new Error('Method not implemented.'); + } + getRegistryAddress(): Promise { + throw new Error('Method not implemented.'); + } + getBlockNumber(): Promise; + getBlockNumber(): Promise { + throw new Error('Method not implemented.'); + } + getProvenBlockNumber(): Promise { + throw new Error('Method not implemented.'); + } + getProvenL2EpochNumber(): Promise { + throw new Error('Method not implemented.'); + } + getBlock(number: number): Promise { + throw new Error('Method not implemented.'); + } + getBlockHeader(number: number | 'latest'): Promise
{ + throw new Error('Method not implemented.'); + } + getBlocks(from: number, limit: number, proven?: boolean | undefined): Promise { + throw new Error('Method not implemented.'); + } + getTxEffect(txHash: TxHash): Promise { + throw new Error('Method not implemented.'); + } + getSettledTxReceipt(txHash: TxHash): Promise { + throw new Error('Method not implemented.'); + } + getL2SlotNumber(): Promise { + throw new Error('Method not implemented.'); + } + getL2EpochNumber(): Promise { + throw new Error('Method not implemented.'); + } + getBlocksForEpoch(epochNumber: bigint): Promise { + throw new Error('Method not implemented.'); + } + isEpochComplete(epochNumber: bigint): Promise { + throw new Error('Method not implemented.'); + } + getL2Tips(): Promise { + throw new Error('Method not implemented.'); + } + start(blockUntilSynced: boolean): Promise { + throw new Error('Method not implemented.'); + } + stop(): Promise { + throw new Error('Method not implemented.'); + } + getLogs( + from: number, + limit: number, + logType: TLogType, + ): Promise>[]> { + throw new Error('Method not implemented.'); + } + getLogsByTags(tags: Fr[]): Promise { + throw new Error('Method not implemented.'); + } + getUnencryptedLogs(filter: LogFilter): Promise { + throw new Error('Method not implemented.'); + } + getPublicFunction(address: AztecAddress, selector: FunctionSelector): Promise { + throw new Error('Method not implemented.'); + } + getContractClass(id: Fr): Promise { + throw new Error('Method not implemented.'); + } + getContract(address: AztecAddress): Promise { + throw new Error('Method not implemented.'); + } + getContractClassIds(): Promise { + throw new Error('Method not implemented.'); + } + getContractArtifact(address: AztecAddress): Promise { + throw new Error('Method not implemented.'); + } + addContractArtifact(address: AztecAddress, contract: ContractArtifact): Promise { + throw new Error('Method not implemented.'); + } + getL1ToL2Messages(blockNumber: bigint): Promise { + throw new Error('Method not implemented.'); + } + getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { + throw new Error('Method not implemented.'); + } +} + +export const ArchiverApiSchema: ApiSchemaFor = { + getRollupAddress: z.function().args().returns(schemas.EthAddress), + getRegistryAddress: z.function().args().returns(schemas.EthAddress), + getBlockNumber: z.function().args().returns(schemas.Integer), + getProvenBlockNumber: z.function().args().returns(schemas.Integer), + getProvenL2EpochNumber: z.function().args().returns(schemas.Integer.optional()), + getBlock: z.function().args(schemas.Integer).returns(L2Block.schema.optional()), + getBlockHeader: z + .function() + .args(z.union([schemas.Integer, z.literal('latest')])) + .returns(Header.schema.optional()), + getBlocks: z + .function() + .args(schemas.Integer, schemas.Integer, z.boolean().optional()) + .returns(z.array(L2Block.schema)), + getTxEffect: z.function().args(TxHash.schema).returns(TxEffect.schema.optional()), + getSettledTxReceipt: z.function().args(TxHash.schema).returns(TxReceipt.schema.optional()), + getL2SlotNumber: z.function().args().returns(schemas.BigInt), + getL2EpochNumber: z.function().args().returns(schemas.BigInt), + getBlocksForEpoch: z.function().args(schemas.BigInt).returns(z.array(L2Block.schema)), + isEpochComplete: z.function().args(schemas.BigInt).returns(z.boolean()), + getL2Tips: z.function().args().returns(L2TipsSchema), + getLogs: z.function().args(schemas.Integer, schemas.Integer, LogTypeSchema).returns(z.array(L2BlockL2LogsSchema)), + getLogsByTags: z + .function() + .args(z.array(schemas.Fr)) + .returns(z.array(z.array(EncryptedL2NoteLog.schema))), + getUnencryptedLogs: z.function().args(LogFilterSchema).returns(GetUnencryptedLogsResponseSchema), + getPublicFunction: z + .function() + .args(schemas.AztecAddress, schemas.FunctionSelector) + .returns(PublicFunctionSchema.optional()), + getContractClass: z.function().args(schemas.Fr).returns(ContractClassPublicSchema.optional()), + getContract: z.function().args(schemas.AztecAddress).returns(ContractInstanceWithAddressSchema.optional()), + getContractClassIds: z.function().args().returns(z.array(schemas.Fr)), + getContractArtifact: z.function().args(schemas.AztecAddress).returns(ContractArtifactSchema.optional()), + addContractArtifact: z.function().args(schemas.AztecAddress, ContractArtifactSchema).returns(z.void()), + getL1ToL2Messages: z.function().args(schemas.BigInt).returns(z.array(schemas.Fr)), + getL1ToL2MessageIndex: z.function().args(schemas.Fr).returns(schemas.BigInt.optional()), +}; diff --git a/yarn-project/circuit-types/src/l2_block.ts b/yarn-project/circuit-types/src/l2_block.ts index 092b3fdf2de2..49c47b454614 100644 --- a/yarn-project/circuit-types/src/l2_block.ts +++ b/yarn-project/circuit-types/src/l2_block.ts @@ -4,6 +4,8 @@ import { sha256, sha256ToField } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + import { makeAppendOnlyTreeSnapshot, makeHeader } from './l2_block_code_to_purge.js'; /** @@ -19,6 +21,24 @@ export class L2Block { public body: Body, ) {} + static get schema() { + return z + .object({ + archive: AppendOnlyTreeSnapshot.schema, + header: Header.schema, + body: Body.schema, + }) + .transform(({ archive, header, body }) => new L2Block(archive, header, body)); + } + + toJSON() { + return { + archive: this.archive, + header: this.header, + body: this.body, + }; + } + /** * Deserializes a block from a buffer * @returns A deserialized L2 block. diff --git a/yarn-project/circuit-types/src/l2_block_source.ts b/yarn-project/circuit-types/src/l2_block_source.ts index 6cc0af03059d..d04ab6a2b5dc 100644 --- a/yarn-project/circuit-types/src/l2_block_source.ts +++ b/yarn-project/circuit-types/src/l2_block_source.ts @@ -1,5 +1,7 @@ import { type EthAddress, type Header } from '@aztec/circuits.js'; +import { z } from 'zod'; + import { type L2Block } from './l2_block.js'; import { type TxHash } from './tx/tx_hash.js'; import { type TxReceipt } from './tx/tx_receipt.js'; @@ -130,14 +132,21 @@ export type L2BlockTag = 'latest' | 'proven' | 'finalized'; export type L2Tips = Record; /** Identifies a block by number and hash. */ -export type L2BlockId = - | { - number: 0; - hash: undefined; - } - | { - /** L2 block number. */ - number: number; - /** L2 block hash. */ - hash: string; - }; +export type L2BlockId = z.infer; + +const L2BlockIdSchema = z.union([ + z.object({ + number: z.literal(0), + hash: z.undefined(), + }), + z.object({ + number: z.number(), + hash: z.string(), + }), +]); + +export const L2TipsSchema = z.object({ + latest: L2BlockIdSchema, + proven: L2BlockIdSchema, + finalized: L2BlockIdSchema, +}) satisfies z.ZodType; diff --git a/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts b/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts index 095fe870b29d..f77df34c8c64 100644 --- a/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts +++ b/yarn-project/circuit-types/src/logs/encrypted_l2_note_log.ts @@ -1,5 +1,8 @@ import { Fr, Point } from '@aztec/circuits.js'; import { randomBytes, sha256Trunc } from '@aztec/foundation/crypto'; +import { schemas } from '@aztec/foundation/schemas'; + +import { z } from 'zod'; /** * Represents an individual encrypted log entry. @@ -29,6 +32,14 @@ export class EncryptedL2NoteLog { }; } + static get schema() { + return z + .object({ + data: schemas.HexString, + }) + .transform(({ data }) => new EncryptedL2NoteLog(Buffer.from(data, 'hex'))); + } + /** Converts a plain JSON object into an instance. */ public static fromJSON(obj: any) { return new EncryptedL2NoteLog(Buffer.from(obj.data, 'hex')); diff --git a/yarn-project/circuit-types/src/logs/extended_unencrypted_l2_log.ts b/yarn-project/circuit-types/src/logs/extended_unencrypted_l2_log.ts index 0916f7231643..6893a546c7f2 100644 --- a/yarn-project/circuit-types/src/logs/extended_unencrypted_l2_log.ts +++ b/yarn-project/circuit-types/src/logs/extended_unencrypted_l2_log.ts @@ -1,6 +1,8 @@ import { BufferReader } from '@aztec/foundation/serialize'; +import { type FieldsOf } from '@aztec/foundation/types'; import isEqual from 'lodash.isequal'; +import { z } from 'zod'; import { LogId } from './log_id.js'; import { UnencryptedL2Log } from './unencrypted_l2_log.js'; @@ -16,6 +18,23 @@ export class ExtendedUnencryptedL2Log { public readonly log: UnencryptedL2Log, ) {} + toJSON() { + return { id: this.id, log: this.log }; + } + + static get schema() { + return z + .object({ + id: LogId.schema, + log: UnencryptedL2Log.schema, + }) + .transform(ExtendedUnencryptedL2Log.from); + } + + static from(fields: FieldsOf) { + return new ExtendedUnencryptedL2Log(fields.id, fields.log); + } + /** * Serializes log to a buffer. * @returns A buffer containing the serialized log. diff --git a/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts b/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts index b8c18fa278d8..3e5f54169c9f 100644 --- a/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts +++ b/yarn-project/circuit-types/src/logs/get_unencrypted_logs_response.ts @@ -1,16 +1,16 @@ -import { type ExtendedUnencryptedL2Log } from './extended_unencrypted_l2_log.js'; +import { z } from 'zod'; -/** - * It provides documentation for the GetUnencryptedLogsResponse type. - */ +import { ExtendedUnencryptedL2Log } from './extended_unencrypted_l2_log.js'; + +/** Response for the getUnencryptedLogs archiver call. */ export type GetUnencryptedLogsResponse = { - /** - * An array of ExtendedUnencryptedL2Log elements. - */ + /** An array of ExtendedUnencryptedL2Log elements. */ logs: ExtendedUnencryptedL2Log[]; - - /** - * Indicates if a limit has been reached. - */ + /** Indicates if a limit has been reached. */ maxLogsHit: boolean; }; + +export const GetUnencryptedLogsResponseSchema = z.object({ + logs: z.array(ExtendedUnencryptedL2Log.schema), + maxLogsHit: z.boolean(), +}) satisfies z.ZodType; diff --git a/yarn-project/circuit-types/src/logs/log_filter.ts b/yarn-project/circuit-types/src/logs/log_filter.ts index 93a33725b2f7..0ac97ee5da5b 100644 --- a/yarn-project/circuit-types/src/logs/log_filter.ts +++ b/yarn-project/circuit-types/src/logs/log_filter.ts @@ -1,7 +1,10 @@ import { type AztecAddress } from '@aztec/circuits.js'; +import { schemas } from '@aztec/foundation/schemas'; -import { type TxHash } from '../tx/tx_hash.js'; -import { type LogId } from './log_id.js'; +import { z } from 'zod'; + +import { TxHash } from '../tx/tx_hash.js'; +import { LogId } from './log_id.js'; /** * Log filter used to fetch L2 logs. @@ -19,3 +22,11 @@ export type LogFilter = { /** The contract address to filter logs by. */ contractAddress?: AztecAddress; }; + +export const LogFilterSchema = z.object({ + txHash: TxHash.schema.optional(), + fromBlock: schemas.Integer.optional(), + toBlock: schemas.Integer.optional(), + afterLog: LogId.schema.optional(), + contractAddress: schemas.AztecAddress.optional(), +}); diff --git a/yarn-project/circuit-types/src/logs/log_id.ts b/yarn-project/circuit-types/src/logs/log_id.ts index 9d0d06fd3023..98aa023a7d37 100644 --- a/yarn-project/circuit-types/src/logs/log_id.ts +++ b/yarn-project/circuit-types/src/logs/log_id.ts @@ -1,7 +1,10 @@ import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + /** A globally unique log id. */ export class LogId { /** @@ -29,6 +32,24 @@ export class LogId { } } + static get schema() { + return z + .object({ + blockNumber: schemas.Integer, + txIndex: schemas.Integer, + logIndex: schemas.Integer, + }) + .transform(({ blockNumber, txIndex, logIndex }) => new LogId(blockNumber, txIndex, logIndex)); + } + + toJSON() { + return { + blockNumber: this.blockNumber, + txIndex: this.txIndex, + logIndex: this.logIndex, + }; + } + /** * Serializes log id to a buffer. * @returns A buffer containing the serialized log id. diff --git a/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts b/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts index 266167463403..4e756090e129 100644 --- a/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts +++ b/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts @@ -1,7 +1,10 @@ import { AztecAddress } from '@aztec/circuits.js'; import { randomBytes, sha256Trunc } from '@aztec/foundation/crypto'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, prefixBufferWithLength, toHumanReadable } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + /** * Represents an individual unencrypted log entry. */ @@ -43,6 +46,15 @@ export class UnencryptedL2Log { return `UnencryptedL2Log(contractAddress: ${this.contractAddress.toString()}, data: ${payload})`; } + static get schema() { + return z + .object({ + contractAddress: schemas.AztecAddress, + data: schemas.BufferHex, + }) + .transform(({ contractAddress, data }) => new UnencryptedL2Log(contractAddress, data)); + } + /** Returns a JSON-friendly representation of the log. */ public toJSON(): object { return { diff --git a/yarn-project/circuit-types/src/notes/extended_note.ts b/yarn-project/circuit-types/src/notes/extended_note.ts index 4b3184563d00..603803bd9109 100644 --- a/yarn-project/circuit-types/src/notes/extended_note.ts +++ b/yarn-project/circuit-types/src/notes/extended_note.ts @@ -1,5 +1,6 @@ import { AztecAddress, Fr } from '@aztec/circuits.js'; import { NoteSelector } from '@aztec/foundation/abi'; +import { hexSchemaFor } from '@aztec/foundation/schemas'; import { BufferReader } from '@aztec/foundation/serialize'; import { Note } from '../logs/l1_payload/payload.js'; @@ -48,6 +49,14 @@ export class ExtendedNote { return new this(note, owner, contractAddress, storageSlot, noteTypeId, txHash); } + toJSON() { + return this.toString(); + } + + static get schema() { + return hexSchemaFor(ExtendedNote); + } + toString() { return '0x' + this.toBuffer().toString('hex'); } @@ -78,6 +87,10 @@ export class UniqueNote extends ExtendedNote { super(note, owner, contractAddress, storageSlot, noteTypeId, txHash); } + static override get schema() { + return hexSchemaFor(UniqueNote); + } + override toBuffer(): Buffer { return Buffer.concat([ this.note.toBuffer(), diff --git a/yarn-project/circuit-types/src/public_data_write.ts b/yarn-project/circuit-types/src/public_data_write.ts index f15b8c299910..93a41dce19b3 100644 --- a/yarn-project/circuit-types/src/public_data_write.ts +++ b/yarn-project/circuit-types/src/public_data_write.ts @@ -1,7 +1,10 @@ import { STRING_ENCODING } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + /** * Write operations on the public state tree. */ @@ -19,6 +22,19 @@ export class PublicDataWrite { public readonly newValue: Fr, ) {} + static get schema() { + return z + .object({ + leafIndex: schemas.Fr, + newValue: schemas.Fr, + }) + .transform(PublicDataWrite.from); + } + + toJSON() { + return { leafIndex: this.leafIndex, newValue: this.newValue }; + } + /** * Creates a new public data write operation from the given arguments. * @param args - Arguments containing info used to create a new public data write operation. diff --git a/yarn-project/circuit-types/src/tx/tx_hash.ts b/yarn-project/circuit-types/src/tx/tx_hash.ts index 921903d75a00..748154641d33 100644 --- a/yarn-project/circuit-types/src/tx/tx_hash.ts +++ b/yarn-project/circuit-types/src/tx/tx_hash.ts @@ -1,15 +1,18 @@ import { Buffer32 } from '@aztec/foundation/buffer'; +import { schemas } from '@aztec/foundation/schemas'; /** * A class representing hash of Aztec transaction. */ export class TxHash extends Buffer32 { constructor( - /** - * The buffer containing the hash. - */ + /** The buffer containing the hash. */ hash: Buffer, ) { super(hash); } + + static get schema() { + return schemas.Buffer32; + } } diff --git a/yarn-project/circuit-types/src/tx/tx_receipt.ts b/yarn-project/circuit-types/src/tx/tx_receipt.ts index e784e938c9c0..a9469af4cb83 100644 --- a/yarn-project/circuit-types/src/tx/tx_receipt.ts +++ b/yarn-project/circuit-types/src/tx/tx_receipt.ts @@ -1,8 +1,12 @@ import { RevertCode } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; +import { type FieldsOf } from '@aztec/foundation/types'; -import { type UniqueNote } from '../notes/extended_note.js'; -import { type PublicDataWrite } from '../public_data_write.js'; +import { z } from 'zod'; + +import { UniqueNote } from '../notes/extended_note.js'; +import { PublicDataWrite } from '../public_data_write.js'; import { TxHash } from './tx_hash.js'; /** @@ -25,33 +29,19 @@ export enum TxStatus { */ export class TxReceipt { constructor( - /** - * A unique identifier for a transaction. - */ + /** A unique identifier for a transaction. */ public txHash: TxHash, - /** - * The transaction's status. - */ + /** The transaction's status. */ public status: TxStatus, - /** - * Description of transaction error, if any. - */ + /** Description of transaction error, if any. */ public error: string, - /** - * The transaction fee paid for the transaction. - */ + /** The transaction fee paid for the transaction. */ public transactionFee?: bigint, - /** - * The hash of the block containing the transaction. - */ + /** The hash of the block containing the transaction. */ public blockHash?: Buffer, - /** - * The block number in which the transaction was included. - */ + /** The block number in which the transaction was included. */ public blockNumber?: number, - /** - * Information useful for testing/debugging, set when test flag is set to true in `waitOpts`. - */ + /** Information useful for testing/debugging, set when test flag is set to true in `waitOpts`. */ public debugInfo?: DebugInfo, ) {} @@ -67,9 +57,36 @@ export class TxReceipt { blockHash: this.blockHash?.toString('hex'), blockNumber: this.blockNumber, transactionFee: this.transactionFee?.toString(), + ...(this.debugInfo && { debugInfo: this.debugInfo }), }; } + static get schema() { + return z + .object({ + txHash: schemas.Buffer32, + status: z.nativeEnum(TxStatus), + error: z.string(), + blockHash: schemas.BufferHex.optional(), + blockNumber: z.number().optional(), + transactionFee: schemas.BigInt.optional(), + debugInfo: DebugInfoSchema.optional(), + }) + .transform(TxReceipt.from); + } + + static from(fields: FieldsOf) { + return new TxReceipt( + fields.txHash, + fields.status, + fields.error, + fields.transactionFee, + fields.blockHash, + fields.blockNumber, + fields.debugInfo, + ); + } + /** * Convert a plain JSON object to a Tx class object. * @param obj - A plain Tx JSON object. @@ -134,3 +151,12 @@ interface DebugInfo { */ visibleOutgoingNotes: UniqueNote[]; } + +const DebugInfoSchema = z.object({ + noteHashes: z.array(schemas.Fr), + nullifiers: z.array(schemas.Fr), + publicDataWrites: z.array(PublicDataWrite.schema), + l2ToL1Msgs: z.array(schemas.Fr), + visibleIncomingNotes: z.array(UniqueNote.schema), + visibleOutgoingNotes: z.array(UniqueNote.schema), +}); diff --git a/yarn-project/circuit-types/src/tx_effect.ts b/yarn-project/circuit-types/src/tx_effect.ts index 1115f1b29114..148eb8e72d42 100644 --- a/yarn-project/circuit-types/src/tx_effect.ts +++ b/yarn-project/circuit-types/src/tx_effect.ts @@ -16,6 +16,7 @@ import { import { makeTuple } from '@aztec/foundation/array'; import { padArrayEnd } from '@aztec/foundation/collection'; import { sha256Trunc } from '@aztec/foundation/crypto'; +import { hexSchemaFor } from '@aztec/foundation/schemas'; import { BufferReader, serializeArrayOfBufferableToVector, serializeToBuffer } from '@aztec/foundation/serialize'; import { inspect } from 'util'; @@ -251,13 +252,19 @@ export class TxEffect { return this.nullifiers.length === 0; } - /** - * Returns a string representation of the TxEffect object. - */ + /** Returns a hex representation of the TxEffect object. */ toString(): string { return this.toBuffer().toString('hex'); } + toJSON() { + return this.toString(); + } + + static get schema() { + return hexSchemaFor(TxEffect); + } + [inspect.custom]() { // print out the non-empty fields diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index 5a83df9826bd..707220e1fe1f 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -44,7 +44,8 @@ "@aztec/types": "workspace:^", "eslint": "^8.35.0", "lodash.chunk": "^4.2.0", - "tslib": "^2.4.0" + "tslib": "^2.4.0", + "zod": "^3.23.8" }, "devDependencies": { "@jest/globals": "^29.5.0", diff --git a/yarn-project/circuits.js/src/contract/interfaces/contract_class.ts b/yarn-project/circuits.js/src/contract/interfaces/contract_class.ts index bccf0db1528d..4c0c30103fa3 100644 --- a/yarn-project/circuits.js/src/contract/interfaces/contract_class.ts +++ b/yarn-project/circuits.js/src/contract/interfaces/contract_class.ts @@ -1,5 +1,8 @@ import { type FunctionSelector } from '@aztec/foundation/abi'; import { type Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; + +import { z } from 'zod'; const VERSION = 1 as const; @@ -30,6 +33,21 @@ export interface PrivateFunction { vkHash: Fr; } +const PrivateFunctionSchema = z.object({ + selector: schemas.FunctionSelector, + vkHash: schemas.Fr, +}) satisfies z.ZodType; + +/** Private function definition with executable bytecode. */ +export interface ExecutablePrivateFunction extends PrivateFunction { + /** ACIR and Brillig bytecode */ + bytecode: Buffer; +} + +const ExecutablePrivateFunctionSchema = PrivateFunctionSchema.and( + z.object({ bytecode: schemas.BufferB64 }), +) satisfies z.ZodType; + /** Public function definition within a contract class. */ export interface PublicFunction { /** Selector of the function. Calculated as the hash of the method name and parameters. The specification of this is not enforced by the protocol. */ @@ -38,6 +56,11 @@ export interface PublicFunction { bytecode: Buffer; } +export const PublicFunctionSchema = z.object({ + selector: schemas.FunctionSelector, + bytecode: schemas.BufferB64, +}) satisfies z.ZodType; + /** Unconstrained function definition. */ export interface UnconstrainedFunction { /** Selector of the function. Calculated as the hash of the method name and parameters. The specification of this is not enforced by the protocol. */ @@ -46,34 +69,11 @@ export interface UnconstrainedFunction { bytecode: Buffer; } -/** Commitments to fields of a contract class. */ -interface ContractClassCommitments { - /** Identifier of the contract class. */ - id: Fr; - /** Commitment to the public bytecode. */ - publicBytecodeCommitment: Fr; - /** Root of the private functions tree */ - privateFunctionsRoot: Fr; -} - -/** A contract class with its precomputed id. */ -export type ContractClassWithId = ContractClass & Pick; - -/** A contract class with public bytecode information, and optional private and unconstrained. */ -export type ContractClassPublic = { - privateFunctions: ExecutablePrivateFunctionWithMembershipProof[]; - unconstrainedFunctions: UnconstrainedFunctionWithMembershipProof[]; -} & Pick & - Omit; - -/** The contract class with the block it was initially deployed at */ -export type ContractClassPublicWithBlockNumber = { l2BlockNumber: number } & ContractClassPublic; - -/** Private function definition with executable bytecode. */ -export interface ExecutablePrivateFunction extends PrivateFunction { - /** ACIR and Brillig bytecode */ - bytecode: Buffer; -} +const UnconstrainedFunctionSchema = z.object({ + /** lala */ + selector: schemas.FunctionSelector, + bytecode: schemas.BufferB64, +}) satisfies z.ZodType; /** Sibling paths and sibling commitments for proving membership of a private function within a contract class. */ export type PrivateFunctionMembershipProof = { @@ -86,6 +86,16 @@ export type PrivateFunctionMembershipProof = { artifactTreeLeafIndex: number; }; +const PrivateFunctionMembershipProofSchema = z.object({ + artifactMetadataHash: schemas.Fr, + functionMetadataHash: schemas.Fr, + unconstrainedFunctionsArtifactTreeRoot: schemas.Fr, + privateFunctionTreeSiblingPath: z.array(schemas.Fr), + privateFunctionTreeLeafIndex: schemas.Integer, + artifactTreeSiblingPath: z.array(schemas.Fr), + artifactTreeLeafIndex: schemas.Integer, +}) satisfies z.ZodType; + /** A private function with a memebership proof. */ export type ExecutablePrivateFunctionWithMembershipProof = ExecutablePrivateFunction & PrivateFunctionMembershipProof; @@ -98,5 +108,53 @@ export type UnconstrainedFunctionMembershipProof = { artifactTreeLeafIndex: number; }; +const UnconstrainedFunctionMembershipProofSchema = z.object({ + artifactMetadataHash: schemas.Fr, + functionMetadataHash: schemas.Fr, + privateFunctionsArtifactTreeRoot: schemas.Fr, + artifactTreeSiblingPath: z.array(schemas.Fr), + artifactTreeLeafIndex: schemas.Integer, +}) satisfies z.ZodType; + /** An unconstrained function with a membership proof. */ export type UnconstrainedFunctionWithMembershipProof = UnconstrainedFunction & UnconstrainedFunctionMembershipProof; + +export const ContractClassSchema = z.object({ + version: z.literal(VERSION), + artifactHash: schemas.Fr, + privateFunctions: z.array(PrivateFunctionSchema), + publicFunctions: z.array(PublicFunctionSchema), + packedBytecode: schemas.BufferB64, +}) satisfies z.ZodType; + +/** Commitments to fields of a contract class. */ +interface ContractClassCommitments { + /** Identifier of the contract class. */ + id: Fr; + /** Commitment to the public bytecode. */ + publicBytecodeCommitment: Fr; + /** Root of the private functions tree */ + privateFunctionsRoot: Fr; +} + +/** A contract class with its precomputed id. */ +export type ContractClassWithId = ContractClass & Pick; + +/** A contract class with public bytecode information, and optional private and unconstrained. */ +export type ContractClassPublic = { + privateFunctions: ExecutablePrivateFunctionWithMembershipProof[]; + unconstrainedFunctions: UnconstrainedFunctionWithMembershipProof[]; +} & Pick & + Omit; + +export const ContractClassPublicSchema = z + .object({ + id: schemas.Fr, + privateFunctionsRoot: schemas.Fr, + privateFunctions: z.array(ExecutablePrivateFunctionSchema.and(PrivateFunctionMembershipProofSchema)), + unconstrainedFunctions: z.array(UnconstrainedFunctionSchema.and(UnconstrainedFunctionMembershipProofSchema)), + }) + .and(ContractClassSchema.omit({ privateFunctions: true })) satisfies z.ZodType; + +/** The contract class with the block it was initially deployed at */ +export type ContractClassPublicWithBlockNumber = { l2BlockNumber: number } & ContractClassPublic; diff --git a/yarn-project/circuits.js/src/structs/content_commitment.ts b/yarn-project/circuits.js/src/structs/content_commitment.ts index ae18898ac61c..578a6831ff57 100644 --- a/yarn-project/circuits.js/src/structs/content_commitment.ts +++ b/yarn-project/circuits.js/src/structs/content_commitment.ts @@ -1,6 +1,9 @@ import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + import { CONTENT_COMMITMENT_LENGTH } from '../constants.gen.js'; export const NUM_BYTES_PER_SHA256 = 32; @@ -27,6 +30,28 @@ export class ContentCommitment { } } + static get schema() { + return z + .object({ + numTxs: schemas.Fr, + txsEffectsHash: schemas.BufferHex, + inHash: schemas.BufferHex, + outHash: schemas.BufferHex, + }) + .transform( + ({ numTxs, txsEffectsHash, inHash, outHash }) => new ContentCommitment(numTxs, txsEffectsHash, inHash, outHash), + ); + } + + toJSON() { + return { + numTxs: this.numTxs, + txsEffectsHash: this.txsEffectsHash.toString('hex'), + inHash: this.inHash.toString('hex'), + outHash: this.outHash.toString('hex'), + }; + } + getSize() { return this.toBuffer().length; } diff --git a/yarn-project/circuits.js/src/structs/gas_fees.ts b/yarn-project/circuits.js/src/structs/gas_fees.ts index 17d5e0835fee..6cab097c0cec 100644 --- a/yarn-project/circuits.js/src/structs/gas_fees.ts +++ b/yarn-project/circuits.js/src/structs/gas_fees.ts @@ -1,8 +1,10 @@ import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { inspect } from 'util'; +import { z } from 'zod'; import { type GasDimensions } from './gas.js'; @@ -16,6 +18,15 @@ export class GasFees { this.feePerL2Gas = new Fr(feePerL2Gas); } + static get schema() { + return z + .object({ + feePerDaGas: schemas.Fr, + feePerL2Gas: schemas.Fr, + }) + .transform(GasFees.from); + } + clone(): GasFees { return new GasFees(this.feePerDaGas, this.feePerL2Gas); } diff --git a/yarn-project/circuits.js/src/structs/global_variables.ts b/yarn-project/circuits.js/src/structs/global_variables.ts index e36f75035739..f49badfe8ff8 100644 --- a/yarn-project/circuits.js/src/structs/global_variables.ts +++ b/yarn-project/circuits.js/src/structs/global_variables.ts @@ -1,10 +1,12 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { inspect } from 'util'; +import { z } from 'zod'; import { GLOBAL_VARIABLES_LENGTH } from '../constants.gen.js'; import { GasFees } from './gas_fees.js'; @@ -32,6 +34,21 @@ export class GlobalVariables { public gasFees: GasFees, ) {} + static get schema() { + return z + .object({ + chainId: schemas.Fr, + version: schemas.Fr, + blockNumber: schemas.Fr, + slotNumber: schemas.Fr, + timestamp: schemas.Fr, + coinbase: schemas.EthAddress, + feeRecipient: schemas.AztecAddress, + gasFees: GasFees.schema, + }) + .transform(GlobalVariables.from); + } + getSize(): number { return this.toBuffer().length; } diff --git a/yarn-project/circuits.js/src/structs/header.ts b/yarn-project/circuits.js/src/structs/header.ts index 4e62a930783f..f31f85a2ef9e 100644 --- a/yarn-project/circuits.js/src/structs/header.ts +++ b/yarn-project/circuits.js/src/structs/header.ts @@ -1,9 +1,11 @@ import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { inspect } from 'util'; +import { z } from 'zod'; import { GeneratorIndex, HEADER_LENGTH } from '../constants.gen.js'; import { ContentCommitment } from './content_commitment.js'; @@ -26,6 +28,28 @@ export class Header { public totalFees: Fr, ) {} + static get schema() { + return z + .object({ + lastArchive: AppendOnlyTreeSnapshot.schema, + contentCommitment: ContentCommitment.schema, + state: StateReference.schema, + globalVariables: GlobalVariables.schema, + totalFees: schemas.Fr, + }) + .transform(Header.from); + } + + toJSON() { + return { + lastArchive: this.lastArchive, + contentCommitment: this.contentCommitment, + state: this.state, + globalVariables: this.globalVariables, + totalFees: this.totalFees, + }; + } + static getFields(fields: FieldsOf
) { // Note: The order here must match the order in the HeaderLib solidity library. return [ diff --git a/yarn-project/circuits.js/src/structs/parity/root_parity_input.ts b/yarn-project/circuits.js/src/structs/parity/root_parity_input.ts index 89cc1b14f91b..aa26e28386e6 100644 --- a/yarn-project/circuits.js/src/structs/parity/root_parity_input.ts +++ b/yarn-project/circuits.js/src/structs/parity/root_parity_input.ts @@ -65,6 +65,6 @@ export class RootParityInput { /** Creates an instance from a hex string with expected size. */ static schemaFor(expectedSize?: N) { - return schemas.Hex.transform(str => RootParityInput.fromString(str, expectedSize)); + return schemas.HexString.transform(str => RootParityInput.fromString(str, expectedSize)); } } diff --git a/yarn-project/circuits.js/src/structs/partial_state_reference.ts b/yarn-project/circuits.js/src/structs/partial_state_reference.ts index 15a5c09a093c..fe484113835c 100644 --- a/yarn-project/circuits.js/src/structs/partial_state_reference.ts +++ b/yarn-project/circuits.js/src/structs/partial_state_reference.ts @@ -1,6 +1,8 @@ import { type Fr } from '@aztec/foundation/fields'; import { BufferReader, FieldReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { z } from 'zod'; + import { PARTIAL_STATE_REFERENCE_LENGTH } from '../constants.gen.js'; import { AppendOnlyTreeSnapshot } from './rollup/append_only_tree_snapshot.js'; @@ -17,6 +19,27 @@ export class PartialStateReference { public readonly publicDataTree: AppendOnlyTreeSnapshot, ) {} + toJSON() { + return { + noteHashTree: this.noteHashTree, + nullifierTree: this.nullifierTree, + publicDataTree: this.publicDataTree, + }; + } + + static get schema() { + return z + .object({ + noteHashTree: AppendOnlyTreeSnapshot.schema, + nullifierTree: AppendOnlyTreeSnapshot.schema, + publicDataTree: AppendOnlyTreeSnapshot.schema, + }) + .transform( + ({ noteHashTree, nullifierTree, publicDataTree }) => + new PartialStateReference(noteHashTree, nullifierTree, publicDataTree), + ); + } + getSize() { return this.noteHashTree.getSize() + this.nullifierTree.getSize() + this.publicDataTree.getSize(); } diff --git a/yarn-project/circuits.js/src/structs/recursive_proof.ts b/yarn-project/circuits.js/src/structs/recursive_proof.ts index b617a3d9437e..ce5a3b0dcaef 100644 --- a/yarn-project/circuits.js/src/structs/recursive_proof.ts +++ b/yarn-project/circuits.js/src/structs/recursive_proof.ts @@ -92,7 +92,7 @@ export class RecursiveProof { /** Creates an instance from a hex string with expected size. */ static schemaFor(expectedSize?: N) { - return schemas.Hex.transform(str => RecursiveProof.fromString(str, expectedSize)); + return schemas.HexString.transform(str => RecursiveProof.fromString(str, expectedSize)); } } diff --git a/yarn-project/circuits.js/src/structs/rollup/append_only_tree_snapshot.ts b/yarn-project/circuits.js/src/structs/rollup/append_only_tree_snapshot.ts index f6c1b8d7a05d..b4c71e37be1d 100644 --- a/yarn-project/circuits.js/src/structs/rollup/append_only_tree_snapshot.ts +++ b/yarn-project/circuits.js/src/structs/rollup/append_only_tree_snapshot.ts @@ -1,7 +1,9 @@ import { Fr } from '@aztec/foundation/fields'; +import { schemas } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { inspect } from 'util'; +import { z } from 'zod'; import { STRING_ENCODING, type UInt32 } from '../shared.js'; @@ -28,6 +30,19 @@ export class AppendOnlyTreeSnapshot { public nextAvailableLeafIndex: UInt32, ) {} + static get schema() { + return z + .object({ + root: schemas.Fr, + nextAvailableLeafIndex: schemas.UInt32, + }) + .transform(({ root, nextAvailableLeafIndex }) => new AppendOnlyTreeSnapshot(root, nextAvailableLeafIndex)); + } + + toJSON() { + return { root: this.root, nextAvailableLeafIndex: this.nextAvailableLeafIndex }; + } + getSize() { return this.root.size + 4; } diff --git a/yarn-project/circuits.js/src/structs/state_reference.ts b/yarn-project/circuits.js/src/structs/state_reference.ts index e29ca1d6f9fc..8f0c1fd0be0b 100644 --- a/yarn-project/circuits.js/src/structs/state_reference.ts +++ b/yarn-project/circuits.js/src/structs/state_reference.ts @@ -2,6 +2,7 @@ import { type Fr } from '@aztec/foundation/fields'; import { BufferReader, FieldReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { inspect } from 'util'; +import { z } from 'zod'; import { STATE_REFERENCE_LENGTH } from '../constants.gen.js'; import { PartialStateReference } from './partial_state_reference.js'; @@ -18,6 +19,19 @@ export class StateReference { public partial: PartialStateReference, ) {} + toJSON() { + return { l1ToL2MessageTree: this.l1ToL2MessageTree, partial: this.partial }; + } + + static get schema() { + return z + .object({ + l1ToL2MessageTree: AppendOnlyTreeSnapshot.schema, + partial: PartialStateReference.schema, + }) + .transform(({ l1ToL2MessageTree, partial }) => new StateReference(l1ToL2MessageTree, partial)); + } + getSize() { return this.l1ToL2MessageTree.getSize() + this.partial.getSize(); } diff --git a/yarn-project/foundation/package.json b/yarn-project/foundation/package.json index cd08e0f109be..448fc6c2590d 100644 --- a/yarn-project/foundation/package.json +++ b/yarn-project/foundation/package.json @@ -162,4 +162,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts index 160be402bff1..759c8e5d7fb7 100644 --- a/yarn-project/foundation/src/abi/abi.ts +++ b/yarn-project/foundation/src/abi/abi.ts @@ -1,4 +1,5 @@ import { inflate } from 'pako'; +import { z } from 'zod'; import { type Fr } from '../fields/fields.js'; import { type FunctionSelector } from './function_selector.js'; @@ -335,43 +336,30 @@ export type FieldLayout = { slot: Fr; }; -/** - * Defines artifact of a contract. - */ +/** Defines artifact of a contract. */ export interface ContractArtifact { - /** - * The name of the contract. - */ + /** The name of the contract. */ name: string; - /** - * The version of compiler used to create this artifact - */ + /** The version of compiler used to create this artifact */ aztecNrVersion?: string; - /** - * The functions of the contract. - */ + /** The functions of the contract. */ functions: FunctionArtifact[]; - /** - * The outputs of the contract. - */ + + /** The outputs of the contract. */ outputs: { structs: Record; globals: Record; }; - /** - * Storage layout - */ + + /** Storage layout */ storageLayout: Record; - /** - * The notes used in the contract. - */ + + /** The notes used in the contract. */ notes: Record; - /** - * The map of file ID to the source code and path of the file. - */ + /** The map of file ID to the source code and path of the file. */ fileMap: DebugFileMap; } @@ -393,6 +381,20 @@ export interface FunctionDebugMetadata { assertMessages?: Record; } +// CONTINUE HERE +export const ContractArtifactSchema = z.object({ + name: z.string(), + aztecNrVersion: z.string().optional(), + functions: z.array(FunctionArtifactSchema), + outputs: z.object({ + structs: z.record(z.array(AbiTypeSchema)), + globals: z.record(z.array(AbiValueSchema)), + }), + storageLayout: z.record(FieldLayoutSchema), + notes: z.record(ContractNoteSchema), + fileMap: z.record(DebugFileMapSchema), +}); + /** * Gets a function artifact including debug metadata given its name or selector. */ diff --git a/yarn-project/foundation/src/aztec-address/index.ts b/yarn-project/foundation/src/aztec-address/index.ts index 53229d1fda7a..ee2efc71a480 100644 --- a/yarn-project/foundation/src/aztec-address/index.ts +++ b/yarn-project/foundation/src/aztec-address/index.ts @@ -19,6 +19,10 @@ export class AztecAddress extends Fr { super(buffer); } + static isAddress(str: string) { + return /^(0x)?[a-fA-F0-9]{64}$/.test(str); + } + [inspect.custom]() { return `AztecAddress<${this.toString()}>`; } diff --git a/yarn-project/foundation/src/buffer/buffer32.ts b/yarn-project/foundation/src/buffer/buffer32.ts index a3a1d6b67029..4b1c2e1c25d6 100644 --- a/yarn-project/foundation/src/buffer/buffer32.ts +++ b/yarn-project/foundation/src/buffer/buffer32.ts @@ -70,6 +70,10 @@ export class Buffer32 { return this.buffer.toString('hex'); } + toJSON() { + return this.toString(); + } + public to0xString(): `0x${string}` { return `0x${this.buffer.toString('hex')}`; } @@ -89,6 +93,7 @@ export class Buffer32 { public static fromBigInt(hash: bigint) { return new Buffer32(serializeBigInt(hash, Buffer32.SIZE)); } + public static fromField(hash: Fr) { return new Buffer32(serializeBigInt(hash.toBigInt())); } diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 08443b004687..e411302e0714 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -302,6 +302,7 @@ export class Fr extends BaseField { return Fr.fromBuffer(rootBuf); } + // TODO(palla/schemas): Use toString instead of structured type toJSON() { return { type: 'Fr', diff --git a/yarn-project/foundation/src/json-rpc/fixtures/test_state.ts b/yarn-project/foundation/src/json-rpc/fixtures/test_state.ts index 6e67d64d7859..b30d3ba43066 100644 --- a/yarn-project/foundation/src/json-rpc/fixtures/test_state.ts +++ b/yarn-project/foundation/src/json-rpc/fixtures/test_state.ts @@ -36,7 +36,8 @@ export class TestNote { export interface TestStateApi { getNote: (index: number) => Promise; - getNotes: () => Promise; + getNotes: (limit?: number) => Promise; + getBigNotes: (limit: bigint) => Promise; clear: () => Promise; addNotes: (notes: TestNote[]) => Promise; fail: () => Promise; @@ -73,9 +74,14 @@ export class TestState implements TestStateApi { return this.notes.length; } - async getNotes(): Promise { + async getNotes(limit?: number): Promise { await sleep(0.1); - return this.notes; + return limit ? this.notes.slice(0, limit) : this.notes; + } + + async getBigNotes(limit: bigint): Promise { + await sleep(0.1); + return limit ? this.notes.slice(0, Number(limit)) : this.notes; } async clear(): Promise { @@ -113,7 +119,8 @@ export class TestState implements TestStateApi { export const TestStateSchema: ApiSchemaFor = { getNote: z.function().args(z.number()).returns(TestNote.schema), - getNotes: z.function().returns(z.array(TestNote.schema)), + getNotes: z.function().args(schemas.Integer.optional()).returns(z.array(TestNote.schema)), + getBigNotes: z.function().args(schemas.BigInt).returns(z.array(TestNote.schema)), clear: z.function().returns(z.void()), addNotes: z.function().args(z.array(TestNote.schema)).returns(z.array(TestNote.schema)), fail: z.function().returns(z.void()), diff --git a/yarn-project/foundation/src/json-rpc/server/safe_json_rpc_server.ts b/yarn-project/foundation/src/json-rpc/server/safe_json_rpc_server.ts index 7fa2ca149991..2a91472dfc6a 100644 --- a/yarn-project/foundation/src/json-rpc/server/safe_json_rpc_server.ts +++ b/yarn-project/foundation/src/json-rpc/server/safe_json_rpc_server.ts @@ -9,7 +9,7 @@ import { ZodError } from 'zod'; import { createDebugLogger } from '../../log/index.js'; import { promiseWithResolvers } from '../../promise/utils.js'; -import { type ApiSchema, type ApiSchemaFor, schemaHasMethod } from '../../schemas/index.js'; +import { type ApiSchema, type ApiSchemaFor, parseWithOptionals, schemaHasMethod } from '../../schemas/index.js'; import { jsonStringify2 } from '../convert.js'; import { assert } from '../js_utils.js'; @@ -44,7 +44,7 @@ export class SafeJsonRpcServer { ctx.status = 400; ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32700, message: `Parse error: ${err.message}` } }; } else if (err instanceof ZodError) { - const message = err.issues.map(e => e.message).join(', ') || 'Validation error'; + const message = err.issues.map(e => `${e.message} (${e.path.join('.')})`).join('. ') || 'Validation error'; ctx.status = 400; ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32701, message } }; } else { @@ -180,8 +180,7 @@ export class SafeJsonProxy implements Proxy { assert(schemaHasMethod(this.schema, methodName), `Method ${methodName} not found in schema`); const method = this.handler[methodName as keyof T]; assert(typeof method === 'function', `Method ${methodName} is not a function`); - - const args = this.schema[methodName].parameters().parse(jsonParams); + const args = parseWithOptionals(jsonParams, this.schema[methodName].parameters()); const ret = await method.apply(this.handler, args); this.log.debug(format('response', methodName, ret)); return ret; diff --git a/yarn-project/foundation/src/json-rpc/test/integration.test.ts b/yarn-project/foundation/src/json-rpc/test/integration.test.ts index b30565155f9e..a68a12431316 100644 --- a/yarn-project/foundation/src/json-rpc/test/integration.test.ts +++ b/yarn-project/foundation/src/json-rpc/test/integration.test.ts @@ -37,6 +37,24 @@ describe('JsonRpc integration', () => { expect(note).toBeInstanceOf(TestNote); }); + it('calls an RPC function without an optional parameter', async () => { + const notes = await client.getNotes(); + expect(notes).toEqual(testNotes); + expect(notes.every(note => note instanceof TestNote)).toBe(true); + }); + + it('calls an RPC function with an optional parameter', async () => { + const notes = await client.getNotes(1); + expect(notes).toEqual([testNotes[0]]); + expect(notes.every(note => note instanceof TestNote)).toBe(true); + }); + + it('calls an RPC function with a bigint parameter', async () => { + const notes = await client.getBigNotes(1n); + expect(notes).toEqual([testNotes[0]]); + expect(notes.every(note => note instanceof TestNote)).toBe(true); + }); + it('calls an RPC function with incorrect parameter type', async () => { await expect(() => client.getNote('foo' as any)).rejects.toThrow('Expected number, received string'); }); diff --git a/yarn-project/foundation/src/json-rpc/test/integration.ts b/yarn-project/foundation/src/json-rpc/test/integration.ts index a78a61659e48..a7d9c38ffacd 100644 --- a/yarn-project/foundation/src/json-rpc/test/integration.ts +++ b/yarn-project/foundation/src/json-rpc/test/integration.ts @@ -1,6 +1,7 @@ import type http from 'http'; import { type ApiSchemaFor } from '../../schemas/api.js'; +import { makeFetch } from '../client/json_rpc_client.js'; import { createSafeJsonRpcClient } from '../client/safe_json_rpc_client.js'; import { startHttpRpcServer } from '../server/json_rpc_server.js'; import { type SafeJsonRpcServer, createSafeJsonRpcServer } from '../server/safe_json_rpc_server.js'; @@ -17,6 +18,7 @@ export async function createJsonRpcTestSetup( ): Promise> { const server = createSafeJsonRpcServer(handler, schema); const httpServer = await startHttpRpcServer(server, { host: '127.0.0.1' }); - const client = createSafeJsonRpcClient(`http://127.0.0.1:${httpServer.port}`, schema); + const noRetryFetch = makeFetch([], true); + const client = createSafeJsonRpcClient(`http://127.0.0.1:${httpServer.port}`, schema, false, false, noRetryFetch); return { server, client, httpServer }; } diff --git a/yarn-project/foundation/src/schemas/api.ts b/yarn-project/foundation/src/schemas/api.ts index a2e77bf970f0..f3d5f37c4c10 100644 --- a/yarn-project/foundation/src/schemas/api.ts +++ b/yarn-project/foundation/src/schemas/api.ts @@ -1,10 +1,20 @@ import { type z } from 'zod'; type ZodFor = z.ZodType; + +// This monstruosity is used for mapping function arguments to their schema representation. +// The complexity is required to satisfy ZodTuple which requires a fixed length tuple and +// has a very annoying type of [] | [ZodTypeAny, ...ZodTypeAny], and most types fail to match +// the second option. While a purely recursive approach works, it fails when trying to deal +// with optional arguments (ie optional items in the tuple), and ts does not really like them +// during a recursion and fails with infinite stack depth. +// This type appears to satisfy everyone. Everyone but me. type ZodMapTypes = T extends [] ? [] - : T extends [infer Head, ...infer Rest] - ? [ZodFor, ...ZodMapTypes] + : T extends [item: infer Head, ...infer Rest] + ? [ZodFor, ...{ [K in keyof Rest]: ZodFor }] + : T extends [item?: infer Head, ...infer Rest] + ? [z.ZodOptional>, ...{ [K in keyof Rest]: ZodFor }] : never; /** Maps all functions in an interface to their schema representation. */ diff --git a/yarn-project/foundation/src/schemas/hex.ts b/yarn-project/foundation/src/schemas/hex.ts deleted file mode 100644 index 377f7a38c67d..000000000000 --- a/yarn-project/foundation/src/schemas/hex.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type ZodType } from 'zod'; - -import { schemas } from './schemas.js'; - -export function hexSchemaFor( - klazz: TClass, -): ZodType { - return schemas.Hex.transform(klazz.fromString); -} diff --git a/yarn-project/foundation/src/schemas/index.ts b/yarn-project/foundation/src/schemas/index.ts index 106f78fd7bd5..828af7de22fd 100644 --- a/yarn-project/foundation/src/schemas/index.ts +++ b/yarn-project/foundation/src/schemas/index.ts @@ -1,4 +1,4 @@ export * from './api.js'; export * from './parse.js'; export * from './schemas.js'; -export * from './hex.js'; +export { hexSchemaFor, maybeStructuredStringSchemaFor } from './utils.js'; diff --git a/yarn-project/foundation/src/schemas/parse.test.ts b/yarn-project/foundation/src/schemas/parse.test.ts new file mode 100644 index 000000000000..539115e7145f --- /dev/null +++ b/yarn-project/foundation/src/schemas/parse.test.ts @@ -0,0 +1,54 @@ +import { z } from 'zod'; + +import { parseWithOptionals } from './parse.js'; + +describe('parse', () => { + it('parses arguments without optionals', () => { + expect(parseWithOptionals([1, 2], z.tuple([z.number(), z.number()]))).toEqual([1, 2]); + }); + + it('handles providing all optional arguments', () => { + const schema = z.tuple([z.number(), z.number().optional(), z.number().optional()]); + expect(parseWithOptionals([1, 2, 3], schema)).toEqual([1, 2, 3]); + }); + + it('handles some missing optional arguments', () => { + const schema = z.tuple([z.number(), z.number().optional(), z.number().optional()]); + expect(parseWithOptionals([1, 2], schema)).toEqual([1, 2, undefined]); + }); + + it('handles all missing optional arguments', () => { + const schema = z.tuple([z.number(), z.number().optional(), z.number().optional()]); + expect(parseWithOptionals([1], schema)).toEqual([1, undefined, undefined]); + }); + + it('handles no arguments if all optional', () => { + const schema = z.tuple([z.number().optional(), z.number().optional(), z.number().optional()]); + expect(parseWithOptionals([], schema)).toEqual([undefined, undefined, undefined]); + }); + + it('fails if a required argument is missing', () => { + const schema = z.tuple([z.number(), z.number(), z.number().optional()]); + expect(() => parseWithOptionals([1], schema)).toThrow(); + }); + + it('handles coerced bigint', () => { + const schema = z.tuple([z.coerce.bigint()]); + expect(parseWithOptionals(['1'], schema)).toEqual([1n]); + }); + + it('handles coerced optional bigint', () => { + const schema = z.tuple([z.coerce.bigint().optional()]); + expect(parseWithOptionals(['1'], schema)).toEqual([1n]); + }); + + it('handles missing coerced optional bigint', () => { + const schema = z.tuple([z.coerce.bigint().optional()]); + expect(parseWithOptionals([], schema)).toEqual([undefined]); + }); + + it('fails on missing coerced required bigint', () => { + const schema = z.tuple([z.coerce.bigint()]); + expect(() => parseWithOptionals([], schema)).toThrow(); + }); +}); diff --git a/yarn-project/foundation/src/schemas/parse.ts b/yarn-project/foundation/src/schemas/parse.ts index d49a690ec13e..d6eac1e98511 100644 --- a/yarn-project/foundation/src/schemas/parse.ts +++ b/yarn-project/foundation/src/schemas/parse.ts @@ -1,6 +1,28 @@ import { z } from 'zod'; +import { times } from '../collection/array.js'; + /** Parses the given arguments using a tuple from the provided schemas. */ export function parse(args: IArguments, ...schemas: T) { return z.tuple(schemas).parse(args); } + +/** + * Parses the given arguments against a tuple, allowing empty for optional items. + * @dev Zod doesn't like tuplues with optional items. See https://github.com/colinhacks/zod/discussions/949. + */ +export function parseWithOptionals(args: any[], schema: T): T['_output'] { + const missingCount = schema.items.length - args.length; + const optionalCount = schema.items.filter(isOptional).length; + const toParse = missingCount <= optionalCount ? args.concat(times(missingCount, () => undefined)) : args; + return schema.parse(toParse); +} + +function isOptional(schema: z.ZodTypeAny) { + try { + return schema.isOptional(); + } catch (err) { + // See https://github.com/colinhacks/zod/issues/1911 + return schema._def.typeName === 'ZodOptional'; + } +} diff --git a/yarn-project/foundation/src/schemas/schemas.ts b/yarn-project/foundation/src/schemas/schemas.ts index ecb8c76e59ae..a3fa0368d5b2 100644 --- a/yarn-project/foundation/src/schemas/schemas.ts +++ b/yarn-project/foundation/src/schemas/schemas.ts @@ -1,23 +1,30 @@ import { z } from 'zod'; +import { FunctionSelector } from '../abi/function_selector.js'; +import { AztecAddress } from '../aztec-address/index.js'; +import { Buffer32 } from '../buffer/buffer32.js'; import { EthAddress } from '../eth-address/index.js'; import { Signature } from '../eth-signature/eth_signature.js'; +import { Fq, Fr } from '../fields/fields.js'; import { hasHexPrefix, isHex, withoutHexPrefix } from '../string/index.js'; +import { hexSchema, maybeStructuredStringSchemaFor } from './utils.js'; -/** - * Validation schemas for common types. Every schema should match its type toJSON. - */ +/** Validation schemas for common types. Every schema must match its toJSON. */ export const schemas = { /** Accepts both a 0x string and a structured { type: EthAddress, value: '0x...' } */ - EthAddress: z - .union([ - z.string().refine(EthAddress.isAddress, 'Not a valid Ethereum address'), - z.object({ - type: z.literal('EthAddress'), - value: z.string().refine(EthAddress.isAddress, 'Not a valid Ethereum address'), - }), - ]) - .transform(input => EthAddress.fromString(typeof input === 'string' ? input : input.value)), + EthAddress: maybeStructuredStringSchemaFor('EthAddress', EthAddress, EthAddress.isAddress), + + /** Accepts both a 0x string and a structured { type: AztecAddress, value: '0x...' } */ + AztecAddress: maybeStructuredStringSchemaFor('AztecAddress', AztecAddress, AztecAddress.isAddress), + + /** Accepts both a 0x string and a structured type. */ + FunctionSelector: maybeStructuredStringSchemaFor('FunctionSelector', FunctionSelector), + + /** Field element. Accepts a 0x prefixed hex string or a structured type. */ + Fr: maybeStructuredStringSchemaFor('Fr', Fr, isHex), + + /** Field element. Accepts a 0x prefixed hex string or a structured type. */ + Fq: maybeStructuredStringSchemaFor('Fq', Fq, isHex), /** Accepts a 0x string */ Signature: z @@ -32,8 +39,17 @@ export const schemas = { /** Coerces any input to integer number */ Integer: z.coerce.number().int(), - /** Accepts a base64 string or a structured { type: 'Buffer', data: [byte, byte...] } */ - Buffer: z.union([ + /** Coerces input to UInt32 */ + UInt32: z.coerce + .number() + .int() + .max(2 ** 32 - 1), + + /** Accepts a hex string as a Buffer32 type */ + Buffer32: z.string().refine(isHex, 'Not a valid hex string').transform(Buffer32.fromString), + + /** Accepts a base64 string or a structured { type: 'Buffer', data: [byte, byte...] } as a buffer */ + BufferB64: z.union([ z .string() .base64() @@ -46,6 +62,13 @@ export const schemas = { .transform(({ data }) => Buffer.from(data)), ]), + /** Accepts a hex string with optional 0x prefix as a buffer */ + BufferHex: z + .string() + .refine(isHex, 'Not a valid hex string') + .transform(withoutHexPrefix) + .transform(data => Buffer.from(data, 'hex')), + /** Hex string with an optional 0x prefix, which gets removed as part of the parsing */ - Hex: z.string().refine(isHex, 'Not a valid hex string').transform(withoutHexPrefix), + HexString: hexSchema, }; diff --git a/yarn-project/foundation/src/schemas/utils.ts b/yarn-project/foundation/src/schemas/utils.ts new file mode 100644 index 000000000000..8ec488dd249a --- /dev/null +++ b/yarn-project/foundation/src/schemas/utils.ts @@ -0,0 +1,23 @@ +import { type ZodType, z } from 'zod'; + +import { isHex, withoutHexPrefix } from '../string/index.js'; + +export const hexSchema = z.string().refine(isHex, 'Not a valid hex string').transform(withoutHexPrefix); + +export function hexSchemaFor( + klazz: TClass, +): ZodType { + return hexSchema.transform(klazz.fromString); +} + +// TODO(palla/schemas): Delete this class once all serialization of the type { type: string, value: string } are removed. +export function maybeStructuredStringSchemaFor( + name: string, + klazz: TClass, + refinement?: (input: string) => boolean, +): ZodType { + const stringSchema = refinement ? z.string().refine(refinement, `Not a valid ${name}`) : z.string(); + return z + .union([stringSchema, z.object({ type: z.literal(name), value: stringSchema })]) + .transform(input => klazz.fromString(typeof input === 'string' ? input : input.value)); +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 16c668952a98..0258663963a1 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -418,6 +418,7 @@ __metadata: ts-node: ^10.9.1 tslib: ^2.4.0 typescript: ^5.0.4 + zod: ^3.23.8 languageName: unknown linkType: soft