Skip to content

Commit

Permalink
feat: Contract class registerer contract (AztecProtocol#4403)
Browse files Browse the repository at this point in the history
Adds contract class registerer canonical contract, and listens to its
events from the archiver. This contract is meant for broadcasting the
public bytecode of any given contract class. If the public bytecode for
a contract class has not been broadcasted, the public kernel should
refuse to execute any calls to a contract instance of this class. More
info
[here](https://yp-aztec.netlify.app/docs/contract-deployment/classes#canonical-contract-class-registerer).

For now the contract assigned to a hardcoded address in the archiver,
computed with salt=1. And since event selectors are not yet implemented
(all events are emitted with selector `5`) uses a magic number for
identifying the event, which should be removed later.

Other changes include:

- Add serialization for an arbitrary-sized buffer into a Field array,
where each field carries 31 bytes of info of the original buffer.
- Increase the maximum number of args that can be passed (from 512 to
1024) to a Noir private function.
- Introduce a new `ContractClassPublic` interface that represents a
contract class that has info on public functions only, and uses it in
the Aztec Node.
- Remove the `SerializableContractClass` class in favor of serialization
functions used only in the archiver.
- Add an endpoint to the Aztec Node for retrieving contract classes.

Pending for future PRs:

- Unhardcode the registerer contract address from the archiver.
- Set up a nicer API for class registration.
- Generalize buffer serialization into field array.
- Add methods for broadcasting private and unconstrained functions.

Fixes AztecProtocol#4069
Fixes AztecProtocol#4070
  • Loading branch information
spalladino authored Feb 5, 2024
1 parent 5deb397 commit aedb2f8
Show file tree
Hide file tree
Showing 44 changed files with 795 additions and 320 deletions.
5 changes: 4 additions & 1 deletion l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ library Constants {
uint256 internal constant MAPPING_SLOT_PEDERSEN_SEPARATOR = 4;
uint256 internal constant NUM_FIELDS_PER_SHA256 = 2;
uint256 internal constant ARGS_HASH_CHUNK_LENGTH = 32;
uint256 internal constant ARGS_HASH_CHUNK_COUNT = 16;
uint256 internal constant ARGS_HASH_CHUNK_COUNT = 32;
uint256 internal constant MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 1000;
uint256 internal constant CONTRACT_CLASS_REGISTERED_MAGIC_VALUE =
0x6999d1e02b08a447a463563453cb36919c9dd7150336fc7c4d2b52f8;
uint256 internal constant L1_TO_L2_MESSAGE_LENGTH = 8;
uint256 internal constant L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25;
uint256 internal constant MAX_NOTE_FIELDS_LENGTH = 20;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ export class ClientExecutionContext extends ViewDataOracle {
*/
public emitUnencryptedLog(log: UnencryptedL2Log) {
this.unencryptedLogs.push(log);
this.log(`Emitted unencrypted log: "${log.toHumanReadable()}"`);
const text = log.toHumanReadable();
this.log(`Emitted unencrypted log: "${text.length > 100 ? text.slice(0, 100) + '...' : text}"`);
}

/**
Expand Down
62 changes: 58 additions & 4 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ import {
LogFilter,
LogType,
TxHash,
UnencryptedL2Log,
} from '@aztec/circuit-types';
import { FunctionSelector, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/circuits.js';
import {
CONTRACT_CLASS_REGISTERED_MAGIC_VALUE,
ContractClassRegisteredEvent,
FunctionSelector,
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
} from '@aztec/circuits.js';
import { createEthereumChain } from '@aztec/ethereum';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { toBigIntBE } from '@aztec/foundation/bigint-buffer';
import { padArrayEnd } from '@aztec/foundation/collection';
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,
ContractClassPublic,
ContractInstance,
ContractInstanceWithAddress,
} from '@aztec/types/contracts';
Expand Down Expand Up @@ -63,6 +70,12 @@ export class Archiver implements ArchiveSource {
*/
private lastLoggedL1BlockNumber = 0n;

// TODO(@spalladino): Calculate this on the fly somewhere else!
/** Address of the ClassRegisterer contract with a salt=1 */
private classRegistererAddress = AztecAddress.fromString(
'0x1c9f737a5ab5a7bb5ea970ba40737d44dc22fbcbe19fd8171429f2c2c433afb5',
);

/**
* Creates a new instance of the Archiver.
* @param publicClient - A client for interacting with the Ethereum node.
Expand Down Expand Up @@ -272,6 +285,16 @@ export class Archiver implements ArchiveSource {
),
);

// Unroll all logs emitted during the retrieved blocks and extract any contract classes from them
await Promise.all(
retrievedBlocks.retrievedData.map(async block => {
const blockLogs = (block.newUnencryptedLogs?.txLogs ?? [])
.flatMap(txLog => txLog.unrollLogs())
.map(log => UnencryptedL2Log.fromBuffer(log));
await this.storeRegisteredContractClasses(blockLogs, block.number);
}),
);

// store contracts for which we have retrieved L2 blocks
const lastKnownL2BlockNum = retrievedBlocks.retrievedData[retrievedBlocks.retrievedData.length - 1].number;
await Promise.all(
Expand Down Expand Up @@ -302,6 +325,33 @@ export class Archiver implements ArchiveSource {
);
}

/**
* Extracts and stores contract classes out of ContractClassRegistered events emitted by the class registerer contract.
* @param allLogs - All logs emitted in a bunch of blocks.
*/
private async storeRegisteredContractClasses(allLogs: UnencryptedL2Log[], blockNum: number) {
const contractClasses: ContractClassPublic[] = [];
for (const log of allLogs) {
try {
if (
!log.contractAddress.equals(this.classRegistererAddress) ||
toBigIntBE(log.data.subarray(0, 32)) !== CONTRACT_CLASS_REGISTERED_MAGIC_VALUE
) {
continue;
}
const event = ContractClassRegisteredEvent.fromLogData(log.data);
contractClasses.push(event.toContractClassPublic());
} catch (err) {
this.log.warn(`Error processing log ${log.toHumanReadable()}: ${err}`);
}
}

if (contractClasses.length > 0) {
contractClasses.forEach(c => this.log(`Registering contract class ${c.id.toString()}`));
await this.store.addContractClasses(contractClasses, blockNum);
}
}

/**
* Stores extended contract data as classes and instances.
* Temporary solution until we source this data from the contract class registerer and instance deployer.
Expand Down Expand Up @@ -448,6 +498,10 @@ export class Archiver implements ArchiveSource {
return this.store.getBlockNumber();
}

public getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
return this.store.getContractClass(id);
}

/**
* Gets up to `limit` amount of pending L1 to L2 messages.
* @param limit - The number of messages to return.
Expand Down Expand Up @@ -475,7 +529,7 @@ export class Archiver implements ArchiveSource {
*/
function extendedContractDataToContractClassAndInstance(
data: ExtendedContractData,
): [ContractClassWithId, ContractInstanceWithAddress] {
): [ContractClassPublic, ContractInstanceWithAddress] {
const contractClass: ContractClass = {
version: 1,
artifactHash: Fr.ZERO,
Expand All @@ -498,7 +552,7 @@ function extendedContractDataToContractClassAndInstance(
};
const address = data.contractData.contractAddress;
return [
{ ...contractClass, id: contractClassId },
{ ...contractClass, id: contractClassId, privateFunctionsRoot: Fr.ZERO },
{ ...contractInstance, address },
];
}
6 changes: 3 additions & 3 deletions yarn-project/archiver/src/archiver/archiver_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +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';
import { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts';

/**
* Represents the latest L1 block processed by the archiver for various objects in L2.
Expand Down Expand Up @@ -175,13 +175,13 @@ export interface ArchiverDataStore {
* @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<boolean>;
addContractClasses(data: ContractClassPublic[], blockNumber: number): Promise<boolean>;

/**
* Returns a contract class given its id, or undefined if not exists.
* @param id - Id of the contract class.
*/
getContractClass(id: Fr): Promise<ContractClassWithId | undefined>;
getContractClass(id: Fr): Promise<ContractClassPublic | undefined>;

/**
* Add new contract instances from an L2 block to the store's list.
Expand Down
12 changes: 4 additions & 8 deletions yarn-project/archiver/src/archiver/archiver_store_test_suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import {
} from '@aztec/circuit-types';
import '@aztec/circuit-types/jest';
import { AztecAddress, Fr } from '@aztec/circuits.js';
import { makeContractClassPublic } from '@aztec/circuits.js/factories';
import { randomBytes } from '@aztec/foundation/crypto';
import {
ContractClassWithId,
ContractInstanceWithAddress,
SerializableContractClass,
SerializableContractInstance,
} from '@aztec/types/contracts';
import { ContractClassPublic, ContractInstanceWithAddress, SerializableContractInstance } from '@aztec/types/contracts';

import { ArchiverDataStore } from './archiver_store.js';

Expand Down Expand Up @@ -345,11 +341,11 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
});

describe('contractClasses', () => {
let contractClass: ContractClassWithId;
let contractClass: ContractClassPublic;
const blockNum = 10;

beforeEach(async () => {
contractClass = { ...SerializableContractClass.random(), id: Fr.random() };
contractClass = makeContractClassPublic();
await store.addContractClasses([contractClass], blockNum);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Fr } from '@aztec/foundation/fields';
import { Fr, FunctionSelector } from '@aztec/circuits.js';
import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize';
import { AztecKVStore, AztecMap } from '@aztec/kv-store';
import { ContractClassWithId, SerializableContractClass } from '@aztec/types/contracts';
import { ContractClassPublic } from '@aztec/types/contracts';

/**
* LMDB implementation of the ArchiverDataStore interface.
Expand All @@ -12,15 +13,52 @@ export class ContractClassStore {
this.#contractClasses = db.openMap('archiver_contract_classes');
}

addContractClass(contractClass: ContractClassWithId): Promise<boolean> {
return this.#contractClasses.set(
contractClass.id.toString(),
new SerializableContractClass(contractClass).toBuffer(),
);
addContractClass(contractClass: ContractClassPublic): Promise<boolean> {
return this.#contractClasses.set(contractClass.id.toString(), serializeContractClassPublic(contractClass));
}

getContractClass(id: Fr): ContractClassWithId | undefined {
getContractClass(id: Fr): ContractClassPublic | undefined {
const contractClass = this.#contractClasses.get(id.toString());
return contractClass && SerializableContractClass.fromBuffer(contractClass).withId(id);
return contractClass && { ...deserializeContractClassPublic(contractClass), id };
}
}

export function serializeContractClassPublic(contractClass: ContractClassPublic): Buffer {
return serializeToBuffer(
numToUInt8(contractClass.version),
contractClass.artifactHash,
contractClass.privateFunctions?.length ?? 0,
contractClass.privateFunctions?.map(f => serializeToBuffer(f.selector, f.vkHash, f.isInternal)) ?? [],
contractClass.publicFunctions.length,
contractClass.publicFunctions?.map(f =>
serializeToBuffer(f.selector, f.bytecode.length, f.bytecode, f.isInternal),
) ?? [],
contractClass.packedBytecode.length,
contractClass.packedBytecode,
contractClass.privateFunctionsRoot,
);
}

export function deserializeContractClassPublic(buffer: Buffer): Omit<ContractClassPublic, 'id'> {
const reader = BufferReader.asReader(buffer);
return {
version: reader.readUInt8() as 1,
artifactHash: reader.readObject(Fr),
privateFunctions: reader.readVector({
fromBuffer: reader => ({
selector: reader.readObject(FunctionSelector),
vkHash: reader.readObject(Fr),
isInternal: reader.readBoolean(),
}),
}),
publicFunctions: reader.readVector({
fromBuffer: reader => ({
selector: reader.readObject(FunctionSelector),
bytecode: reader.readBuffer(),
isInternal: reader.readBoolean(),
}),
}),
packedBytecode: reader.readBuffer(),
privateFunctionsRoot: reader.readObject(Fr),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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 { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts';

import { ArchiverDataStore, ArchiverL1SynchPoint } from '../archiver_store.js';
import { BlockStore } from './block_store.js';
Expand Down Expand Up @@ -46,15 +46,15 @@ export class KVArchiverDataStore implements ArchiverDataStore {
this.#contractInstanceStore = new ContractInstanceStore(db);
}

getContractClass(id: Fr): Promise<ContractClassWithId | undefined> {
getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
return Promise.resolve(this.#contractClassStore.getContractClass(id));
}

getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
return Promise.resolve(this.#contractInstanceStore.getContractInstance(address));
}

async addContractClasses(data: ContractClassWithId[], _blockNumber: number): Promise<boolean> {
async addContractClasses(data: ContractClassPublic[], _blockNumber: number): Promise<boolean> {
return (await Promise.all(data.map(c => this.#contractClassStore.addContractClass(c)))).every(Boolean);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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 { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts';

import { ArchiverDataStore } from '../archiver_store.js';
import { L1ToL2MessageStore, PendingL1ToL2MessageStore } from './l1_to_l2_message_store.js';
Expand Down Expand Up @@ -69,7 +69,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {
*/
private pendingL1ToL2Messages: PendingL1ToL2MessageStore = new PendingL1ToL2MessageStore();

private contractClasses: Map<string, ContractClassWithId> = new Map();
private contractClasses: Map<string, ContractClassPublic> = new Map();

private contractInstances: Map<string, ContractInstanceWithAddress> = new Map();

Expand All @@ -81,15 +81,15 @@ export class MemoryArchiverStore implements ArchiverDataStore {
public readonly maxLogs: number,
) {}

public getContractClass(id: Fr): Promise<ContractClassWithId | undefined> {
public getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
return Promise.resolve(this.contractClasses.get(id.toString()));
}

public getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
return Promise.resolve(this.contractInstances.get(address.toString()));
}

public addContractClasses(data: ContractClassWithId[], _blockNumber: number): Promise<boolean> {
public addContractClasses(data: ContractClassPublic[], _blockNumber: number): Promise<boolean> {
for (const contractClass of data) {
this.contractClasses.set(contractClass.id.toString(), contractClass);
}
Expand Down
5 changes: 5 additions & 0 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
SequencerClient,
getGlobalVariableBuilder,
} from '@aztec/sequencer-client';
import { ContractClassPublic } from '@aztec/types/contracts';
import {
MerkleTrees,
ServerWorldStateSynchronizer,
Expand Down Expand Up @@ -237,6 +238,10 @@ export class AztecNodeService implements AztecNode {
return await this.contractDataSource.getContractData(contractAddress);
}

public getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
return this.contractDataSource.getContractClass(id);
}

/**
* Gets up to `limit` amount of logs starting from `from`.
* @param from - Number of the L2 block to which corresponds the first logs to be returned.
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/circuit-types/src/contract_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
serializeBufferArrayToVector,
serializeToBuffer,
} from '@aztec/foundation/serialize';
import { ContractClassPublic } from '@aztec/types/contracts';

/**
* Used for retrieval of contract data (A3 address, portal contract address, bytecode).
Expand Down Expand Up @@ -55,6 +56,12 @@ export interface ContractDataSource {
* @returns The number of the latest L2 block processed by the implementation.
*/
getBlockNumber(): Promise<number>;

/**
* Returns the contract class for a given contract class id, or undefined if not found.
* @param id - Contract class id.
*/
getContractClass(id: Fr): Promise<ContractClassPublic | undefined>;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions yarn-project/circuit-types/src/interfaces/aztec-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Header } from '@aztec/circuits.js';
import { L1ContractAddresses } from '@aztec/ethereum';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr } from '@aztec/foundation/fields';
import { ContractClassPublic } from '@aztec/types/contracts';

import { ContractData, ExtendedContractData } from '../contract_data.js';
import { L2Block } from '../l2_block.js';
Expand Down Expand Up @@ -136,4 +137,10 @@ export interface AztecNode extends StateInfoProvider {
* @param config - Updated configuration to be merged with the current one.
*/
setConfig(config: Partial<SequencerConfig>): Promise<void>;

/**
* Returns a registered contract class given its id.
* @param id - Id of the contract class.
*/
getContractClass(id: Fr): Promise<ContractClassPublic | undefined>;
}
Loading

0 comments on commit aedb2f8

Please sign in to comment.