diff --git a/docs/docs/protocol-specs/contract-deployment/classes.md b/docs/docs/protocol-specs/contract-deployment/classes.md index 9f573be52cd1..e9f97ba884fe 100644 --- a/docs/docs/protocol-specs/contract-deployment/classes.md +++ b/docs/docs/protocol-specs/contract-deployment/classes.md @@ -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) @@ -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) @@ -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; @@ -155,13 +152,15 @@ 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. 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. @@ -169,29 +168,25 @@ The metadata hash for each function is suggested to be computed as the sha256 of 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. + + +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) ); @@ -349,7 +344,7 @@ 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`. @@ -357,11 +352,13 @@ The `register`, `broadcast_unconstrained_function`, and `broadcast_private_funct 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 diff --git a/playground/src/components/contract/components/deployContractDialog.tsx b/playground/src/components/contract/components/deployContractDialog.tsx index 0feb9329238a..457d8cf51329 100644 --- a/playground/src/components/contract/components/deployContractDialog.tsx +++ b/playground/src/components/contract/components/deployContractDialog.tsx @@ -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'; @@ -40,7 +40,7 @@ export function DeployContractDialog({ onClose: (contract?: ContractInstanceWithAddress, alias?: string) => void; }) { const [alias, setAlias] = useState(''); - const [initializer, setInitializer] = useState(null); + const [initializer, setInitializer] = useState(null); const [parameters, setParameters] = useState([]); const [deploying, setDeploying] = useState(false); const { wallet, setLogsOpen } = useContext(AztecContext); @@ -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); diff --git a/playground/src/components/contract/components/registerContractDialog.tsx b/playground/src/components/contract/components/registerContractDialog.tsx index 81301a895ec3..f17bdc26b7c0 100644 --- a/playground/src/components/contract/components/registerContractDialog.tsx +++ b/playground/src/components/contract/components/registerContractDialog.tsx @@ -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)); 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 index 2fb3770e104c..2bf458fd10e2 100644 --- 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 @@ -97,6 +97,7 @@ function serializeContractClassPublic(contractClass: Omit serializeToBuffer(f.selector, f.bytecode.length, f.bytecode)) ?? [], contractClass.privateFunctions.length, contractClass.privateFunctions.map(serializePrivateFunction), diff --git a/yarn-project/aztec.js/src/api/abi.ts b/yarn-project/aztec.js/src/api/abi.ts index 202228406304..c1aef893650b 100644 --- a/yarn-project/aztec.js/src/api/abi.ts +++ b/yarn-project/aztec.js/src/api/abi.ts @@ -1,7 +1,9 @@ export { type ContractArtifact, type FunctionArtifact, + type FunctionAbi, EventSelector, + FunctionType, FunctionSelector, FunctionCall, NoteSelector, @@ -15,6 +17,8 @@ export { isWrappedFieldStruct, isFunctionSelectorStruct, loadContractArtifact, + loadContractArtifactForPublic, + getAllFunctionAbis, contractArtifactToBuffer, contractArtifactFromBuffer, } from '@aztec/stdlib/abi'; diff --git a/yarn-project/aztec.js/src/contract/checker.test.ts b/yarn-project/aztec.js/src/contract/checker.test.ts index d2c945623f42..91fe34950d6b 100644 --- a/yarn-project/aztec.js/src/contract/checker.test.ts +++ b/yarn-project/aztec.js/src/contract/checker.test.ts @@ -139,6 +139,7 @@ describe('abiChecker', () => { }, }, ], + isInitializer: true, }, ], }; @@ -170,6 +171,7 @@ describe('abiChecker', () => { }, }, ], + isInitializer: true, }, ], }; diff --git a/yarn-project/aztec.js/src/contract/checker.ts b/yarn-project/aztec.js/src/contract/checker.ts index 8ee4fee2a0d6..63112d9d3ee8 100644 --- a/yarn-project/aztec.js/src/contract/checker.ts +++ b/yarn-project/aztec.js/src/contract/checker.ts @@ -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. @@ -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'); } diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 48d50f2b07f0..4bce90b740bf 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -121,6 +121,7 @@ describe('Contract Class', () => { errorTypes: {}, }, ], + nonDispatchPublicFunctions: [], outputs: { structs: {}, globals: {}, diff --git a/yarn-project/aztec.js/src/contract/contract_base.ts b/yarn-project/aztec.js/src/contract/contract_base.ts index 84fd3ba018f2..9183e21bf70a 100644 --- a/yarn-project/aztec.js/src/contract/contract_base.ts +++ b/yarn-project/aztec.js/src/contract/contract_base.ts @@ -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'; @@ -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); }; diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 12c5ad68cd66..95522bc7994c 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -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, @@ -51,7 +51,7 @@ export class DeployMethod extends Bas private instance?: ContractInstanceWithAddress = undefined; /** Constructor function to call. */ - private constructorArtifact: FunctionArtifact | undefined; + private constructorArtifact: FunctionAbi | undefined; constructor( private publicKeys: PublicKeys, diff --git a/yarn-project/builder/src/contract-interface-gen/typescript.ts b/yarn-project/builder/src/contract-interface-gen/typescript.ts index 80f2148f8757..0887a89c1574 100644 --- a/yarn-project/builder/src/contract-interface-gen/typescript.ts +++ b/yarn-project/builder/src/contract-interface-gen/typescript.ts @@ -3,8 +3,9 @@ import { type ABIVariable, type ContractArtifact, EventSelector, - type FunctionArtifact, + type FunctionAbi, decodeFunctionSignature, + getAllFunctionAbis, getDefaultInitializer, isAztecAddressStruct, isEthAddressStruct, @@ -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(', ')}) */ @@ -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 ` /** @@ -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); + } `; } @@ -298,7 +306,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); @@ -306,7 +314,7 @@ export async function generateTypescriptContractInterface(input: ContractArtifac 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); @@ -338,6 +346,7 @@ import { type FunctionSelectorLike, L1EventPayload, loadContractArtifact, + loadContractArtifactForPublic, type NoirCompiledContract, NoteSelector, Point, diff --git a/yarn-project/cli-wallet/src/cmds/deploy.ts b/yarn-project/cli-wallet/src/cmds/deploy.ts index a4082f388e28..930e146eefa9 100644 --- a/yarn-project/cli-wallet/src/cmds/deploy.ts +++ b/yarn-project/cli-wallet/src/cmds/deploy.ts @@ -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); diff --git a/yarn-project/cli/src/cmds/contracts/inspect_contract.ts b/yarn-project/cli/src/cmds/contracts/inspect_contract.ts index d6fd6bcda3a9..c221561b48cb 100644 --- a/yarn-project/cli/src/cmds/contracts/inspect_contract.ts +++ b/yarn-project/cli/src/cmds/contracts/inspect_contract.ts @@ -5,6 +5,7 @@ import { FunctionSelector, decodeFunctionSignature, decodeFunctionSignatureWithParameterNames, + retainBytecode, } from '@aztec/stdlib/abi'; import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; @@ -12,7 +13,9 @@ 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}`); } @@ -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}`, + ); + } } diff --git a/yarn-project/cli/src/utils/aztec.ts b/yarn-project/cli/src/utils/aztec.ts index e74abdc7d3cb..728cc3e8a13f 100644 --- a/yarn-project/cli/src/utils/aztec.ts +++ b/yarn-project/cli/src/utils/aztec.ts @@ -1,11 +1,16 @@ import type { EthAddress, PXE } from '@aztec/aztec.js'; -import { type ContractArtifact, type FunctionArtifact, loadContractArtifact } from '@aztec/aztec.js/abi'; +import { + type ContractArtifact, + type FunctionAbi, + FunctionType, + getAllFunctionAbis, + loadContractArtifact, +} from '@aztec/aztec.js/abi'; import type { DeployL1ContractsReturnType, L1ContractsConfig, RollupContract } from '@aztec/ethereum'; import type { Fr } from '@aztec/foundation/fields'; import type { LogFn, Logger } from '@aztec/foundation/log'; import type { NoirPackageConfig } from '@aztec/foundation/noir'; import { ProtocolContractAddress, protocolContractTreeRoot } from '@aztec/protocol-contracts'; -import { FunctionType } from '@aztec/stdlib/abi'; import TOML from '@iarna/toml'; import { readFile } from 'fs/promises'; @@ -19,8 +24,8 @@ import { encodeArgs } from './encoding.js'; * @param fnName - Function name to be found. * @returns The function's ABI. */ -export function getFunctionArtifact(artifact: ContractArtifact, fnName: string): FunctionArtifact { - const fn = artifact.functions.find(({ name }) => name === fnName); +export function getFunctionAbi(artifact: ContractArtifact, fnName: string): FunctionAbi { + const fn = getAllFunctionAbis(artifact).find(({ name }) => name === fnName); if (!fn) { throw Error(`Function ${fnName} not found in contract ABI.`); } @@ -178,7 +183,7 @@ export async function getContractArtifact(fileDir: string, log: LogFn) { */ export async function prepTx(contractFile: string, functionName: string, _functionArgs: string[], log: LogFn) { const contractArtifact = await getContractArtifact(contractFile, log); - const functionArtifact = getFunctionArtifact(contractArtifact, functionName); + const functionArtifact = getFunctionAbi(contractArtifact, functionName); const functionArgs = encodeArgs(_functionArgs, functionArtifact.parameters); const isPrivate = functionArtifact.functionType === FunctionType.PRIVATE; diff --git a/yarn-project/simulator/src/public/avm/fixtures/index.ts b/yarn-project/simulator/src/public/avm/fixtures/index.ts index d957267f59e6..b0a5051bc776 100644 --- a/yarn-project/simulator/src/public/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/public/avm/fixtures/index.ts @@ -5,9 +5,15 @@ import { } from '@aztec/constants'; 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 { AvmGadgetsTestContract } from '@aztec/noir-contracts.js/AvmGadgetsTest'; +import { AvmTestContract } from '@aztec/noir-contracts.js/AvmTest'; +import { + type ContractArtifact, + type FunctionAbi, + type FunctionArtifact, + FunctionSelector, + getAllFunctionAbis, +} from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractClassPublic, @@ -147,7 +153,7 @@ export function getFunctionSelector( functionName: string, contractArtifact: ContractArtifact, ): Promise { - 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); @@ -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; } @@ -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; } @@ -183,18 +190,18 @@ export function resolveContractAssertionMessage( } export function getAvmTestContractFunctionSelector(functionName: string): Promise { - return getFunctionSelector(functionName, AvmTestContractArtifact); + return getFunctionSelector(functionName, AvmTestContract.artifactForPublic); } export function getAvmGadgetsTestContractFunctionSelector(functionName: string): Promise { - const artifact = AvmGadgetsTestContractArtifact.functions.find(f => f.name === functionName)!; + const artifact = getAllFunctionAbis(AvmGadgetsTestContract.artifactForPublic).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, AvmTestContract.artifactForPublic) as FunctionArtifact; assert( !!artifact?.bytecode, `No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`, @@ -203,7 +210,7 @@ export function getAvmTestContractArtifact(functionName: string): FunctionArtifa } export function getAvmGadgetsTestContractArtifact(functionName: string): FunctionArtifact { - const artifact = AvmGadgetsTestContractArtifact.functions.find(f => f.name === functionName)!; + const artifact = AvmGadgetsTestContract.artifactForPublic.functions.find(f => f.name === functionName)!; assert( !!artifact?.bytecode, `No bytecode found for function ${functionName}. Try re-running bootstrap.sh on the repository root.`, @@ -226,7 +233,7 @@ export function resolveAvmTestContractAssertionMessage( revertReason: AvmRevertReason, output: Fr[], ): string | undefined { - return resolveContractAssertionMessage(functionName, revertReason, output, AvmTestContractArtifact); + return resolveContractAssertionMessage(functionName, revertReason, output, AvmTestContract.artifactForPublic); } export function resolveAvmGadgetsTestContractAssertionMessage( @@ -238,7 +245,7 @@ export function resolveAvmGadgetsTestContractAssertionMessage( revertReason = cause as AvmRevertReason; }); - const functionArtifact = AvmGadgetsTestContractArtifact.functions.find(f => f.name === functionName); + const functionArtifact = AvmGadgetsTestContract.artifactForPublic.functions.find(f => f.name === functionName); if (!functionArtifact || !revertReason.noirCallStack || !isNoirCallStackUnresolved(revertReason.noirCallStack)) { return undefined; } @@ -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) }, diff --git a/yarn-project/stdlib/src/abi/abi.ts b/yarn-project/stdlib/src/abi/abi.ts index ecec5fd16e5e..15f78acf37e0 100644 --- a/yarn-project/stdlib/src/abi/abi.ts +++ b/yarn-project/stdlib/src/abi/abi.ts @@ -334,12 +334,12 @@ export interface ContractArtifact { /** The name of the contract. */ name: string; - /** The version of compiler used to create this artifact */ - aztecNrVersion?: string; - - /** The functions of the contract. */ + /** The functions of the contract. Includes private and unconstrained functions, plus the public dispatch function. */ functions: FunctionArtifact[]; + /** The public functions of the contract, excluding dispatch. */ + nonDispatchPublicFunctions: FunctionAbi[]; + /** The outputs of the contract. */ outputs: { structs: Record; @@ -358,8 +358,8 @@ export interface ContractArtifact { export const ContractArtifactSchema: ZodFor = z.object({ name: z.string(), - aztecNrVersion: z.string().optional(), functions: z.array(FunctionArtifactSchema), + nonDispatchPublicFunctions: z.array(FunctionAbiSchema), outputs: z.object({ structs: z.record(z.array(AbiTypeSchema)).transform(structs => { for (const [key, value] of Object.entries(structs)) { @@ -419,6 +419,11 @@ export async function getFunctionArtifact( return { ...functionArtifact, debug: debugMetadata }; } +/** Gets all function abis */ +export function getAllFunctionAbis(artifact: ContractArtifact): FunctionAbi[] { + return artifact.functions.map(f => f as FunctionAbi).concat(artifact.nonDispatchPublicFunctions || []); +} + export function parseDebugSymbols(debugSymbols: string): DebugInfo[] { return JSON.parse(inflate(Buffer.from(debugSymbols, 'base64'), { to: 'string', raw: true })).debug_infos; } @@ -467,8 +472,9 @@ export function getFunctionDebugMetadata( * @param contractArtifact - The contract artifact. * @returns An initializer function, or none if there are no functions flagged as initializers in the contract. */ -export function getDefaultInitializer(contractArtifact: ContractArtifact): FunctionArtifact | undefined { - const initializers = contractArtifact.functions.filter(f => f.isInitializer); +export function getDefaultInitializer(contractArtifact: ContractArtifact): FunctionAbi | undefined { + const functionAbis = getAllFunctionAbis(contractArtifact); + const initializers = functionAbis.filter(f => f.isInitializer); return initializers.length > 1 ? initializers.find(f => f.name === 'constructor') ?? initializers.find(f => f.name === 'initializer') ?? @@ -486,9 +492,10 @@ export function getDefaultInitializer(contractArtifact: ContractArtifact): Funct export function getInitializer( contract: ContractArtifact, initializerNameOrArtifact: string | undefined | FunctionArtifact, -): FunctionArtifact | undefined { +): FunctionAbi | undefined { if (typeof initializerNameOrArtifact === 'string') { - const found = contract.functions.find(f => f.name === initializerNameOrArtifact); + const functionAbis = getAllFunctionAbis(contract); + const found = functionAbis.find(f => f.name === initializerNameOrArtifact); if (!found) { throw new Error(`Constructor method ${initializerNameOrArtifact} not found in contract artifact`); } else if (!found.isInitializer) { diff --git a/yarn-project/stdlib/src/abi/contract_artifact.ts b/yarn-project/stdlib/src/abi/contract_artifact.ts index b5ad86c03906..037942a380b6 100644 --- a/yarn-project/stdlib/src/abi/contract_artifact.ts +++ b/yarn-project/stdlib/src/abi/contract_artifact.ts @@ -9,6 +9,7 @@ import { ContractArtifactSchema, type ContractNote, type FieldLayout, + type FunctionAbi, type FunctionArtifact, FunctionType, type IntegerValue, @@ -44,6 +45,7 @@ export function contractArtifactFromBuffer(buffer: Buffer): Promise & { bytecode: string } { +function generateFunctionAbi(fn: NoirCompiledContractFunction, contract: NoirCompiledContract): FunctionAbi { if (fn.custom_attributes === undefined) { throw new Error( `No custom attributes found for contract function ${fn.name}. Try rebuilding the contract with the latest nargo version.`, @@ -156,10 +179,26 @@ function generateFunctionArtifact( isInitializer: fn.custom_attributes.includes(AZTEC_INITIALIZER_ATTRIBUTE), parameters, returnTypes, - bytecode: fn.bytecode, - debugSymbols: fn.debug_symbols, errorTypes: fn.abi.error_types, ...(fn.assert_messages ? { assertMessages: fn.assert_messages } : undefined), + }; +} + +/** + * Generates a function build artifact. + * @param fn - Noir function entry. + * @param contract - Parent contract. + * @returns Function artifact. + */ +function generateFunctionArtifact( + fn: NoirCompiledContractFunction, + contract: NoirCompiledContract, +): Omit & { bytecode: string } { + const abi = generateFunctionAbi(fn, contract); + return { + ...abi, + bytecode: fn.bytecode, + debugSymbols: fn.debug_symbols, ...(fn.verification_key ? { verificationKey: fn.verification_key } : undefined), }; } @@ -274,19 +313,46 @@ function getNoteTypes(input: NoirCompiledContract) { /** * Given a Nargo output generates an Aztec-compatible contract artifact. + * Does not include public bytecode, apart from the public_dispatch function. + * @param compiled - Noir build output. + * @returns Aztec contract build artifact. + */ +function generateContractArtifact(contract: NoirCompiledContract): ContractArtifact { + try { + return ContractArtifactSchema.parse({ + name: contract.name, + functions: contract.functions.filter(f => retainBytecode(f)).map(f => generateFunctionArtifact(f, contract)), + nonDispatchPublicFunctions: contract.functions + .filter(f => !retainBytecode(f)) + .map(f => generateFunctionAbi(f, contract)), + outputs: contract.outputs, + storageLayout: getStorageLayout(contract), + notes: getNoteTypes(contract), + fileMap: contract.file_map, + }); + } catch (err) { + throw new Error(`Could not generate contract artifact for ${contract.name}: ${err}`); + } +} + +/** + * Given a Nargo output generates an Aztec-compatible contract artifact. + * Retains all public bytecode. * @param compiled - Noir build output. * @returns Aztec contract build artifact. */ -function generateContractArtifact(contract: NoirCompiledContract, aztecNrVersion?: string): ContractArtifact { +function generateContractArtifactForPublic(contract: NoirCompiledContract): ContractArtifact { try { return ContractArtifactSchema.parse({ name: contract.name, functions: contract.functions.map(f => generateFunctionArtifact(f, contract)), + nonDispatchPublicFunctions: contract.functions + .filter(f => !retainBytecode(f)) + .map(f => generateFunctionAbi(f, contract)), outputs: contract.outputs, storageLayout: getStorageLayout(contract), notes: getNoteTypes(contract), fileMap: contract.file_map, - ...(aztecNrVersion ? { aztecNrVersion } : {}), }); } catch (err) { throw new Error(`Could not generate contract artifact for ${contract.name}: ${err}`); diff --git a/yarn-project/stdlib/src/contract/artifact_hash.test.ts b/yarn-project/stdlib/src/contract/artifact_hash.test.ts index d90ed2cc527f..f7642e8d0257 100644 --- a/yarn-project/stdlib/src/contract/artifact_hash.test.ts +++ b/yarn-project/stdlib/src/contract/artifact_hash.test.ts @@ -7,6 +7,7 @@ describe('ArtifactHash', () => { const emptyArtifact: ContractArtifact = { fileMap: [], functions: [], + nonDispatchPublicFunctions: [], name: 'Test', outputs: { globals: {}, diff --git a/yarn-project/stdlib/src/contract/contract_class.test.ts b/yarn-project/stdlib/src/contract/contract_class.test.ts index 153e47ae0d46..9bd9f7fb41bb 100644 --- a/yarn-project/stdlib/src/contract/contract_class.test.ts +++ b/yarn-project/stdlib/src/contract/contract_class.test.ts @@ -16,6 +16,7 @@ describe('ContractClass', () => { // Assert bytecode has a reasonable length expect(contractClass.packedBytecode.length).toBeGreaterThan(100); contractClass.publicFunctions.forEach(publicFunction => { + // TODO(#8985): The below should only contain the public dispatch function, and no others expect(publicFunction.bytecode.length).toBeGreaterThan(100); }); diff --git a/yarn-project/stdlib/src/contract/contract_class.ts b/yarn-project/stdlib/src/contract/contract_class.ts index 6316c6ff8753..c12ea9a196cd 100644 --- a/yarn-project/stdlib/src/contract/contract_class.ts +++ b/yarn-project/stdlib/src/contract/contract_class.ts @@ -20,6 +20,8 @@ export async function getContractClassFromArtifact( ): Promise { const artifactHash = 'artifactHash' in artifact ? artifact.artifactHash : await computeArtifactHash(artifact); const publicFunctions = artifact.functions.filter(f => f.functionType === FunctionType.PUBLIC); + // TODO(#8985): ContractArtifact.functions should ensure that the below only contains the public dispatch function + // So we can likely remove this and just use the below to assign the dispatch. const artifactPublicFunctions: ContractClass['publicFunctions'] = await Promise.all( publicFunctions.map(async f => ({ selector: await FunctionSelector.fromNameAndParameters(f.name, f.parameters), diff --git a/yarn-project/stdlib/src/contract/contract_instance.ts b/yarn-project/stdlib/src/contract/contract_instance.ts index 2a53253c9112..bde6c2a46c82 100644 --- a/yarn-project/stdlib/src/contract/contract_instance.ts +++ b/yarn-project/stdlib/src/contract/contract_instance.ts @@ -2,7 +2,14 @@ import { Fr } from '@aztec/foundation/fields'; import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize'; import type { FieldsOf } from '@aztec/foundation/types'; -import { type ContractArtifact, type FunctionArtifact, FunctionSelector, getDefaultInitializer } from '../abi/index.js'; +import { + type ContractArtifact, + type FunctionAbi, + type FunctionArtifact, + FunctionSelector, + getAllFunctionAbis, + getDefaultInitializer, +} from '../abi/index.js'; import { AztecAddress } from '../aztec-address/index.js'; import { getContractClassFromArtifact } from '../contract/contract_class.js'; import { PublicKeys } from '../keys/public_keys.js'; @@ -101,7 +108,7 @@ export class SerializableContractInstance { export async function getContractInstanceFromDeployParams( artifact: ContractArtifact, opts: { - constructorArtifact?: FunctionArtifact | string; + constructorArtifact?: FunctionAbi | string; constructorArgs?: any[]; skipArgsDecoding?: boolean; salt?: Fr; @@ -138,14 +145,15 @@ export async function getContractInstanceFromDeployParams( function getConstructorArtifact( artifact: ContractArtifact, - requestedConstructorArtifact: FunctionArtifact | string | undefined, -): FunctionArtifact | undefined { + requestedConstructorArtifact: FunctionArtifact | FunctionAbi | string | undefined, +): FunctionAbi | undefined { if (typeof requestedConstructorArtifact === 'string') { - const found = artifact.functions.find(fn => fn.name === requestedConstructorArtifact); + const found = getAllFunctionAbis(artifact).find(fn => fn.name === requestedConstructorArtifact); if (!found) { throw new Error(`No constructor found with name ${requestedConstructorArtifact}`); } return found; } + // TODO: shouldn't we check that requestedConstructorArtifact exists on artifact before returning? return requestedConstructorArtifact ?? getDefaultInitializer(artifact); } diff --git a/yarn-project/stdlib/src/tests/mocks.ts b/yarn-project/stdlib/src/tests/mocks.ts index 53dd8e17c4ee..c6d1f31ffe9f 100644 --- a/yarn-project/stdlib/src/tests/mocks.ts +++ b/yarn-project/stdlib/src/tests/mocks.ts @@ -228,6 +228,7 @@ export const mockSimulatedTx = async (seed = 1) => { export const randomContractArtifact = (): ContractArtifact => ({ name: randomBytes(4).toString('hex'), functions: [], + nonDispatchPublicFunctions: [], outputs: { structs: {}, globals: {},