Skip to content

Commit

Permalink
feat: Emit single functions from class registerer (AztecProtocol#4429)
Browse files Browse the repository at this point in the history
Adds functions to broadcast individual private and unconstrained
functions from the class registerer contract. This is not required for a
contract to be callable, but it's rather a convenience for easily
broadcasting private and unconstrained functions to all users.

The node does not yet capture these events.

Fixes AztecProtocol#4427
  • Loading branch information
spalladino authored Feb 6, 2024
1 parent 8b427cc commit a252f5d
Show file tree
Hide file tree
Showing 26 changed files with 478 additions and 179 deletions.
9 changes: 8 additions & 1 deletion l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ library Constants {
uint256 internal constant NULLIFIER_TREE_HEIGHT = 20;
uint256 internal constant L1_TO_L2_MSG_TREE_HEIGHT = 16;
uint256 internal constant ROLLUP_VK_TREE_HEIGHT = 8;
uint256 internal constant ARTIFACT_FUNCTION_TREE_MAX_HEIGHT = 5;
uint256 internal constant CONTRACT_SUBTREE_HEIGHT = 0;
uint256 internal constant CONTRACT_SUBTREE_SIBLING_PATH_LENGTH = 16;
uint256 internal constant NOTE_HASH_SUBTREE_HEIGHT = 6;
Expand All @@ -67,8 +68,14 @@ library Constants {
uint256 internal constant ARGS_HASH_CHUNK_LENGTH = 32;
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 =
uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 500;
uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 500;
uint256 internal constant REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE =
0x6999d1e02b08a447a463563453cb36919c9dd7150336fc7c4d2b52f8;
uint256 internal constant REGISTERER_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE =
0x1b70e95fde0b70adc30496b90a327af6a5e383e028e7a43211a07bcd;
uint256 internal constant REGISTERER_UNCONSTRAINED_FUNCTION_BROADCASTED_MAGIC_VALUE =
0xe7af816635466f128568edb04c9fa024f6c87fb9010fdbffa68b3d99;
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
11 changes: 7 additions & 4 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import {
UnencryptedL2Log,
} from '@aztec/circuit-types';
import {
CONTRACT_CLASS_REGISTERED_MAGIC_VALUE,
ContractClassRegisteredEvent,
FunctionSelector,
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE,
} from '@aztec/circuits.js';
import { createEthereumChain } from '@aztec/ethereum';
import { AztecAddress } from '@aztec/foundation/aztec-address';
Expand Down Expand Up @@ -70,10 +70,13 @@ export class Archiver implements ArchiveSource {
*/
private lastLoggedL1BlockNumber = 0n;

// TODO(@spalladino): Calculate this on the fly somewhere else!
// TODO(@spalladino): Calculate this on the fly somewhere else.
// Today this is printed in the logs for end-to-end test at
// end-to-end/src/e2e_deploy_contract.test.ts -t 'registering a new contract class'
// as "Added contract ContractClassRegisterer ADDRESS"
/** Address of the ClassRegisterer contract with a salt=1 */
private classRegistererAddress = AztecAddress.fromString(
'0x1c9f737a5ab5a7bb5ea970ba40737d44dc22fbcbe19fd8171429f2c2c433afb5',
'0x29c0cd0000951bba8af520ad5513cc53d9f0413c5a24a72a4ba8c17894c0bef9',
);

/**
Expand Down Expand Up @@ -335,7 +338,7 @@ export class Archiver implements ArchiveSource {
try {
if (
!log.contractAddress.equals(this.classRegistererAddress) ||
toBigIntBE(log.data.subarray(0, 32)) !== CONTRACT_CLASS_REGISTERED_MAGIC_VALUE
toBigIntBE(log.data.subarray(0, 32)) !== REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE
) {
continue;
}
Expand Down
10 changes: 9 additions & 1 deletion yarn-project/circuits.js/src/constants.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const PUBLIC_DATA_TREE_HEIGHT = 40;
export const NULLIFIER_TREE_HEIGHT = 20;
export const L1_TO_L2_MSG_TREE_HEIGHT = 16;
export const ROLLUP_VK_TREE_HEIGHT = 8;
export const ARTIFACT_FUNCTION_TREE_MAX_HEIGHT = 5;
export const CONTRACT_SUBTREE_HEIGHT = 0;
export const CONTRACT_SUBTREE_SIBLING_PATH_LENGTH = 16;
export const NOTE_HASH_SUBTREE_HEIGHT = 6;
Expand All @@ -53,7 +54,14 @@ export const NUM_FIELDS_PER_SHA256 = 2;
export const ARGS_HASH_CHUNK_LENGTH = 32;
export const ARGS_HASH_CHUNK_COUNT = 32;
export const MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 1000;
export const CONTRACT_CLASS_REGISTERED_MAGIC_VALUE = 0x6999d1e02b08a447a463563453cb36919c9dd7150336fc7c4d2b52f8n;
export const MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 500;
export const MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 500;
export const REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE =
0x6999d1e02b08a447a463563453cb36919c9dd7150336fc7c4d2b52f8n;
export const REGISTERER_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE =
0x1b70e95fde0b70adc30496b90a327af6a5e383e028e7a43211a07bcdn;
export const REGISTERER_UNCONSTRAINED_FUNCTION_BROADCASTED_MAGIC_VALUE =
0xe7af816635466f128568edb04c9fa024f6c87fb9010fdbffa68b3d99n;
export const L1_TO_L2_MESSAGE_LENGTH = 8;
export const L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25;
export const MAX_NOTE_FIELDS_LENGTH = 20;
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/circuits.js/src/contract/artifact_hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getArtifactHash } from './artifact_hash.js';
import { computeArtifactHash } from './artifact_hash.js';

describe('ArtifactHash', () => {
it('calculates the artifact hash', () => {
const artifact = getSampleContractArtifact();
expect(getArtifactHash(artifact).toString()).toMatchInlineSnapshot(
`"0x1cd31b12181cf7516720f4675ffea13c8c538dc4875232776adb8bbe8364ed5c"`,
expect(computeArtifactHash(artifact).toString()).toMatchInlineSnapshot(
`"0x242a46b1aa0ed341fe71f1068a1289cdbb01fbef14e2250783333cc0607db940"`,
);
});
});
36 changes: 23 additions & 13 deletions yarn-project/circuits.js/src/contract/artifact_hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sha256 } from '@aztec/foundation/crypto';
import { Fr } from '@aztec/foundation/fields';
import { numToUInt8 } from '@aztec/foundation/serialize';

import { MerkleTree } from '../merkle/merkle_tree.js';
import { MerkleTreeCalculator } from '../merkle/merkle_tree_calculator.js';

const VERSION = 1;
Expand All @@ -29,39 +30,48 @@ const VERSION = 1;
* ```
* @param artifact - Artifact to calculate the hash for.
*/
export function getArtifactHash(artifact: ContractArtifact): Fr {
const privateFunctionRoot = getFunctionRoot(artifact, FunctionType.SECRET);
const unconstrainedFunctionRoot = getFunctionRoot(artifact, FunctionType.OPEN);
const metadataHash = getArtifactMetadataHash(artifact);
export function computeArtifactHash(artifact: ContractArtifact): Fr {
const privateFunctionRoot = computeArtifactFunctionTreeRoot(artifact, FunctionType.SECRET);
const unconstrainedFunctionRoot = computeArtifactFunctionTreeRoot(artifact, FunctionType.UNCONSTRAINED);
const metadataHash = computeArtifactMetadataHash(artifact);
const preimage = [numToUInt8(VERSION), privateFunctionRoot, unconstrainedFunctionRoot, metadataHash];
// TODO(@spalladino) Reducing sha256 to a field may have security implications. Validate this with crypto team.
return Fr.fromBufferReduce(sha256(Buffer.concat(preimage)));
}

function getArtifactMetadataHash(artifact: ContractArtifact) {
export function computeArtifactMetadataHash(artifact: ContractArtifact) {
const metadata = { name: artifact.name, events: artifact.events }; // TODO(@spalladino): Should we use the sorted event selectors instead? They'd need to be unique for that.
return sha256(Buffer.from(JSON.stringify(metadata), 'utf-8'));
}

type FunctionArtifactWithSelector = FunctionArtifact & { selector: FunctionSelector };
export function computeArtifactFunctionTreeRoot(artifact: ContractArtifact, fnType: FunctionType) {
return computeArtifactFunctionTree(artifact, fnType)?.root ?? Fr.ZERO.toBuffer();
}

function getFunctionRoot(artifact: ContractArtifact, fnType: FunctionType) {
const leaves = getFunctionLeaves(artifact, fnType);
export function computeArtifactFunctionTree(artifact: ContractArtifact, fnType: FunctionType): MerkleTree | undefined {
const leaves = computeFunctionLeaves(artifact, fnType);
// TODO(@spalladino) Consider implementing a null-object for empty trees
if (leaves.length === 0) {
return undefined;
}
const height = Math.ceil(Math.log2(leaves.length));
const calculator = new MerkleTreeCalculator(height, Buffer.alloc(32), (l, r) => sha256(Buffer.concat([l, r])));
return calculator.computeTreeRoot(leaves);
return calculator.computeTree(leaves);
}

function getFunctionLeaves(artifact: ContractArtifact, fnType: FunctionType) {
function computeFunctionLeaves(artifact: ContractArtifact, fnType: FunctionType) {
return artifact.functions
.filter(f => f.functionType === fnType)
.map(f => ({ ...f, selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters) }))
.sort((a, b) => a.selector.value - b.selector.value)
.map(getFunctionArtifactHash);
.map(computeFunctionArtifactHash);
}

function getFunctionArtifactHash(fn: FunctionArtifactWithSelector): Buffer {
export function computeFunctionArtifactHash(fn: FunctionArtifact & { selector?: FunctionSelector }): Buffer {
const selector =
(fn as { selector: FunctionSelector }).selector ?? FunctionSelector.fromNameAndParameters(fn.name, fn.parameters);
const bytecodeHash = sha256(Buffer.from(fn.bytecode, 'hex'));
const metadata = JSON.stringify(fn.returnTypes);
const metadataHash = sha256(Buffer.from(metadata, 'utf8'));
return sha256(Buffer.concat([numToUInt8(VERSION), fn.selector.toBuffer(), metadataHash, bytecodeHash]));
return sha256(Buffer.concat([numToUInt8(VERSION), selector.toBuffer(), metadataHash, bytecodeHash]));
}
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/contract/contract_class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContractArtifact, FunctionSelector, FunctionType } from '@aztec/foundat
import { Fr } from '@aztec/foundation/fields';
import { ContractClass, ContractClassWithId } from '@aztec/types/contracts';

import { getArtifactHash } from './artifact_hash.js';
import { computeArtifactHash } from './artifact_hash.js';
import { computeContractClassId } from './contract_class_id.js';
import { packBytecode } from './public_bytecode.js';

Expand All @@ -13,7 +13,7 @@ type ContractArtifactWithHash = ContractArtifact & { artifactHash: Fr };
export function getContractClassFromArtifact(
artifact: ContractArtifact | ContractArtifactWithHash,
): ContractClassWithId {
const artifactHash = (artifact as ContractArtifactWithHash).artifactHash ?? getArtifactHash(artifact);
const artifactHash = (artifact as ContractArtifactWithHash).artifactHash ?? computeArtifactHash(artifact);
const publicFunctions: ContractClass['publicFunctions'] = artifact.functions
.filter(f => f.functionType === FunctionType.OPEN)
.map(f => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { bufferFromFields } from '@aztec/foundation/abi';
import { toBigIntBE } from '@aztec/foundation/bigint-buffer';
import { Fr } from '@aztec/foundation/fields';
import { BufferReader } from '@aztec/foundation/serialize';
import { ContractClassPublic } from '@aztec/types/contracts';

import chunk from 'lodash.chunk';

import { CONTRACT_CLASS_REGISTERED_MAGIC_VALUE } from '../constants.gen.js';
import { REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE } from '../constants.gen.js';
import { computeContractClassId, computePublicBytecodeCommitment } from './contract_class_id.js';
import { packedBytecodeFromFields, unpackBytecode } from './public_bytecode.js';
import { unpackBytecode } from './public_bytecode.js';

/** Event emitted from the ContractClassRegisterer. */
export class ContractClassRegisteredEvent {
Expand All @@ -20,20 +21,20 @@ export class ContractClassRegisteredEvent {
) {}

static isContractClassRegisteredEvent(log: Buffer) {
return toBigIntBE(log.subarray(0, 32)) == CONTRACT_CLASS_REGISTERED_MAGIC_VALUE;
return toBigIntBE(log.subarray(0, 32)) == REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE;
}

static fromLogData(log: Buffer) {
if (!this.isContractClassRegisteredEvent(log)) {
const magicValue = CONTRACT_CLASS_REGISTERED_MAGIC_VALUE.toString(16);
const magicValue = REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE.toString(16);
throw new Error(`Log data for ContractClassRegisteredEvent is not prefixed with magic value 0x${magicValue}`);
}
const reader = new BufferReader(log.subarray(32));
const contractClassId = reader.readObject(Fr);
const version = reader.readObject(Fr).toNumber();
const artifactHash = reader.readObject(Fr);
const privateFunctionsRoot = reader.readObject(Fr);
const packedPublicBytecode = packedBytecodeFromFields(
const packedPublicBytecode = bufferFromFields(
chunk(reader.readToEnd(), Fr.SIZE_IN_BYTES).map(Buffer.from).map(Fr.fromBuffer),
);

Expand Down
14 changes: 1 addition & 13 deletions yarn-project/circuits.js/src/contract/public_bytecode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContractArtifact } from '@aztec/foundation/abi';

import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getContractClassFromArtifact } from './contract_class.js';
import { packBytecode, packedBytecodeAsFields, packedBytecodeFromFields, unpackBytecode } from './public_bytecode.js';
import { packBytecode, unpackBytecode } from './public_bytecode.js';

describe('PublicBytecode', () => {
let artifact: ContractArtifact;
Expand All @@ -16,16 +16,4 @@ describe('PublicBytecode', () => {
const unpackedBytecode = unpackBytecode(packedBytecode);
expect(unpackedBytecode).toEqual(publicFunctions);
});

it('converts small packed bytecode back and forth from fields', () => {
const packedBytecode = Buffer.from('1234567890abcdef'.repeat(10), 'hex');
const fields = packedBytecodeAsFields(packedBytecode);
expect(packedBytecodeFromFields(fields).toString('hex')).toEqual(packedBytecode.toString('hex'));
});

it('converts real packed bytecode back and forth from fields', () => {
const { packedBytecode } = getContractClassFromArtifact(artifact);
const fields = packedBytecodeAsFields(packedBytecode);
expect(packedBytecodeFromFields(fields).toString('hex')).toEqual(packedBytecode.toString('hex'));
});
});
41 changes: 1 addition & 40 deletions yarn-project/circuits.js/src/contract/public_bytecode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FunctionSelector } from '@aztec/foundation/abi';
import { Fr } from '@aztec/foundation/fields';
import {
BufferReader,
numToInt32BE,
Expand All @@ -8,9 +7,7 @@ import {
} from '@aztec/foundation/serialize';
import { ContractClass } from '@aztec/types/contracts';

import chunk from 'lodash.chunk';

import { FUNCTION_SELECTOR_NUM_BYTES, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS } from '../constants.gen.js';
import { FUNCTION_SELECTOR_NUM_BYTES } from '../constants.gen.js';

/**
* Packs together a set of public functions for a contract class.
Expand All @@ -36,39 +33,3 @@ export function unpackBytecode(buffer: Buffer): ContractClass['publicFunctions']
}),
});
}

/**
* Formats packed bytecode as an array of fields. Splits the input into 31-byte chunks, and stores each
* of them into a field, omitting the field's first byte, then adds zero-fields at the end until the max length.
* @param packedBytecode - Packed bytecode for a contract.
* @returns A field with the total length in bytes, followed by an array of fields such that their concatenation is equal to the input buffer.
* @remarks This function is more generic than just for packed bytecode, perhaps it could be moved elsewhere.
*/
export function packedBytecodeAsFields(packedBytecode: Buffer): Fr[] {
const encoded = [
new Fr(packedBytecode.length),
...chunk(packedBytecode, Fr.SIZE_IN_BYTES - 1).map(c => {
const fieldBytes = Buffer.alloc(32);
Buffer.from(c).copy(fieldBytes, 1);
return Fr.fromBuffer(fieldBytes);
}),
];
if (encoded.length > MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS) {
throw new Error(
`Packed bytecode exceeds maximum size: got ${encoded.length} but max is ${MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS}`,
);
}
// Fun fact: we cannot use padArrayEnd here since typescript cannot deal with a Tuple this big
return [...encoded, ...Array(MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - encoded.length).fill(Fr.ZERO)];
}

/**
* Recovers packed bytecode from an array of fields.
* @param fields - An output from packedBytecodeAsFields.
* @returns The packed bytecode.
* @remarks This function is more generic than just for packed bytecode, perhaps it could be moved elsewhere.
*/
export function packedBytecodeFromFields(fields: Fr[]): Buffer {
const [length, ...payload] = fields;
return Buffer.concat(payload.map(f => f.toBuffer().subarray(1))).subarray(0, length.toNumber());
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class MerkleTreeCalculator {

computeTree(leaves: Buffer[] = []): MerkleTree {
if (leaves.length === 0) {
// TODO(#4425): We should be returning a number of nodes that matches the tree height.
return new MerkleTree(this.height, [this.zeroHashes[this.zeroHashes.length - 1]]);
}

Expand Down
Loading

0 comments on commit a252f5d

Please sign in to comment.