diff --git a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts index c1d850218ef..49449ffcbcb 100644 --- a/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/acir-simulator/src/acvm/oracle/oracle.ts @@ -1,6 +1,6 @@ import { MerkleTreeId, UnencryptedL2Log } from '@aztec/circuit-types'; import { RETURN_VALUES_LENGTH } from '@aztec/circuits.js'; -import { FunctionSelector } from '@aztec/foundation/abi'; +import { EventSelector, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { padArrayEnd } from '@aztec/foundation/collection'; import { Fr, Point } from '@aztec/foundation/fields'; @@ -269,7 +269,7 @@ export class Oracle { const logPayload = Buffer.concat(message.map(charBuffer => Fr.fromString(charBuffer).toBuffer().subarray(-1))); const log = new UnencryptedL2Log( AztecAddress.fromString(contractAddress), - FunctionSelector.fromField(fromACVMField(eventSelector)), // TODO https://github.com/AztecProtocol/aztec-packages/issues/2632 + EventSelector.fromField(fromACVMField(eventSelector)), logPayload, ); diff --git a/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts b/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts index 184801cd9bd..f5ae65370b3 100644 --- a/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts +++ b/yarn-project/circuit-types/src/aztec_node/rpc/aztec_node_client.ts @@ -1,4 +1,5 @@ import { BlockHeader, FunctionSelector } from '@aztec/circuits.js'; +import { EventSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; @@ -29,6 +30,7 @@ export function createAztecNodeClient(url: string, fetch = defaultFetch): AztecN ExtendedUnencryptedL2Log, ContractData, Fr, + EventSelector, FunctionSelector, BlockHeader, L2Block, diff --git a/yarn-project/circuit-types/src/logs/log_filter.ts b/yarn-project/circuit-types/src/logs/log_filter.ts index c77c73b2d90..0b472bb9939 100644 --- a/yarn-project/circuit-types/src/logs/log_filter.ts +++ b/yarn-project/circuit-types/src/logs/log_filter.ts @@ -1,4 +1,5 @@ -import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; +import { AztecAddress } from '@aztec/circuits.js'; +import { EventSelector } from '@aztec/foundation/abi'; import { TxHash } from '../tx/tx_hash.js'; import { LogId } from './log_id.js'; @@ -8,25 +9,16 @@ import { LogId } from './log_id.js'; * @remarks This filter is applied as an intersection of all it's params. */ export type LogFilter = { - /** - * Hash of a transaction from which to fetch the logs. - */ + /** Hash of a transaction from which to fetch the logs. */ txHash?: TxHash; - /** - * The block number from which to start fetching logs (inclusive). - */ + /** The block number from which to start fetching logs (inclusive). */ fromBlock?: number; /** The block number until which to fetch logs (not inclusive). */ toBlock?: number; - /** - * Log id after which to start fetching logs. - */ + /** Log id after which to start fetching logs. */ afterLog?: LogId; /** The contract address to filter logs by. */ contractAddress?: AztecAddress; - /** - * Selector of the event/log topic. - * TODO: https://github.com/AztecProtocol/aztec-packages/issues/2632 - */ - selector?: FunctionSelector; + /** Selector of the event/log topic. */ + selector?: EventSelector; }; 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 777c9811db4..eb171ebb374 100644 --- a/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts +++ b/yarn-project/circuit-types/src/logs/unencrypted_l2_log.ts @@ -1,4 +1,5 @@ -import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; +import { AztecAddress } from '@aztec/circuits.js'; +import { EventSelector } from '@aztec/foundation/abi'; import { BufferReader, prefixBufferWithLength } from '@aztec/foundation/serialize'; import { randomBytes } from 'crypto'; @@ -17,17 +18,14 @@ export class UnencryptedL2Log { * TODO: Optimize this once it makes sense. */ public readonly contractAddress: AztecAddress, - /** - * Selector of the event/log topic. - * TODO: https://github.com/AztecProtocol/aztec-packages/issues/2632 - */ - public readonly selector: FunctionSelector, + /** Selector of the event/log topic. */ + public readonly selector: EventSelector, /** The data contents of the log. */ public readonly data: Buffer, ) {} get length(): number { - return FunctionSelector.SIZE + this.data.length; + return EventSelector.SIZE + this.data.length; } /** @@ -60,7 +58,7 @@ export class UnencryptedL2Log { public static fromBuffer(buffer: Buffer | BufferReader): UnencryptedL2Log { const reader = BufferReader.asReader(buffer); const contractAddress = AztecAddress.fromBuffer(reader); - const selector = FunctionSelector.fromBuffer(reader); + const selector = EventSelector.fromBuffer(reader); const data = reader.readBuffer(); return new UnencryptedL2Log(contractAddress, selector, data); } @@ -71,8 +69,8 @@ export class UnencryptedL2Log { */ public static random(): UnencryptedL2Log { const contractAddress = AztecAddress.random(); - const selector = new FunctionSelector(Math.floor(Math.random() * (2 ** (FunctionSelector.SIZE * 8) - 1))); - const dataLength = FunctionSelector.SIZE + Math.floor(Math.random() * 200); + const selector = new EventSelector(Math.floor(Math.random() * (2 ** (EventSelector.SIZE * 8) - 1))); + const dataLength = EventSelector.SIZE + Math.floor(Math.random() * 200); const data = randomBytes(dataLength); return new UnencryptedL2Log(contractAddress, selector, data); } diff --git a/yarn-project/cli/src/cmds/get_logs.ts b/yarn-project/cli/src/cmds/get_logs.ts index 73a6501b9cf..1a48dd920cc 100644 --- a/yarn-project/cli/src/cmds/get_logs.ts +++ b/yarn-project/cli/src/cmds/get_logs.ts @@ -1,4 +1,5 @@ -import { AztecAddress, FunctionSelector, LogFilter, LogId, TxHash } from '@aztec/aztec.js'; +import { AztecAddress, LogFilter, LogId, TxHash } from '@aztec/aztec.js'; +import { EventSelector } from '@aztec/foundation/abi'; import { DebugLogger, LogFn } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; @@ -13,7 +14,7 @@ export async function getLogs( toBlock: number, afterLog: LogId, contractAddress: AztecAddress, - selector: FunctionSelector, + selector: EventSelector, rpcUrl: string, follow: boolean, debugLogger: DebugLogger, diff --git a/yarn-project/foundation/src/abi/function_selector.ts b/yarn-project/foundation/src/abi/function_selector.ts deleted file mode 100644 index de0b879cf6b..00000000000 --- a/yarn-project/foundation/src/abi/function_selector.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { toBigIntBE, toBufferBE } from '@aztec/foundation/bigint-buffer'; -import { BufferReader } from '@aztec/foundation/serialize'; - -import { keccak } from '../crypto/keccak/index.js'; -import { Fr } from '../fields/index.js'; -import { ABIParameter } from './abi.js'; -import { decodeFunctionSignature } from './decoder.js'; - -/** - * A function selector is the first 4 bytes of the hash of a function signature. - */ -export class FunctionSelector { - /** - * The size of the function selector in bytes. - */ - public static SIZE = 4; - - constructor(/** number representing the function selector */ public value: number) { - if (value > 2 ** (FunctionSelector.SIZE * 8) - 1) { - throw new Error(`Function selector must fit in ${FunctionSelector.SIZE} bytes.`); - } - } - - /** - * Checks if the function selector is empty (all bytes are 0). - * @returns True if the function selector is empty (all bytes are 0). - */ - public isEmpty(): boolean { - return this.value === 0; - } - - /** - * Serialize as a buffer. - * @param bufferSize - The buffer size. - * @returns The buffer. - */ - toBuffer(bufferSize = FunctionSelector.SIZE): Buffer { - return toBufferBE(BigInt(this.value), bufferSize); - } - - /** - * Serialize as a hex string. - * @returns The string. - */ - toString(): string { - return this.toBuffer().toString('hex'); - } - - /** - * Checks if this function selector is equal to another. - * @param other - The other function selector. - * @returns True if the function selectors are equal. - */ - equals(other: FunctionSelector): boolean { - return this.value === other.value; - } - - /** - * Deserializes from a buffer or reader, corresponding to a write in cpp. - * @param buffer - Buffer or BufferReader to read from. - * @returns The FunctionSelector. - */ - static fromBuffer(buffer: Buffer | BufferReader): FunctionSelector { - const reader = BufferReader.asReader(buffer); - const value = Number(toBigIntBE(reader.readBytes(FunctionSelector.SIZE))); - return new FunctionSelector(value); - } - - /** - * Returns a new field with the same contents as this EthAddress. - * - * @returns An Fr instance. - */ - public toField() { - return new Fr(BigInt(this.value)); - } - - /** - * Converts a field to function selector. - * @param fr - The field to convert. - * @returns The function selector. - */ - static fromField(fr: Fr): FunctionSelector { - return new FunctionSelector(Number(fr.toBigInt())); - } - - /** - * Creates a function selector from a signature. - * @param signature - Signature of the function to generate the selector for (e.g. "transfer(field,field)"). - * @returns Function selector. - */ - static fromSignature(signature: string): FunctionSelector { - // throw if signature contains whitespace - if (/\s/.test(signature)) { - throw new Error('Function Signature cannot contain whitespace'); - } - return FunctionSelector.fromBuffer(keccak(Buffer.from(signature)).subarray(0, FunctionSelector.SIZE)); - } - - /** - * Creates a function selector for a given function name and parameters. - * @param name - The name of the function. - * @param parameters - An array of ABIParameter objects, each containing the type information of a function parameter. - * @returns A Buffer containing the 4-byte function selector. - */ - static fromNameAndParameters(name: string, parameters: ABIParameter[]) { - const signature = decodeFunctionSignature(name, parameters); - const selector = FunctionSelector.fromSignature(signature); - // If using the debug logger here it kill the typing in the `server_world_state_synchronizer` and jest tests. - // console.log(`Function selector for ${signature} is ${selector}`); - return selector; - } - - /** - * Create an AztecAddress instance from a hex-encoded string. - * The input 'address' should be prefixed with '0x' or not, and have exactly 64 hex characters. - * Throws an error if the input length is invalid or address value is out of range. - * - * @param selector - The hex-encoded string representing the Aztec address. - * @returns An AztecAddress instance. - */ - static fromString(selector: string) { - const buf = Buffer.from(selector.replace(/^0x/i, ''), 'hex'); - if (buf.length !== FunctionSelector.SIZE) { - throw new Error(`Invalid FunctionSelector length ${buf.length}.`); - } - return FunctionSelector.fromBuffer(buf); - } - - /** - * Creates an empty function selector. - * @returns An empty function selector. - */ - static empty(): FunctionSelector { - return new FunctionSelector(0); - } -} diff --git a/yarn-project/foundation/src/abi/index.ts b/yarn-project/foundation/src/abi/index.ts index 8ec67fd2c74..8369df9cd6e 100644 --- a/yarn-project/foundation/src/abi/index.ts +++ b/yarn-project/foundation/src/abi/index.ts @@ -2,5 +2,5 @@ export * from './abi.js'; export * from './abi_coder.js'; export * from './encoder.js'; export * from './decoder.js'; -export * from './function_selector.js'; +export * from './selector.js'; export * from './utils.js'; diff --git a/yarn-project/foundation/src/abi/selector.ts b/yarn-project/foundation/src/abi/selector.ts new file mode 100644 index 00000000000..0c631418b29 --- /dev/null +++ b/yarn-project/foundation/src/abi/selector.ts @@ -0,0 +1,210 @@ +import { fromHex, toBigIntBE, toBufferBE } from '@aztec/foundation/bigint-buffer'; +import { BufferReader } from '@aztec/foundation/serialize'; + +import { keccak } from '../crypto/keccak/index.js'; +import { Fr } from '../fields/index.js'; +import { ABIParameter } from './abi.js'; +import { decodeFunctionSignature } from './decoder.js'; + +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ + +/** A selector is the first 4 bytes of the hash of a signature. */ +abstract class Selector { + /** The size of the selector in bytes. */ + public static SIZE = 4; + + constructor(/** Value of the selector */ public value: number) { + if (value > 2 ** (Selector.SIZE * 8) - 1) { + throw new Error(`selector must fit in ${Selector.SIZE} bytes.`); + } + } + + /** + * Checks if the selector is empty (all bytes are 0). + * @returns True if the selector is empty (all bytes are 0). + */ + public isEmpty(): boolean { + return this.value === 0; + } + + /** + * Serialize as a buffer. + * @param bufferSize - The buffer size. + * @returns The buffer. + */ + toBuffer(bufferSize = Selector.SIZE): Buffer { + return toBufferBE(BigInt(this.value), bufferSize); + } + + /** + * Serialize as a hex string. + * @returns The string. + */ + toString(): string { + return this.toBuffer().toString('hex'); + } + + /** + * Checks if this selector is equal to another. + * @param other - The other selector. + * @returns True if the selectors are equal. + */ + equals(other: Selector): boolean { + return this.value === other.value; + } + + /** + * Returns a new field with the same contents as this EthAddress. + * + * @returns An Fr instance. + */ + public toField() { + return new Fr(BigInt(this.value)); + } +} + +/** Function selector branding */ +export interface FunctionSelector { + /** Brand. */ + _branding: 'FunctionSelector'; +} + +/** A function selector is the first 4 bytes of the hash of a function signature. */ +export class FunctionSelector extends Selector { + /** + * Deserializes from a buffer or reader, corresponding to a write in cpp. + * @param buffer - Buffer or BufferReader to read from. + * @returns The Selector. + */ + static fromBuffer(buffer: Buffer | BufferReader) { + const reader = BufferReader.asReader(buffer); + const value = Number(toBigIntBE(reader.readBytes(Selector.SIZE))); + return new FunctionSelector(value); + } + + /** + * Converts a field to selector. + * @param fr - The field to convert. + * @returns The selector. + */ + static fromField(fr: Fr) { + return new FunctionSelector(Number(fr.toBigInt())); + } + + /** + * Creates a selector from a signature. + * @param signature - Signature to generate the selector for (e.g. "transfer(field,field)"). + * @returns selector. + */ + static fromSignature(signature: string) { + // throw if signature contains whitespace + if (/\s/.test(signature)) { + throw new Error('Signature cannot contain whitespace'); + } + return FunctionSelector.fromBuffer(keccak(Buffer.from(signature)).subarray(0, Selector.SIZE)); + } + + /** + * Create a Selector instance from a hex-encoded string. + * The input 'address' should be prefixed with '0x' or not, and have exactly 64 hex characters. + * Throws an error if the input length is invalid or address value is out of range. + * + * @param selector - The hex-encoded string representing the Selector. + * @returns An Selector instance. + */ + static fromString(selector: string) { + const buf = fromHex(selector); + if (buf.length !== Selector.SIZE) { + throw new Error(`Invalid Selector length ${buf.length} (expected ${Selector.SIZE}).`); + } + return FunctionSelector.fromBuffer(buf); + } + + /** + * Creates an empty selector. + * @returns An empty selector. + */ + static empty() { + return new FunctionSelector(0); + } + + /** + * Creates a function selector for a given function name and parameters. + * @param name - The name of the function. + * @param parameters - An array of ABIParameter objects, each containing the type information of a function parameter. + * @returns A Buffer containing the 4-byte selector. + */ + static fromNameAndParameters(name: string, parameters: ABIParameter[]) { + const signature = decodeFunctionSignature(name, parameters); + const selector = this.fromSignature(signature); + // If using the debug logger here it kill the typing in the `server_world_state_synchronizer` and jest tests. + // console.log(`selector for ${signature} is ${selector}`); + return selector; + } +} + +/** Event selector branding */ +export interface EventSelector { + /** Brand. */ + _branding: 'EventSelector'; +} + +/** An event selector is the first 4 bytes of the hash of an event signature. */ +export class EventSelector extends Selector { + /** + * Deserializes from a buffer or reader, corresponding to a write in cpp. + * @param buffer - Buffer or BufferReader to read from. + * @returns The Selector. + */ + static fromBuffer(buffer: Buffer | BufferReader) { + const reader = BufferReader.asReader(buffer); + const value = Number(toBigIntBE(reader.readBytes(Selector.SIZE))); + return new EventSelector(value); + } + + /** + * Converts a field to selector. + * @param fr - The field to convert. + * @returns The selector. + */ + static fromField(fr: Fr) { + return new EventSelector(Number(fr.toBigInt())); + } + + /** + * Creates a selector from a signature. + * @param signature - Signature to generate the selector for (e.g. "transfer(field,field)"). + * @returns selector. + */ + static fromSignature(signature: string) { + // throw if signature contains whitespace + if (/\s/.test(signature)) { + throw new Error('Signature cannot contain whitespace'); + } + return EventSelector.fromBuffer(keccak(Buffer.from(signature)).subarray(0, Selector.SIZE)); + } + + /** + * Create a Selector instance from a hex-encoded string. + * The input 'address' should be prefixed with '0x' or not, and have exactly 64 hex characters. + * Throws an error if the input length is invalid or address value is out of range. + * + * @param selector - The hex-encoded string representing the Selector. + * @returns An Selector instance. + */ + static fromString(selector: string) { + const buf = fromHex(selector); + if (buf.length !== Selector.SIZE) { + throw new Error(`Invalid Selector length ${buf.length} (expected ${Selector.SIZE}).`); + } + return EventSelector.fromBuffer(buf); + } + + /** + * Creates an empty selector. + * @returns An empty selector. + */ + static empty() { + return new EventSelector(0); + } +} diff --git a/yarn-project/foundation/src/bigint-buffer/bigint-buffer.test.ts b/yarn-project/foundation/src/bigint-buffer/bigint-buffer.test.ts index d3f6df20d85..6280eca4753 100644 --- a/yarn-project/foundation/src/bigint-buffer/bigint-buffer.test.ts +++ b/yarn-project/foundation/src/bigint-buffer/bigint-buffer.test.ts @@ -1,4 +1,4 @@ -import { toHex } from './index.js'; +import { fromHex, toHex } from './index.js'; describe('bigint-buffer', () => { describe('toHex', () => { @@ -18,4 +18,35 @@ describe('bigint-buffer', () => { expect(toHex(0n, true)).toEqual('0x0000000000000000000000000000000000000000000000000000000000000000'); }); }); + + describe('fromHex', () => { + it('should convert a valid hex string to a Buffer', () => { + const hexString = '0x1234567890abcdef'; + const expectedBuffer = Buffer.from('1234567890abcdef', 'hex'); + const result = fromHex(hexString); + expect(result).toEqual(expectedBuffer); + }); + + it('should convert a valid hex string without prefix to a Buffer', () => { + const hexString = '1234567890abcdef'; + const expectedBuffer = Buffer.from('1234567890abcdef', 'hex'); + const result = fromHex(hexString); + expect(result).toEqual(expectedBuffer); + }); + + it('should throw an error for an invalid hex string', () => { + const invalidHexString = '0x12345G'; + expect(() => fromHex(invalidHexString)).toThrowError('Invalid hex string: 0x12345G'); + }); + + it('should throw an error for an odd-length hex string', () => { + const oddLengthHexString = '0x1234567'; + expect(() => fromHex(oddLengthHexString)).toThrowError('Invalid hex string: 0x1234567'); + }); + + it('should handle an empty hex string', () => { + expect(fromHex('')).toEqual(Buffer.alloc(0)); + expect(fromHex('0x')).toEqual(Buffer.alloc(0)); + }); + }); }); diff --git a/yarn-project/foundation/src/bigint-buffer/index.ts b/yarn-project/foundation/src/bigint-buffer/index.ts index 88c42d6a46a..a97045ac4be 100644 --- a/yarn-project/foundation/src/bigint-buffer/index.ts +++ b/yarn-project/foundation/src/bigint-buffer/index.ts @@ -72,3 +72,16 @@ export function toHex(num: bigint, padTo32 = false): `0x${string}` { const paddedStr = str.padStart(padTo32 ? 64 : targetLen, '0'); return `0x${paddedStr}`; } + +/** + * Converts a hex string to a buffer. Throws if input is not a valid hex string. + * @param value - The hex string to convert. May be 0x prefixed or not. + * @returns A buffer. + */ +export function fromHex(value: string): Buffer { + const hexRegex = /^(0x)?[0-9a-fA-F]*$/; + if (!hexRegex.test(value) || value.length % 2 !== 0) { + throw new Error(`Invalid hex string: ${value}`); + } + return Buffer.from(value.replace(/^0x/i, ''), 'hex'); +}