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: Remove/replace optional aztecNrVersion from ContractArtifact and explain here -->

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 @@ -97,6 +97,7 @@ function serializeContractClassPublic(contractClass: Omit<ContractClassPublicWit
numToUInt8(contractClass.version),
contractClass.artifactHash,
contractClass.publicFunctions.length,
// bytecode ref?
contractClass.publicFunctions?.map(f => serializeToBuffer(f.selector, f.bytecode.length, f.bytecode)) ?? [],
contractClass.privateFunctions.length,
contractClass.privateFunctions.map(serializePrivateFunction),
Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/api/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
EventSelector,
FunctionSelector,
loadContractArtifact,
loadPublicContractArtifact,
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
10 changes: 8 additions & 2 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 @@ -39,7 +45,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: {},
},
],
publicFunctions: [],
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
10 changes: 8 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,11 @@
import type { Fr } from '@aztec/foundation/fields';
import { type ContractArtifact, type FunctionArtifact, type FunctionCall, getInitializer } from '@aztec/stdlib/abi';
import {
type ContractArtifact,
type FunctionAbi,
type FunctionArtifact,
type FunctionCall,
getInitializer,
} from '@aztec/stdlib/abi';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type ContractInstanceWithAddress,
Expand Down Expand Up @@ -51,7 +57,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
12 changes: 8 additions & 4 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 @@ -176,9 +177,11 @@ function generateArtifactGetter(name: string) {
* @returns Code.
*/
function generateAbiStatement(name: string, artifactImportPath: string) {
// TODO(MW): fix hack - AVM needs some public fns bytecode for ts testing
const loadPublic = name.includes('Avm') ? `Public` : ``;
const stmts = [
`import ${name}ContractArtifactJson from '${artifactImportPath}' assert { type: 'json' };`,
`export const ${name}ContractArtifact = loadContractArtifact(${name}ContractArtifactJson as NoirCompiledContract);`,
`export const ${name}ContractArtifact = load${loadPublic}ContractArtifact(${name}ContractArtifactJson as NoirCompiledContract);`,
];
return stmts.join('\n');
}
Comment thread
MirandaWood marked this conversation as resolved.
Expand Down Expand Up @@ -298,7 +301,7 @@ 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);
Expand Down Expand Up @@ -338,6 +341,7 @@ import {
type FunctionSelectorLike,
L1EventPayload,
loadContractArtifact,
loadPublicContractArtifact,
type NoirCompiledContract,
NoteSelector,
Point,
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/cli/src/cmds/contracts/inspect_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FunctionSelector,
decodeFunctionSignature,
decodeFunctionSignatureWithParameterNames,
getAllFunctionAbis,
} from '@aztec/stdlib/abi';
import { getContractClassFromArtifact } from '@aztec/stdlib/contract';

Expand All @@ -13,7 +14,7 @@ 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;
if (contractFns.length === 0) {
if (getAllFunctionAbis(contractArtifact).length === 0) {
log(`No functions found for contract ${contractArtifact.name}`);
}
const contractClass = await getContractClassFromArtifact(contractArtifact);
Expand All @@ -26,6 +27,7 @@ export async function inspectContract(contractArtifactFile: string, debugLogger:
log(`\tpublic bytecode commitment: ${contractClass.publicBytecodeCommitment.toString()}`);
log(`\tpublic bytecode length: ${contractClass.packedBytecode.length} bytes (${bytecodeLengthInFields} fields)`);

// TODO(MW): include public fns and convert below to FunctionAbi
const externalFunctions = contractFns.filter(f => !f.isInternal);
if (externalFunctions.length > 0) {
log(`\nExternal functions:`);
Expand Down
24 changes: 16 additions & 8 deletions yarn-project/simulator/src/public/avm/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { AvmGadgetsTestContractArtifact } from '@aztec/noir-contracts.js/AvmGadgetsTest';
import { AvmTestContractArtifact } from '@aztec/noir-contracts.js/AvmTest';
import { type ContractArtifact, type FunctionArtifact, FunctionSelector } from '@aztec/stdlib/abi';
import {
type ContractArtifact,
type FunctionAbi,
type FunctionArtifact,
FunctionSelector,
getAllFunctionAbis,
} from '@aztec/stdlib/abi';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type ContractClassPublic,
Expand Down Expand Up @@ -147,7 +153,7 @@ export function getFunctionSelector(
functionName: string,
contractArtifact: ContractArtifact,
): Promise<FunctionSelector> {
const fnArtifact = contractArtifact.functions.find(f => f.name === functionName)!;
const fnArtifact = getAllFunctionAbis(contractArtifact).find(f => f.name === functionName)!;
assert(!!fnArtifact, `Function ${functionName} not found in ${contractArtifact.name}`);
const params = fnArtifact.parameters;
return FunctionSelector.fromNameAndParameters(fnArtifact.name, params);
Expand All @@ -156,10 +162,11 @@ export function getFunctionSelector(
export function getContractFunctionArtifact(
functionName: string,
contractArtifact: ContractArtifact,
): FunctionArtifact | undefined {
): FunctionArtifact | FunctionAbi | undefined {
const artifact = contractArtifact.functions.find(f => f.name === functionName)!;
if (!artifact) {
return undefined;
const abi = getAllFunctionAbis(contractArtifact).find(f => f.name === functionName);
return abi || undefined;
}
return artifact;
}
Expand All @@ -174,7 +181,7 @@ export function resolveContractAssertionMessage(
revertReason = cause as AvmRevertReason;
});

const functionArtifact = contractArtifact.functions.find(f => f.name === functionName);
const functionArtifact = getAllFunctionAbis(contractArtifact).find(f => f.name === functionName);
if (!functionArtifact || !revertReason.noirCallStack || !isNoirCallStackUnresolved(revertReason.noirCallStack)) {
return undefined;
}
Expand All @@ -187,14 +194,14 @@ export function getAvmTestContractFunctionSelector(functionName: string): Promis
}

export function getAvmGadgetsTestContractFunctionSelector(functionName: string): Promise<FunctionSelector> {
const artifact = AvmGadgetsTestContractArtifact.functions.find(f => f.name === functionName)!;
const artifact = getAllFunctionAbis(AvmGadgetsTestContractArtifact).find(f => f.name === functionName)!;
assert(!!artifact, `Function ${functionName} not found in AvmGadgetsTestContractArtifact`);
const params = artifact.parameters;
return FunctionSelector.fromNameAndParameters(artifact.name, params);
}

export function getAvmTestContractArtifact(functionName: string): FunctionArtifact {
const artifact = getContractFunctionArtifact(functionName, AvmTestContractArtifact);
const artifact = getContractFunctionArtifact(functionName, AvmTestContractArtifact) as FunctionArtifact;
assert(
!!artifact?.bytecode,
`No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`,
Expand Down Expand Up @@ -267,7 +274,8 @@ export async function createContractClassAndInstance(
contractInstance: ContractInstanceWithAddress;
contractAddressNullifier: Fr;
}> {
const bytecode = getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact)!.bytecode;
const bytecode = (getContractFunctionArtifact(PUBLIC_DISPATCH_FN_NAME, contractArtifact) as FunctionArtifact)!
.bytecode;
const contractClass = await makeContractClassPublic(
seed,
/*publicDispatchFunction=*/ { bytecode, selector: new FunctionSelector(PUBLIC_DISPATCH_SELECTOR) },
Expand Down
Loading