diff --git a/yarn-project/archiver/package.json b/yarn-project/archiver/package.json index b837e1d9b38e..6a6048af0d3a 100644 --- a/yarn-project/archiver/package.json +++ b/yarn-project/archiver/package.json @@ -41,7 +41,7 @@ "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", "@aztec/l1-artifacts": "workspace:^", - "@types/lodash.omit": "^4.5.7", + "@aztec/types": "workspace:^", "debug": "^4.3.4", "lmdb": "^2.9.1", "lodash.omit": "^4.5.0", @@ -54,6 +54,7 @@ "@jest/globals": "^29.5.0", "@types/debug": "^4.1.7", "@types/jest": "^29.5.0", + "@types/lodash.omit": "^4.5.7", "@types/node": "^18.15.11", "@types/ws": "^8.5.4", "concurrently": "^8.0.1", diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 26eaaa71eeb0..780020a012c6 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -15,7 +15,7 @@ import { LogType, TxHash, } from '@aztec/circuit-types'; -import { FunctionSelector, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/circuits.js'; +import { FunctionSelector, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, getContractClassId } from '@aztec/circuits.js'; import { createEthereumChain } from '@aztec/ethereum'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { padArrayEnd } from '@aztec/foundation/collection'; @@ -23,6 +23,12 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; +import { + ContractClass, + ContractClassWithId, + ContractInstance, + ContractInstanceWithAddress, +} from '@aztec/types/contracts'; import omit from 'lodash.omit'; import { Chain, HttpTransport, PublicClient, createPublicClient, http } from 'viem'; @@ -268,6 +274,7 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource this.log(`Retrieved extended contract data for l2 block number: ${l2BlockNum}`); if (l2BlockNum <= lastKnownL2BlockNum) { await this.store.addExtendedContractData(contracts, l2BlockNum); + await this.storeContractDataAsClassesAndInstances(contracts, l2BlockNum); } }), ); @@ -294,6 +301,24 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource ); } + /** + * Stores extended contract data as classes and instances. + * Temporary solution until we source this data from the contract class registerer and instance deployer. + * @param contracts - The extended contract data to be stored. + * @param l2BlockNum - The L2 block number to which the contract data corresponds. + */ + async storeContractDataAsClassesAndInstances(contracts: ExtendedContractData[], l2BlockNum: number) { + const classesAndInstances = contracts.map(extendedContractDataToContractClassAndInstance); + await this.store.addContractClasses( + classesAndInstances.map(([c, _]) => c), + l2BlockNum, + ); + await this.store.addContractInstances( + classesAndInstances.map(([_, i]) => i), + l2BlockNum, + ); + } + /** * Stops the archiver. * @returns A promise signalling completion of the stop process. @@ -440,3 +465,34 @@ export class Archiver implements L2BlockSource, L2LogsSource, ContractDataSource return this.store.getConfirmedL1ToL2Message(messageKey); } } + +/** Converts ExtendedContractData into contract classes and instances. */ +function extendedContractDataToContractClassAndInstance( + data: ExtendedContractData, +): [ContractClassWithId, ContractInstanceWithAddress] { + const contractClass: ContractClass = { + version: 1, + artifactHash: Fr.ZERO, + publicFunctions: data.publicFunctions.map(f => ({ + selector: f.selector, + bytecode: f.bytecode, + isInternal: f.isInternal, + })), + privateFunctions: [], + packedBytecode: data.bytecode, + }; + const contractClassId = getContractClassId(contractClass); + const contractInstance: ContractInstance = { + version: 1, + salt: Fr.ZERO, + contractClassId, + initializationHash: Fr.ZERO, + portalContractAddress: data.contractData.portalContractAddress, + publicKeysHash: data.partialAddress, + }; + const address = data.contractData.contractAddress; + return [ + { ...contractClass, id: contractClassId }, + { ...contractInstance, address }, + ]; +} diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 0d4c1f13c265..c8d033d40fd2 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -12,6 +12,7 @@ import { } from '@aztec/circuit-types'; import { Fr } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { ContractClassWithId, ContractInstanceWithAddress } from '@aztec/types/contracts'; /** * Represents the latest L1 block processed by the archiver for various objects in L2. @@ -167,4 +168,32 @@ export interface ArchiverDataStore { * Gets the last L1 block number processed by the archiver */ getL1BlockNumber(): Promise; + + /** + * Add new contract classes from an L2 block to the store's list. + * @param data - List of contract classes to be added. + * @param blockNumber - Number of the L2 block the contracts were registered in. + * @returns True if the operation is successful. + */ + addContractClasses(data: ContractClassWithId[], blockNumber: number): Promise; + + /** + * Returns a contract class given its id, or undefined if not exists. + * @param id - Id of the contract class. + */ + getContractClass(id: Fr): Promise; + + /** + * Add new contract instances from an L2 block to the store's list. + * @param data - List of contract instances to be added. + * @param blockNumber - Number of the L2 block the instances were deployed in. + * @returns True if the operation is successful. + */ + addContractInstances(data: ContractInstanceWithAddress[], blockNumber: number): Promise; + + /** + * Returns a contract instance given its address, or undefined if not exists. + * @param address - Address of the contract. + */ + getContractInstance(address: AztecAddress): Promise; } diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index 788c0894d4e7..592addaefb0a 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -12,6 +12,12 @@ import { import '@aztec/circuit-types/jest'; import { AztecAddress, Fr } from '@aztec/circuits.js'; import { randomBytes } from '@aztec/foundation/crypto'; +import { + ContractClassWithId, + ContractInstanceWithAddress, + SerializableContractClass, + SerializableContractInstance, +} from '@aztec/types/contracts'; import { ArchiverDataStore } from './archiver_store.js'; @@ -320,6 +326,42 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch }); }); + describe('contractInstances', () => { + let contractInstance: ContractInstanceWithAddress; + const blockNum = 10; + + beforeEach(async () => { + contractInstance = { ...SerializableContractInstance.random(), address: AztecAddress.random() }; + await store.addContractInstances([contractInstance], blockNum); + }); + + it('returns previously stored contract instances', async () => { + await expect(store.getContractInstance(contractInstance.address)).resolves.toMatchObject(contractInstance); + }); + + it('returns undefined if contract instance is not found', async () => { + await expect(store.getContractInstance(AztecAddress.random())).resolves.toBeUndefined(); + }); + }); + + describe('contractClasses', () => { + let contractClass: ContractClassWithId; + const blockNum = 10; + + beforeEach(async () => { + contractClass = { ...SerializableContractClass.random(), id: Fr.random() }; + await store.addContractClasses([contractClass], blockNum); + }); + + it('returns previously stored contract class', async () => { + await expect(store.getContractClass(contractClass.id)).resolves.toMatchObject(contractClass); + }); + + it('returns undefined if contract class is not found', async () => { + await expect(store.getContractClass(Fr.random())).resolves.toBeUndefined(); + }); + }); + describe('getContractData', () => { let block: L2Block; beforeEach(async () => { diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts index 025221fe73f4..3a2446fa0d5d 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts @@ -3,7 +3,6 @@ import { AztecAddress } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { AztecKVStore, AztecMap, Range } from '@aztec/kv-store'; -/* eslint-disable */ type BlockIndexValue = [blockNumber: number, index: number]; type BlockContext = { @@ -12,7 +11,6 @@ type BlockContext = { block: Buffer; blockHash: Buffer; }; -/* eslint-enable */ /** * LMDB implementation of the ArchiverDataStore interface. diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/contract_class_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/contract_class_store.ts new file mode 100644 index 000000000000..012ac013632f --- /dev/null +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/contract_class_store.ts @@ -0,0 +1,29 @@ +import { Fr } from '@aztec/foundation/fields'; +import { AztecKVStore, AztecMap } from '@aztec/kv-store'; +import { ContractClassWithId, SerializableContractClass } from '@aztec/types/contracts'; + +/** + * LMDB implementation of the ArchiverDataStore interface. + */ +export class ContractClassStore { + #contractClasses: AztecMap; + + constructor(db: AztecKVStore) { + this.#contractClasses = db.createMap('archiver_contract_classes'); + } + + addContractClass(contractClass: ContractClassWithId): Promise { + return this.#contractClasses.set( + contractClass.id.toString(), + new SerializableContractClass(contractClass).toBuffer(), + ); + } + + getContractClass(id: Fr): ContractClassWithId | undefined { + const contractClass = this.#contractClasses.get(id.toString()); + if (!contractClass) { + return undefined; + } + return { ...SerializableContractClass.fromBuffer(contractClass), id }; + } +} diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/contract_instance_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/contract_instance_store.ts new file mode 100644 index 000000000000..0efc76dc8278 --- /dev/null +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/contract_instance_store.ts @@ -0,0 +1,29 @@ +import { AztecAddress } from '@aztec/circuits.js'; +import { AztecKVStore, AztecMap } from '@aztec/kv-store'; +import { ContractInstanceWithAddress, SerializableContractInstance } from '@aztec/types/contracts'; + +/** + * LMDB implementation of the ArchiverDataStore interface. + */ +export class ContractInstanceStore { + #contractInstances: AztecMap; + + constructor(db: AztecKVStore) { + this.#contractInstances = db.createMap('archiver_contract_instances'); + } + + addContractInstance(contractInstance: ContractInstanceWithAddress): Promise { + return this.#contractInstances.set( + contractInstance.address.toString(), + new SerializableContractInstance(contractInstance).toBuffer(), + ); + } + + getContractInstance(address: AztecAddress): ContractInstanceWithAddress | undefined { + const contractInstance = this.#contractInstances.get(address.toString()); + if (!contractInstance) { + return undefined; + } + return { ...SerializableContractInstance.fromBuffer(contractInstance), address }; + } +} diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index 74be29d9d586..db9f0e4cddd9 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -14,9 +14,12 @@ import { Fr } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { createDebugLogger } from '@aztec/foundation/log'; import { AztecKVStore } from '@aztec/kv-store'; +import { ContractClassWithId, ContractInstanceWithAddress } from '@aztec/types/contracts'; import { ArchiverDataStore, ArchiverL1SynchPoint } from '../archiver_store.js'; import { BlockStore } from './block_store.js'; +import { ContractClassStore } from './contract_class_store.js'; +import { ContractInstanceStore } from './contract_instance_store.js'; import { ContractStore } from './contract_store.js'; import { LogStore } from './log_store.js'; import { MessageStore } from './message_store.js'; @@ -29,6 +32,8 @@ export class KVArchiverDataStore implements ArchiverDataStore { #logStore: LogStore; #contractStore: ContractStore; #messageStore: MessageStore; + #contractClassStore: ContractClassStore; + #contractInstanceStore: ContractInstanceStore; #log = createDebugLogger('aztec:archiver:lmdb'); @@ -37,6 +42,24 @@ export class KVArchiverDataStore implements ArchiverDataStore { this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize); this.#contractStore = new ContractStore(db, this.#blockStore); this.#messageStore = new MessageStore(db); + this.#contractClassStore = new ContractClassStore(db); + this.#contractInstanceStore = new ContractInstanceStore(db); + } + + getContractClass(id: Fr): Promise { + return Promise.resolve(this.#contractClassStore.getContractClass(id)); + } + + getContractInstance(address: AztecAddress): Promise { + return Promise.resolve(this.#contractInstanceStore.getContractInstance(address)); + } + + async addContractClasses(data: ContractClassWithId[], _blockNumber: number): Promise { + return (await Promise.all(data.map(c => this.#contractClassStore.addContractClass(c)))).every(Boolean); + } + + async addContractInstances(data: ContractInstanceWithAddress[], _blockNumber: number): Promise { + return (await Promise.all(data.map(c => this.#contractInstanceStore.addContractInstance(c)))).every(Boolean); } /** diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 62824ae1fe3a..ecd0afda6d81 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -17,6 +17,7 @@ import { } from '@aztec/circuit-types'; import { Fr, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { ContractClassWithId, ContractInstanceWithAddress } from '@aztec/types/contracts'; import { ArchiverDataStore } from '../archiver_store.js'; import { L1ToL2MessageStore, PendingL1ToL2MessageStore } from './l1_to_l2_message_store.js'; @@ -68,6 +69,10 @@ export class MemoryArchiverStore implements ArchiverDataStore { */ private pendingL1ToL2Messages: PendingL1ToL2MessageStore = new PendingL1ToL2MessageStore(); + private contractClasses: Map = new Map(); + + private contractInstances: Map = new Map(); + private lastL1BlockAddedMessages: bigint = 0n; private lastL1BlockCancelledMessages: bigint = 0n; @@ -76,6 +81,28 @@ export class MemoryArchiverStore implements ArchiverDataStore { public readonly maxLogs: number, ) {} + public getContractClass(id: Fr): Promise { + return Promise.resolve(this.contractClasses.get(id.toString())); + } + + public getContractInstance(address: AztecAddress): Promise { + return Promise.resolve(this.contractInstances.get(address.toString())); + } + + public addContractClasses(data: ContractClassWithId[], _blockNumber: number): Promise { + for (const contractClass of data) { + this.contractClasses.set(contractClass.id.toString(), contractClass); + } + return Promise.resolve(true); + } + + public addContractInstances(data: ContractInstanceWithAddress[], _blockNumber: number): Promise { + for (const contractInstance of data) { + this.contractInstances.set(contractInstance.address.toString(), contractInstance); + } + return Promise.resolve(true); + } + /** * Append new blocks to the store's list. * @param blocks - The L2 blocks to be added to the store. diff --git a/yarn-project/archiver/tsconfig.json b/yarn-project/archiver/tsconfig.json index 6a0c794ba6dd..69fe51229cc8 100644 --- a/yarn-project/archiver/tsconfig.json +++ b/yarn-project/archiver/tsconfig.json @@ -23,6 +23,9 @@ }, { "path": "../l1-artifacts" + }, + { + "path": "../types" } ], "include": ["src"] diff --git a/yarn-project/circuit-types/src/contract_data.ts b/yarn-project/circuit-types/src/contract_data.ts index 1da2b034bc30..a84e0434d23f 100644 --- a/yarn-project/circuit-types/src/contract_data.ts +++ b/yarn-project/circuit-types/src/contract_data.ts @@ -125,7 +125,7 @@ export class ExtendedContractData { /** The base contract data: aztec & portal addresses. */ public contractData: ContractData, /** Artifacts of public functions. */ - private publicFunctions: EncodedContractFunction[], + public readonly publicFunctions: EncodedContractFunction[], /** Partial addresses of the contract. */ public readonly partialAddress: PartialAddress, /** Public key of the contract. */ diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index e3f18a3ca83b..9aabf460ce33 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -41,6 +41,7 @@ "dependencies": { "@aztec/bb.js": "portal:../../barretenberg/ts", "@aztec/foundation": "workspace:^", + "@aztec/types": "workspace:^", "eslint": "^8.35.0", "lodash.chunk": "^4.2.0", "tslib": "^2.4.0" diff --git a/yarn-project/circuits.js/src/contract/contract_class.test.ts b/yarn-project/circuits.js/src/contract/contract_class.test.ts new file mode 100644 index 000000000000..a60a48941925 --- /dev/null +++ b/yarn-project/circuits.js/src/contract/contract_class.test.ts @@ -0,0 +1,34 @@ +import { Fr } from '@aztec/foundation/fields'; +import { ContractClass } from '@aztec/types/contracts'; + +import { FunctionSelector, getContractClassId } from '../index.js'; + +describe('ContractClass', () => { + describe('getContractClassId', () => { + it('calculates the contract class id', () => { + const contractClass: ContractClass = { + version: 1, + artifactHash: Fr.fromString('0x1234'), + packedBytecode: Buffer.from('123456789012345678901234567890', 'hex'), + privateFunctions: [ + { + selector: FunctionSelector.fromString('0x12345678'), + vkHash: Fr.fromString('0x1234'), + isInternal: false, + }, + ], + publicFunctions: [ + { + selector: FunctionSelector.fromString('0x12345678'), + bytecode: Buffer.from('123456789012345678901234567890', 'hex'), + isInternal: false, + }, + ], + }; + + expect(getContractClassId(contractClass).toString()).toMatchInlineSnapshot( + `"0x1b436781f84669144ec383d6ea5f49b05ccba5c6221ebeb86085443c2a859202"`, + ); + }); + }); +}); diff --git a/yarn-project/circuits.js/src/contract/contract_class.ts b/yarn-project/circuits.js/src/contract/contract_class.ts new file mode 100644 index 000000000000..62882ed5b21e --- /dev/null +++ b/yarn-project/circuits.js/src/contract/contract_class.ts @@ -0,0 +1,78 @@ +import { pedersenHash, sha256 } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; +import { numToUInt8 } from '@aztec/foundation/serialize'; +import { ContractClass, PrivateFunction, PublicFunction } from '@aztec/types/contracts'; + +import { MerkleTreeCalculator } from '../abis/merkle_tree_calculator.js'; +import { FUNCTION_TREE_HEIGHT, GeneratorIndex } from '../constants.gen.js'; + +/** + * Returns the id of a contract class computed as its hash. + * + * ``` + * version = 1 + * private_function_leaves = private_functions.map(fn => pedersen([fn.function_selector as Field, fn.vk_hash], GENERATOR__FUNCTION_LEAF)) + * private_functions_root = merkleize(private_function_leaves) + * bytecode_commitment = calculate_commitment(packed_bytecode) + * contract_class_id = pedersen([version, artifact_hash, private_functions_root, bytecode_commitment], GENERATOR__CLASS_IDENTIFIER) + * ``` + * @param contractClass - Contract class. + * @returns The identifier. + */ +export function getContractClassId(contractClass: ContractClass): Fr { + const privateFunctionsRoot = getPrivateFunctionsRoot(contractClass.privateFunctions); + const publicFunctionsRoot = getPublicFunctionsRoot(contractClass.publicFunctions); // This should be removed once we drop public functions as first class citizens in the protocol + const bytecodeCommitment = getBytecodeCommitment(contractClass.packedBytecode); + return Fr.fromBuffer( + pedersenHash( + [ + numToUInt8(contractClass.version), + contractClass.artifactHash.toBuffer(), + privateFunctionsRoot.toBuffer(), + publicFunctionsRoot.toBuffer(), + bytecodeCommitment.toBuffer(), + ], + GeneratorIndex.CONTRACT_LEAF, // TODO(@spalladino): Review all generator indices in this file + ), + ); +} + +// TODO(@spalladino): Replace with actual implementation +function getBytecodeCommitment(bytecode: Buffer) { + return Fr.fromBufferReduce(sha256(bytecode)); +} + +// Memoize the merkle tree calculators to avoid re-computing the zero-hash for each level in each call +let privateFunctionTreeCalculator: MerkleTreeCalculator | undefined; +let publicFunctionTreeCalculator: MerkleTreeCalculator | undefined; + +const PRIVATE_FUNCTION_SIZE = 2; +const PUBLIC_FUNCTION_SIZE = 2; + +function getPrivateFunctionsRoot(fns: PrivateFunction[]): Fr { + const privateFunctionLeaves = fns.map(fn => + pedersenHash( + [fn.selector, fn.vkHash].map(x => x.toBuffer()), + GeneratorIndex.FUNCTION_LEAF, + ), + ); + if (!privateFunctionTreeCalculator) { + const functionTreeZeroLeaf = pedersenHash(new Array(PRIVATE_FUNCTION_SIZE).fill(Buffer.alloc(32))); + privateFunctionTreeCalculator = new MerkleTreeCalculator(FUNCTION_TREE_HEIGHT, functionTreeZeroLeaf); + } + return Fr.fromBuffer(privateFunctionTreeCalculator.computeTreeRoot(privateFunctionLeaves)); +} + +function getPublicFunctionsRoot(fns: PublicFunction[]): Fr { + const publicFunctionLeaves = fns.map(fn => + pedersenHash( + [fn.selector, getBytecodeCommitment(fn.bytecode)].map(x => x.toBuffer()), + GeneratorIndex.FUNCTION_LEAF, + ), + ); + if (!publicFunctionTreeCalculator) { + const functionTreeZeroLeaf = pedersenHash(new Array(PUBLIC_FUNCTION_SIZE).fill(Buffer.alloc(32))); + publicFunctionTreeCalculator = new MerkleTreeCalculator(FUNCTION_TREE_HEIGHT, functionTreeZeroLeaf); + } + return Fr.fromBuffer(publicFunctionTreeCalculator.computeTreeRoot(publicFunctionLeaves)); +} diff --git a/yarn-project/circuits.js/src/contract/index.ts b/yarn-project/circuits.js/src/contract/index.ts index 38bc67a9e52a..d9ad384d37e2 100644 --- a/yarn-project/circuits.js/src/contract/index.ts +++ b/yarn-project/circuits.js/src/contract/index.ts @@ -1,2 +1,3 @@ export * from './contract_deployment_info.js'; export * from './contract_tree/index.js'; +export * from './contract_class.js'; diff --git a/yarn-project/circuits.js/tsconfig.json b/yarn-project/circuits.js/tsconfig.json index 63f8ab3e9f75..831130c7c84b 100644 --- a/yarn-project/circuits.js/tsconfig.json +++ b/yarn-project/circuits.js/tsconfig.json @@ -8,6 +8,9 @@ "references": [ { "path": "../foundation" + }, + { + "path": "../types" } ], "include": ["src"] diff --git a/yarn-project/foundation/src/abi/selector.ts b/yarn-project/foundation/src/abi/selector.ts index ff70d868f03c..0a6a0eb661cf 100644 --- a/yarn-project/foundation/src/abi/selector.ts +++ b/yarn-project/foundation/src/abi/selector.ts @@ -1,6 +1,8 @@ import { fromHex, toBigIntBE, toBufferBE } from '@aztec/foundation/bigint-buffer'; import { BufferReader } from '@aztec/foundation/serialize'; +import { randomBytes } from 'crypto'; + import { keccak } from '../crypto/keccak/index.js'; import { Fr } from '../fields/index.js'; import { type ABIParameter } from './abi.js'; @@ -15,7 +17,7 @@ abstract class Selector { 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.`); + throw new Error(`Selector must fit in ${Selector.SIZE} bytes (got value ${value}).`); } } @@ -154,6 +156,13 @@ export class FunctionSelector extends Selector { // console.log(`selector for ${signature} is ${selector}`); return selector; } + + /** + * Creates a random instance. + */ + static random() { + return FunctionSelector.fromBuffer(randomBytes(Selector.SIZE)); + } } /** Event selector branding */ diff --git a/yarn-project/foundation/src/eth-address/index.ts b/yarn-project/foundation/src/eth-address/index.ts index 76587ecab263..7594a9ad345c 100644 --- a/yarn-project/foundation/src/eth-address/index.ts +++ b/yarn-project/foundation/src/eth-address/index.ts @@ -232,7 +232,7 @@ export class EthAddress { */ static fromBuffer(buffer: Buffer | BufferReader): EthAddress { const reader = BufferReader.asReader(buffer); - return new EthAddress(reader.readBuffer()); + return new EthAddress(reader.readBytes(32)); } /** diff --git a/yarn-project/foundation/src/serialize/buffer_reader.ts b/yarn-project/foundation/src/serialize/buffer_reader.ts index b54a2d1b52bf..903abcab077c 100644 --- a/yarn-project/foundation/src/serialize/buffer_reader.ts +++ b/yarn-project/foundation/src/serialize/buffer_reader.ts @@ -65,6 +65,17 @@ export class BufferReader { return this.buffer.readUInt16BE(this.index - 2); } + /** + * Reads a 8-bit unsigned integer from the buffer at the current index position. + * Updates the index position by 1 byte after reading the number. + * + * @returns The read 8 bit value. + */ + public readUInt8(): number { + this.index += 1; + return this.buffer.readUInt8(this.index - 1); + } + /** * Reads and returns the next boolean value from the buffer. * Advances the internal index by 1, treating the byte at the current index as a boolean value. diff --git a/yarn-project/types/package.json b/yarn-project/types/package.json index f8e42023289b..4bdfbc0801ed 100644 --- a/yarn-project/types/package.json +++ b/yarn-project/types/package.json @@ -7,6 +7,7 @@ "types": "./dest/index.d.ts", "exports": { "./abi": "./dest/abi/index.js", + "./contracts": "./dest/contracts/index.js", "./interfaces": "./dest/interfaces/index.js", "./membership": "./dest/sibling-path/index.js", "./noir": "./dest/noir/index.js" diff --git a/yarn-project/types/src/contracts/contract_class.test.ts b/yarn-project/types/src/contracts/contract_class.test.ts new file mode 100644 index 000000000000..8521217473c0 --- /dev/null +++ b/yarn-project/types/src/contracts/contract_class.test.ts @@ -0,0 +1,8 @@ +import { SerializableContractClass } from './contract_class.js'; + +describe('ContractClass', () => { + it('can serialize and deserialize a contract class', () => { + const contractClass = SerializableContractClass.random(); + expect(SerializableContractClass.fromBuffer(contractClass.toBuffer())).toEqual(contractClass); + }); +}); diff --git a/yarn-project/types/src/contracts/contract_class.ts b/yarn-project/types/src/contracts/contract_class.ts new file mode 100644 index 000000000000..0569a8b582f8 --- /dev/null +++ b/yarn-project/types/src/contracts/contract_class.ts @@ -0,0 +1,170 @@ +import { FunctionSelector } from '@aztec/foundation/abi'; +import { randomBytes } from '@aztec/foundation/crypto'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize'; + +const VERSION = 1 as const; + +export interface ContractClass { + /** Version of the contract class. */ + version: typeof VERSION; + /** Hash of the contract artifact. The specification of this hash is not enforced by the protocol. Should include commitments to unconstrained code and compilation metadata. Intended to be used by clients to verify that an off-chain fetched artifact matches a registered class. */ + artifactHash: Fr; + /** List of individual private functions, constructors included. */ + privateFunctions: PrivateFunction[]; + /** List of individual public functions. Should be removed once we switch to the AVM where all public bytecode is bundled together. */ + publicFunctions: PublicFunction[]; + /** Packed bytecode representation of the AVM bytecode for all public functions in this contract. Unused for now, see `publicFunctions`. */ + packedBytecode: Buffer; +} + +/** Serializable implementation of the contract class interface. */ +export class SerializableContractClass implements ContractClass { + /** Version identifier. Initially one, bumped for any changes to the contract class struct. */ + public readonly version = VERSION; + + public readonly artifactHash: Fr; + public readonly packedBytecode: Buffer; + public readonly privateFunctions: SerializablePrivateFunction[]; + public readonly publicFunctions: SerializablePublicFunction[]; + + constructor(contractClass: ContractClass) { + if (contractClass.version !== VERSION) { + throw new Error(`Unexpected contract class version ${contractClass.version}`); + } + this.privateFunctions = contractClass.privateFunctions.map(x => new SerializablePrivateFunction(x)); + this.publicFunctions = contractClass.publicFunctions.map(x => new SerializablePublicFunction(x)); + this.artifactHash = contractClass.artifactHash; + this.packedBytecode = contractClass.packedBytecode; + } + + public toBuffer() { + return serializeToBuffer( + numToUInt8(this.version), + this.artifactHash, + this.privateFunctions.length, + this.privateFunctions, + this.publicFunctions.length, + this.publicFunctions, + this.packedBytecode.length, + this.packedBytecode, + ); + } + + static fromBuffer(bufferOrReader: BufferReader | Buffer) { + const reader = BufferReader.asReader(bufferOrReader); + return new SerializableContractClass({ + version: reader.readUInt8() as typeof VERSION, + artifactHash: reader.readObject(Fr), + privateFunctions: reader.readVector(SerializablePrivateFunction), + publicFunctions: reader.readVector(SerializablePublicFunction), + packedBytecode: reader.readBuffer(), + }); + } + + static random() { + return new SerializableContractClass({ + version: VERSION, + artifactHash: Fr.random(), + privateFunctions: [SerializablePrivateFunction.random()], + publicFunctions: [SerializablePublicFunction.random()], + packedBytecode: randomBytes(32), + }); + } +} + +export interface PrivateFunction { + /** Selector of the function. Calculated as the hash of the method name and parameters. The specification of this is not enforced by the protocol. */ + selector: FunctionSelector; + /** Hash of the verification key associated to this private function. */ + vkHash: Fr; + /** + * Whether the function is internal. + * @deprecated To be reimplemented as an app-level macro. + */ + isInternal: boolean; +} + +/** Private function in a Contract Class. */ +export class SerializablePrivateFunction { + public readonly selector: FunctionSelector; + public readonly vkHash: Fr; + public readonly isInternal: boolean; + + constructor(privateFunction: PrivateFunction) { + this.selector = privateFunction.selector; + this.vkHash = privateFunction.vkHash; + this.isInternal = privateFunction.isInternal; + } + + public toBuffer() { + return serializeToBuffer(this.selector, this.vkHash, this.isInternal); + } + + static fromBuffer(bufferOrReader: BufferReader | Buffer): PrivateFunction { + const reader = BufferReader.asReader(bufferOrReader); + return new SerializablePrivateFunction({ + selector: reader.readObject(FunctionSelector), + vkHash: reader.readObject(Fr), + isInternal: reader.readBoolean(), + }); + } + + static random() { + return new SerializablePrivateFunction({ + selector: FunctionSelector.random(), + vkHash: Fr.random(), + isInternal: false, + }); + } +} + +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. */ + selector: FunctionSelector; + /** Public bytecode. */ + bytecode: Buffer; + /** + * Whether the function is internal. + * @deprecated To be reimplemented as an app-level macro. + */ + isInternal: boolean; +} + +/** + * Public function in a Contract Class. Use `packedBytecode` in the parent class once supported. + */ +export class SerializablePublicFunction { + public readonly selector: FunctionSelector; + public readonly bytecode: Buffer; + public readonly isInternal: boolean; + + constructor(publicFunction: PublicFunction) { + this.selector = publicFunction.selector; + this.bytecode = publicFunction.bytecode; + this.isInternal = publicFunction.isInternal; + } + + public toBuffer() { + return serializeToBuffer(this.selector, this.bytecode.length, this.bytecode, this.isInternal); + } + + static fromBuffer(bufferOrReader: BufferReader | Buffer): PublicFunction { + const reader = BufferReader.asReader(bufferOrReader); + return new SerializablePublicFunction({ + selector: reader.readObject(FunctionSelector), + bytecode: reader.readBuffer(), + isInternal: reader.readBoolean(), + }); + } + + static random() { + return new SerializablePublicFunction({ + selector: FunctionSelector.random(), + bytecode: randomBytes(32), + isInternal: false, + }); + } +} + +export type ContractClassWithId = ContractClass & { id: Fr }; diff --git a/yarn-project/types/src/contracts/contract_instance.test.ts b/yarn-project/types/src/contracts/contract_instance.test.ts new file mode 100644 index 000000000000..86818076f933 --- /dev/null +++ b/yarn-project/types/src/contracts/contract_instance.test.ts @@ -0,0 +1,8 @@ +import { SerializableContractInstance } from './contract_instance.js'; + +describe('ContractInstance', () => { + it('can serialize and deserialize an instance', () => { + const instance = SerializableContractInstance.random(); + expect(SerializableContractInstance.fromBuffer(instance.toBuffer())).toEqual(instance); + }); +}); diff --git a/yarn-project/types/src/contracts/contract_instance.ts b/yarn-project/types/src/contracts/contract_instance.ts new file mode 100644 index 000000000000..2dade94953bf --- /dev/null +++ b/yarn-project/types/src/contracts/contract_instance.ts @@ -0,0 +1,78 @@ +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize'; + +const VERSION = 1 as const; + +/** A contract instance is a concrete deployment of a contract class. A contract instance always references a contract class, which dictates what code it executes when called. A contract instance has state (both private and public), as well as an address that acts as its identifier. A contract instance can be called into. */ +export interface ContractInstance { + /** Version identifier. Initially one, bumped for any changes to the contract instance struct. */ + version: typeof VERSION; + /** User-generated pseudorandom value for uniqueness. */ + salt: Fr; + /** Identifier of the contract class for this instance. */ + contractClassId: Fr; + /** Hash of the selector and arguments to the constructor. */ + initializationHash: Fr; + /** Optional address of the L1 portal contract. */ + portalContractAddress: EthAddress; + /** Optional hash of the struct of public keys used for encryption and nullifying by this contract. */ + publicKeysHash: Fr; +} + +export type ContractInstanceWithAddress = ContractInstance & { address: AztecAddress }; + +export class SerializableContractInstance { + public readonly version = VERSION; + public readonly salt: Fr; + public readonly contractClassId: Fr; + public readonly initializationHash: Fr; + public readonly portalContractAddress: EthAddress; + public readonly publicKeysHash: Fr; + + constructor(instance: ContractInstance) { + if (instance.version !== VERSION) { + throw new Error(`Unexpected contract class version ${instance.version}`); + } + this.salt = instance.salt; + this.contractClassId = instance.contractClassId; + this.initializationHash = instance.initializationHash; + this.portalContractAddress = instance.portalContractAddress; + this.publicKeysHash = instance.publicKeysHash; + } + + public toBuffer() { + return serializeToBuffer( + numToUInt8(this.version), + this.salt, + this.contractClassId, + this.initializationHash, + this.portalContractAddress, + this.publicKeysHash, + ); + } + + static fromBuffer(bufferOrReader: Buffer | BufferReader) { + const reader = BufferReader.asReader(bufferOrReader); + return new SerializableContractInstance({ + version: reader.readUInt8() as typeof VERSION, + salt: reader.readObject(Fr), + contractClassId: reader.readObject(Fr), + initializationHash: reader.readObject(Fr), + portalContractAddress: reader.readObject(EthAddress), + publicKeysHash: reader.readObject(Fr), + }); + } + + static random() { + return new SerializableContractInstance({ + version: VERSION, + salt: Fr.random(), + contractClassId: Fr.random(), + initializationHash: Fr.random(), + portalContractAddress: EthAddress.random(), + publicKeysHash: Fr.random(), + }); + } +} diff --git a/yarn-project/types/src/contracts/index.ts b/yarn-project/types/src/contracts/index.ts new file mode 100644 index 000000000000..b90e91afc35d --- /dev/null +++ b/yarn-project/types/src/contracts/index.ts @@ -0,0 +1,2 @@ +export * from './contract_class.js'; +export * from './contract_instance.js'; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 5e7bbb294741..60302d733146 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -117,6 +117,7 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" "@aztec/l1-artifacts": "workspace:^" + "@aztec/types": "workspace:^" "@jest/globals": ^29.5.0 "@types/debug": ^4.1.7 "@types/jest": ^29.5.0 @@ -325,6 +326,7 @@ __metadata: dependencies: "@aztec/bb.js": "portal:../../barretenberg/ts" "@aztec/foundation": "workspace:^" + "@aztec/types": "workspace:^" "@jest/globals": ^29.5.0 "@types/jest": ^29.5.0 "@types/lodash.chunk": ^4.2.7