Skip to content
37 changes: 17 additions & 20 deletions docs/docs/protocol-specs/contract-deployment/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ artifact_crh(

let private_functions_artifact_leaves: Field[] = artifact.private_functions.map(|f|
sha256_modulo(
be_string_to_bits("az_artifact_private_function_leaf"),

VERSION, // 8-bits
f.selector, // 32-bits
f.metadata_hash, // 256-bits
sha256(f.private_bytecode)
Expand All @@ -132,8 +131,7 @@ artifact_crh(

let unconstrained_functions_artifact_leaves: Field[] = artifact.unconstrained_functions.map(|f|
sha256_modulo(
be_string_to_bits("az_artifact_unconstrained_function_leaf"),

VERSION, // 8-bits
f.selector, // 32-bits
f.metadata_hash, // 256-bits
sha256(f.unconstrained_bytecode)
Expand All @@ -142,11 +140,10 @@ artifact_crh(
let unconstrained_functions_artifact_tree_root: Field = merkleize(unconstrained_functions_artifact_leaves);

let artifact_hash: Field = sha256_modulo(
be_string_to_field("az_artifact"),

VERSION, // 8-bits
private_functions_artifact_tree_root, // 256-bits
unconstrained_functions_artifact_tree_root, // 256-bits
artifact_metadata
artifact_metadata_hash
);

let artifact_hash: Field = artifact_hash_256_bit % FIELD_MODULUS;
Expand All @@ -155,43 +152,41 @@ artifact_crh(
}
```

For the artifact hash merkleization and hashing is done using sha256, since it is computed and verified outside of circuits and does not need to be SNARK friendly, and then wrapped around the field's maximum value. Fields are left-padded with zeros to 256 bits before being hashed. Function leaves are sorted in ascending order before being merkleized, according to their function selectors. Note that a tree with dynamic height is built instead of having a tree with a fixed height, since the merkleization is done out of a circuit.
For the artifact hash merkleization and hashing is done using sha256, since it is computed and verified outside of circuits and does not need to be SNARK friendly, and then wrapped around the field's maximum value. Fields are left-padded with zeros to 256 bits before being hashed.

Function leaves are sorted in ascending order before being merkleized, according to their function selectors. Note that a tree with dynamic height is built instead of having a tree with a fixed height, since the merkleization is done out of a circuit.

<!-- TODO: Verify with the crypto team it is ok to wrap around the field modulus, or consider going Poseidon everywhere. -->

Bytecode for private functions is a mix of ACIR and Brillig, whereas unconstrained function bytecode is Brillig exclusively, as described on the [bytecode section](../bytecode/index.md).

The metadata hash for each function is suggested to be computed as the sha256 of all JSON-serialized fields in the function struct of the compilation artifact, except for bytecode and debug symbols. The metadata is JSON-serialized using no spaces, and sorting ascending all keys in objects before serializing them.
The metadata hash for each function is suggested to be computed as the sha256 (modulo) of all JSON-serialized ABI types of the function returns. Other function data is represented in the leaf hash by its bytecode and selector.

<!-- HASH DEFINITION -->

```rust
function_metadata_crh(
function // This type is out of protocol, e.g. the format output by Nargo
) -> Field {
let function_metadata = omit(function, "bytecode", "debug_symbols");

let function_metadata_hash: Field = sha256_modulo(
be_string_to_bits("az_function_metadata"),

json_serialize(function_metadata)
json_serialize(function.return_types)
);

function_metadata_hash
}
```

The artifact metadata stores all data that is not contained within the contract functions and is not debug specific. This includes the compiler version identifier, events interface, and name. Metadata is JSON-serialized in the same fashion as the function metadata.
<!-- TODO(#12081): After removal of aztecNrVersion from ContractArtifact, replace with Noir.lock hash or similar? -->

The artifact metadata stores all data that is not contained within the contract functions, is not debug specific, and is not derivable from other properties. This leaves only the `name` and `outputs` properties. Metadata is JSON-serialized in the same fashion as the function metadata.

```rust
artifact_metadata_crh(
artifact // This type is out of protocol, e.g. the format output by Nargo
) -> Field {
let artifact_metadata = omit(artifact, "functions", "file_map");
let artifact_metadata = pick(artifact, "name", "outputs");

let artifact_metadata_hash: Field = sha256_modulo(
be_string_to_bits("az_artifact_metadata"),

json_serialize(artifact_metadata)
);

Expand Down Expand Up @@ -349,19 +344,21 @@ It is strongly recommended for developers registering new classes to broadcast t

### Encoding Bytecode

The `register`, `broadcast_unconstrained_function`, and `broadcast_private_function` functions all receive and emit variable-length bytecode in unencrypted events. In every function, bytecode is encoded in a fixed-length array of field elements, which sets a maximum length for each:
The `register`, `broadcast_unconstrained_function`, and `broadcast_private_function` functions all receive and emit variable-length bytecode in contract class logs. In every function, bytecode is encoded in a fixed-length array of field elements, which sets a maximum length for each:

- `MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS`: 3000 field elements, used for a contract's public bytecode in the `register` function.
- `MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS`: 3000 field elements, used for the ACIR and Brillig bytecode of a broadcasted private function in `broadcast_private_function`.
- `MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS`: 3000 field elements, used for the Brillig bytecode of a broadcasted unconstrained function in `broadcast_unconstrained_function`.

To encode the bytecode into a fixed-length array of Fields, the bytecode is first split into 31-byte chunks, and each chunk interpreted big-endian as a field element. The total length in bytes is then prepended as an initial element, and then right-padded with zeroes.

The log itself is prepended by the address of the contract that emitted it. This is not stictly necessary because the only contract able to broadcast contract class logs is the `ContractClassRegisterer` (this is enforced in the kernel circuits), but exists to easily check and manage logs of published blocks.

```
chunks = chunk bytecode into 31 bytes elements, last element right-padded with zeroes
fields = right-align each chunk into 32 bytes and cast to a field element
padding = repeat a zero-value field MAX_SIZE - fields.count - 1 times
encoded = [bytecode.length as field, ...fields, ...padding]
encoded = [bytecode.length as field, contract_address, ...fields, ...padding]
```

## Discarded Approaches
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { css } from '@mui/styled-engine';
import { useContext, useEffect, useState } from 'react';
import {
type ContractArtifact,
type FunctionArtifact,
encodeArguments,
type FunctionAbi,
getDefaultInitializer,
getInitializer,
} from '@aztec/stdlib/abi';
Expand All @@ -40,7 +40,7 @@ export function DeployContractDialog({
onClose: (contract?: ContractInstanceWithAddress, alias?: string) => void;
}) {
const [alias, setAlias] = useState('');
const [initializer, setInitializer] = useState<FunctionArtifact>(null);
const [initializer, setInitializer] = useState<FunctionAbi>(null);
const [parameters, setParameters] = useState([]);
const [deploying, setDeploying] = useState(false);
const { wallet, setLogsOpen } = useContext(AztecContext);
Expand All @@ -63,13 +63,7 @@ export function DeployContractDialog({
setDeploying(true);
setLogsOpen(true);

const nodeInfo = await wallet.getNodeInfo();
const expectedAztecNrVersion = `v${nodeInfo.nodeVersion}`;
if (contractArtifact.aztecNrVersion && contractArtifact.aztecNrVersion !== expectedAztecNrVersion) {
throw new Error(
`Contract was compiled with a different version of Aztec.nr: ${contractArtifact.aztecNrVersion}. Consider updating Aztec.nr to ${expectedAztecNrVersion}`,
);
}
// TODO(#12081): Add contractArtifact.noirVersion and check here (via Noir.lock)?

const deployer = new ContractDeployer(contractArtifact, wallet, PublicKeys.default(), initializer?.name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,7 @@ export function RegisterContractDialog({
const register = async () => {
setRegistering(true);

const nodeInfo = await wallet.getNodeInfo();
const expectedAztecNrVersion = `v${nodeInfo.nodeVersion}`;
if (contractArtifact.aztecNrVersion && contractArtifact.aztecNrVersion !== expectedAztecNrVersion) {
throw new Error(
`Contract was compiled with a different version of Aztec.nr: ${contractArtifact.aztecNrVersion}. Consider updating Aztec.nr to ${expectedAztecNrVersion}`,
);
}
// TODO(#12081): Add contractArtifact.noirVersion and check here (via Noir.lock)?

const contractInstance = await node.getContract(AztecAddress.fromString(address));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function serializeContractClassPublic(contractClass: Omit<ContractClassPublicWit
numToUInt8(contractClass.version),
contractClass.artifactHash,
contractClass.publicFunctions.length,
// TODO(#8985): The below should only contain the public dispatch function, and no others
contractClass.publicFunctions?.map(f => serializeToBuffer(f.selector, f.bytecode.length, f.bytecode)) ?? [],
contractClass.privateFunctions.length,
contractClass.privateFunctions.map(serializePrivateFunction),
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/aztec.js/src/api/abi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export {
type ContractArtifact,
type FunctionArtifact,
type FunctionAbi,
EventSelector,
FunctionType,
FunctionSelector,
FunctionCall,
NoteSelector,
Expand All @@ -15,6 +17,8 @@ export {
isWrappedFieldStruct,
isFunctionSelectorStruct,
loadContractArtifact,
loadContractArtifactForPublic,
getAllFunctionAbis,
contractArtifactToBuffer,
contractArtifactFromBuffer,
} from '@aztec/stdlib/abi';
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec.js/src/contract/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ describe('abiChecker', () => {
},
},
],
isInitializer: true,
},
],
};
Expand Down Expand Up @@ -170,6 +171,7 @@ describe('abiChecker', () => {
},
},
],
isInitializer: true,
},
],
};
Expand Down
11 changes: 8 additions & 3 deletions yarn-project/aztec.js/src/contract/checker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { AbiType, BasicType, ContractArtifact, StructType } from '@aztec/stdlib/abi';
import {
type AbiType,
type BasicType,
type ContractArtifact,
type StructType,
getDefaultInitializer,
} from '@aztec/stdlib/abi';

/**
* Represents a type derived from input type T with the 'kind' property removed.
Expand Down Expand Up @@ -38,8 +44,7 @@ export function abiChecker(artifact: ContractArtifact) {
});
});

// TODO: implement a better check for constructor (right now only checks if it has it or not)
if (!artifact.functions.find(func => func.name === 'constructor')) {
if (!getDefaultInitializer(artifact)) {
throw new Error('ABI has no constructor');
}

Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe('Contract Class', () => {
errorTypes: {},
},
],
nonDispatchPublicFunctions: [],
outputs: {
structs: {},
globals: {},
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/aztec.js/src/contract/contract_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {
type ContractArtifact,
type ContractNote,
type FieldLayout,
type FunctionArtifact,
type FunctionAbi,
FunctionSelector,
getAllFunctionAbis,
} from '@aztec/stdlib/abi';
import { type ContractInstanceWithAddress, computePartialAddress } from '@aztec/stdlib/contract';

Expand Down Expand Up @@ -52,7 +53,7 @@ export class ContractBase {
/** The wallet used for interacting with this contract. */
public wallet: Wallet,
) {
artifact.functions.forEach((f: FunctionArtifact) => {
getAllFunctionAbis(artifact).forEach((f: FunctionAbi) => {
const interactionFunction = (...args: any[]) => {
return new ContractFunctionInteraction(this.wallet, this.instance.address, f, args);
};
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/src/contract/deploy_method.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Fr } from '@aztec/foundation/fields';
import { type ContractArtifact, type FunctionArtifact, getInitializer } from '@aztec/stdlib/abi';
import { type ContractArtifact, type FunctionAbi, type FunctionArtifact, getInitializer } from '@aztec/stdlib/abi';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type ContractInstanceWithAddress,
Expand Down Expand Up @@ -51,7 +51,7 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Bas
private instance?: ContractInstanceWithAddress = undefined;

/** Constructor function to call. */
private constructorArtifact: FunctionArtifact | undefined;
private constructorArtifact: FunctionAbi | undefined;

constructor(
private publicKeys: PublicKeys,
Expand Down
21 changes: 15 additions & 6 deletions yarn-project/builder/src/contract-interface-gen/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
type ABIVariable,
type ContractArtifact,
EventSelector,
type FunctionArtifact,
type FunctionAbi,
decodeFunctionSignature,
getAllFunctionAbis,
getDefaultInitializer,
isAztecAddressStruct,
isEthAddressStruct,
Expand Down Expand Up @@ -62,7 +63,7 @@ function generateParameter(param: ABIParameter) {
* @param param - A Noir function.
* @returns The corresponding ts code.
*/
function generateMethod(entry: FunctionArtifact) {
function generateMethod(entry: FunctionAbi) {
const args = entry.parameters.map(generateParameter).join(', ');
return `
/** ${entry.name}(${entry.parameters.map(p => `${p.name}: ${p.type.kind}`).join(', ')}) */
Expand Down Expand Up @@ -154,10 +155,10 @@ function generateAt(name: string) {
}

/**
* Generates a static getter for the contract's artifact.
* Generates static getters for the contract's artifact.
* @param name - Name of the contract used to derive name of the artifact import.
*/
function generateArtifactGetter(name: string) {
function generateArtifactGetters(name: string) {
const artifactName = `${name}ContractArtifact`;
return `
/**
Expand All @@ -166,6 +167,13 @@ function generateArtifactGetter(name: string) {
public static get artifact(): ContractArtifact {
return ${artifactName};
}

/**
* Returns this contract's artifact with public bytecode.
*/
public static get artifactForPublic(): ContractArtifact {
return loadContractArtifactForPublic(${artifactName}Json as NoirCompiledContract);
}
`;
}

Expand Down Expand Up @@ -298,15 +306,15 @@ async function generateEvents(events: any[] | undefined) {
* @returns The corresponding ts code.
*/
export async function generateTypescriptContractInterface(input: ContractArtifact, artifactImportPath?: string) {
const methods = input.functions
const methods = getAllFunctionAbis(input)
.filter(f => !f.isInternal)
.sort((a, b) => a.name.localeCompare(b.name))
.map(generateMethod);
const deploy = artifactImportPath && generateDeploy(input);
const ctor = artifactImportPath && generateConstructor(input.name);
const at = artifactImportPath && generateAt(input.name);
const artifactStatement = artifactImportPath && generateAbiStatement(input.name, artifactImportPath);
const artifactGetter = artifactImportPath && generateArtifactGetter(input.name);
const artifactGetter = artifactImportPath && generateArtifactGetters(input.name);
const storageLayoutGetter = artifactImportPath && generateStorageLayoutGetter(input);
const notesGetter = artifactImportPath && generateNotesGetter(input);
const { eventDefs, events } = await generateEvents(input.outputs.structs?.events);
Expand Down Expand Up @@ -338,6 +346,7 @@ import {
type FunctionSelectorLike,
L1EventPayload,
loadContractArtifact,
loadContractArtifactForPublic,
type NoirCompiledContract,
NoteSelector,
Point,
Expand Down
8 changes: 1 addition & 7 deletions yarn-project/cli-wallet/src/cmds/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,7 @@ export async function deploy(
const contractArtifact = await getContractArtifact(artifactPath, log);
const constructorArtifact = getInitializer(contractArtifact, initializer);

const nodeInfo = await client.getNodeInfo();
const expectedAztecNrVersion = `$v${nodeInfo.nodeVersion}`;
if (contractArtifact.aztecNrVersion && contractArtifact.aztecNrVersion !== expectedAztecNrVersion) {
log(
`\nWarning: Contract was compiled with a different version of Aztec.nr: ${contractArtifact.aztecNrVersion}. Consider updating Aztec.nr to ${expectedAztecNrVersion}\n`,
);
}
// TODO(#12081): Add contractArtifact.noirVersion and check here (via Noir.lock)?

const deployer = new ContractDeployer(contractArtifact, wallet, publicKeys ?? PublicKeys.default(), initializer);

Expand Down
22 changes: 16 additions & 6 deletions yarn-project/cli/src/cmds/contracts/inspect_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import {
FunctionSelector,
decodeFunctionSignature,
decodeFunctionSignatureWithParameterNames,
retainBytecode,
} from '@aztec/stdlib/abi';
import { getContractClassFromArtifact } from '@aztec/stdlib/contract';

import { getContractArtifact } from '../../utils/aztec.js';

export async function inspectContract(contractArtifactFile: string, debugLogger: Logger, log: LogFn) {
const contractArtifact = await getContractArtifact(contractArtifactFile, log);
const contractFns = contractArtifact.functions;
const contractFns = contractArtifact.functions.concat(
contractArtifact.nonDispatchPublicFunctions.map(f => f as FunctionArtifact),
);
if (contractFns.length === 0) {
log(`No functions found for contract ${contractArtifact.name}`);
}
Expand Down Expand Up @@ -43,9 +46,16 @@ async function logFunction(fn: FunctionArtifact, log: LogFn) {
const signatureWithParameterNames = decodeFunctionSignatureWithParameterNames(fn.name, fn.parameters);
const signature = decodeFunctionSignature(fn.name, fn.parameters);
const selector = await FunctionSelector.fromSignature(signature);
const bytecodeSize = fn.bytecode.length;
const bytecodeHash = sha256(fn.bytecode).toString('hex');
log(
`${fn.functionType} ${signatureWithParameterNames} \n\tfunction signature: ${signature}\n\tselector: ${selector}\n\tbytecode: ${bytecodeSize} bytes (sha256 ${bytecodeHash})`,
);

if (retainBytecode(fn)) {
const bytecodeSize = fn.bytecode.length;
const bytecodeHash = sha256(fn.bytecode).toString('hex');
log(
`${fn.functionType} ${signatureWithParameterNames} \n\tfunction signature: ${signature}\n\tselector: ${selector}\n\tbytecode: ${bytecodeSize} bytes (sha256 ${bytecodeHash})`,
);
} else {
log(
`${fn.functionType} ${signatureWithParameterNames} \n\tfunction signature: ${signature}\n\tselector: ${selector}`,
);
}
}
Loading