Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Aztec.js API for registering a contract class #4469

Merged
merged 2 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boxes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@aztec/bb.js": "portal:../barretenberg/ts",
"@aztec/circuit-types": "portal:../yarn-project/circuit-types",
"@aztec/ethereum": "portal:../yarn-project/ethereum",
"@aztec/protocol-contracts": "portal:../yarn-project/protocol-contracts",
"@aztec/types": "portal:../yarn-project/types"
}
}
21 changes: 21 additions & 0 deletions boxes/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ __metadata:
"@aztec/circuits.js": "workspace:^"
"@aztec/ethereum": "workspace:^"
"@aztec/foundation": "workspace:^"
"@aztec/protocol-contracts": "workspace:^"
"@aztec/types": "workspace:^"
tslib: "npm:^2.4.0"
languageName: node
Expand Down Expand Up @@ -289,6 +290,7 @@ __metadata:
koa-router: "npm:^12.0.0"
leveldown: "npm:^6.1.1"
levelup: "npm:^5.1.1"
lodash.chunk: "npm:^4.2.0"
lodash.clonedeepwith: "npm:^4.5.0"
memdown: "npm:^6.1.1"
pako: "npm:^2.1.0"
Expand All @@ -297,6 +299,18 @@ __metadata:
languageName: node
linkType: soft

"@aztec/protocol-contracts@portal:../yarn-project/protocol-contracts::locator=%40aztec%2Fboxes%40workspace%3A.":
version: 0.0.0-use.local
resolution: "@aztec/protocol-contracts@portal:../yarn-project/protocol-contracts::locator=%40aztec%2Fboxes%40workspace%3A."
dependencies:
"@aztec/circuits.js": "workspace:^"
"@aztec/foundation": "workspace:^"
"@aztec/types": "workspace:^"
lodash.omit: "npm:^4.5.0"
tslib: "npm:^2.4.0"
languageName: node
linkType: soft

"@aztec/types@portal:../yarn-project/types::locator=%40aztec%2Fboxes%40workspace%3A.":
version: 0.0.0-use.local
resolution: "@aztec/types@portal:../yarn-project/types::locator=%40aztec%2Fboxes%40workspace%3A."
Expand Down Expand Up @@ -9476,6 +9490,13 @@ __metadata:
languageName: node
linkType: hard

"lodash.omit@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.omit@npm:4.5.0"
checksum: 3808b9b6faae35177174b6ab327f1177e29c91f1e98dcbccf13a72a6767bba337306449d537a4e0d8a33d2673f10d39bc732e30c4b803274ea0c1168ea60e549
languageName: node
linkType: hard

"lodash.times@npm:^4.3.2":
version: 4.3.2
resolution: "lodash.times@npm:4.3.2"
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"./abi": "./dest/api/abi.js",
"./account": "./dest/api/account.js",
"./aztec_address": "./dest/api/aztec_address.js",
"./deployment": "./dest/api/deployment.js",
"./eth_address": "./dest/api/eth_address.js",
"./ethereum": "./dest/api/ethereum.js",
"./fields": "./dest/api/fields.js",
Expand Down Expand Up @@ -51,6 +52,7 @@
"@aztec/circuits.js": "workspace:^",
"@aztec/ethereum": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/protocol-contracts": "workspace:^",
"@aztec/types": "workspace:^",
"tslib": "^2.4.0"
},
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/account_manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AccountContract } from '../account/contract.js';
import { Salt } from '../account/index.js';
import { AccountInterface } from '../account/interface.js';
import { DefaultWaitOpts, DeployMethod, WaitOpts } from '../contract/index.js';
import { ContractDeployer } from '../contract_deployer/index.js';
import { ContractDeployer } from '../deployment/index.js';
import { waitForAccountSynch } from '../utils/account.js';
import { generatePublicKey } from '../utils/index.js';
import { AccountWalletWithPrivateKey } from '../wallet/index.js';
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec.js/src/api/deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { registerContractClass } from '../deployment/register_class.js';
export { broadcastPrivateFunction, broadcastUnconstrainedFunction } from '../deployment/broadcast_function.js';
19 changes: 19 additions & 0 deletions yarn-project/aztec.js/src/contract/unsafe_contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ContractArtifact } from '@aztec/foundation/abi';
import { ContractInstanceWithAddress } from '@aztec/types/contracts';

import { Wallet } from '../wallet/index.js';
import { ContractBase } from './contract_base.js';

/** Unsafe constructor for ContractBase that bypasses the check that the instance is registered in the wallet. */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how/where any bypass takes place since we're just calling super?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor in the base class is protected, this class makes it public.

export class UnsafeContract extends ContractBase {
constructor(
/** The deployed contract instance definition. */
instance: ContractInstanceWithAddress,
/** The Application Binary Interface for the contract. */
artifact: ContractArtifact,
/** The wallet used for interacting with this contract. */
wallet: Wallet,
) {
super(instance, artifact, wallet);
}
}
118 changes: 118 additions & 0 deletions yarn-project/aztec.js/src/deployment/broadcast_function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
ARTIFACT_FUNCTION_TREE_MAX_HEIGHT,
MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS,
computeArtifactFunctionTree,
computeArtifactFunctionTreeRoot,
computeArtifactMetadataHash,
computeFunctionArtifactHash,
computePrivateFunctionsTree,
getContractClassFromArtifact,
} from '@aztec/circuits.js';
import { ContractArtifact, FunctionSelector, FunctionType, bufferAsFields } from '@aztec/foundation/abi';
import { padArrayEnd } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/fields';

import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js';
import { Wallet } from '../wallet/index.js';
import { getRegistererContract } from './protocol_contracts.js';

/**
* Sets up a call to broadcast a private function's bytecode via the ClassRegisterer contract.
* Note that this is not required for users to call the function, but is rather a convenience to make
* this code publicly available so dapps or wallets do not need to redistribute it.
* @param wallet - Wallet to send the transaction.
* @param artifact - Contract artifact that contains the function to be broadcast.
* @param selector - Selector of the function to be broadcast.
* @returns A ContractFunctionInteraction object that can be used to send the transaction.
*/
export function broadcastPrivateFunction(
wallet: Wallet,
artifact: ContractArtifact,
selector: FunctionSelector,
): ContractFunctionInteraction {
const contractClass = getContractClassFromArtifact(artifact);
const privateFunction = contractClass.privateFunctions.find(fn => fn.selector.equals(selector));
if (!privateFunction) {
throw new Error(`Private function with selector ${selector.toString()} not found`);
}
const privateFunctionArtifact = artifact.functions.find(fn =>
FunctionSelector.fromNameAndParameters(fn).equals(selector),
)!;

// TODO(@spalladino): The following is computing the unconstrained root hash twice.
// Feels like we need a nicer API for returning a hash along with all its preimages,
// since it's common to provide all hash preimages to a function that verifies them.
const artifactMetadataHash = computeArtifactMetadataHash(artifact);
const unconstrainedArtifactFunctionTreeRoot = computeArtifactFunctionTreeRoot(artifact, FunctionType.OPEN);

// We need two sibling paths because private function information is split across two trees:
// The "private function tree" captures the selectors and verification keys, and is used in the kernel circuit for verifying the proof generated by the app circuit.
// The "artifact tree" captures function bytecode and metadata, and is used by the pxe to check that its executing the code it's supposed to be executing, but it never goes into circuits.
const privateFunctionTreePath = computePrivateFunctionsTree(contractClass.privateFunctions).getSiblingPath(0);
const artifactFunctionTreePath = computeArtifactFunctionTree(artifact, FunctionType.SECRET)!.getSiblingPath(0);

const vkHash = privateFunction.vkHash;
const metadataHash = computeFunctionArtifactHash(privateFunctionArtifact);
const bytecode = bufferAsFields(
Buffer.from(privateFunctionArtifact.bytecode, 'hex'),
MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS,
);

const registerer = getRegistererContract(wallet);
return registerer.methods.broadcast_private_function(
contractClass.id,
Fr.fromBufferReduce(artifactMetadataHash),
Fr.fromBufferReduce(unconstrainedArtifactFunctionTreeRoot),
privateFunctionTreePath.map(Fr.fromBufferReduce),
padArrayEnd(artifactFunctionTreePath.map(Fr.fromBufferReduce), Fr.ZERO, ARTIFACT_FUNCTION_TREE_MAX_HEIGHT),
// eslint-disable-next-line camelcase
{ selector, metadata_hash: Fr.fromBufferReduce(metadataHash), bytecode, vk_hash: vkHash },
);
}

/**
* Sets up a call to broadcast an unconstrained function's bytecode via the ClassRegisterer contract.
* Note that this is not required for users to call the function, but is rather a convenience to make
* this code publicly available so dapps or wallets do not need to redistribute it.
* @param wallet - Wallet to send the transaction.
* @param artifact - Contract artifact that contains the function to be broadcast.
* @param selector - Selector of the function to be broadcast.
* @returns A ContractFunctionInteraction object that can be used to send the transaction.
*/
export function broadcastUnconstrainedFunction(
wallet: Wallet,
artifact: ContractArtifact,
selector: FunctionSelector,
): ContractFunctionInteraction {
const functionArtifactIndex = artifact.functions.findIndex(
fn => fn.functionType === FunctionType.UNCONSTRAINED && FunctionSelector.fromNameAndParameters(fn).equals(selector),
);
if (functionArtifactIndex < 0) {
throw new Error(`Unconstrained function with selector ${selector.toString()} not found`);
}
const functionArtifact = artifact.functions[functionArtifactIndex];

// TODO(@spalladino): Same comment as above on computing duplicated hashes.
const artifactMetadataHash = computeArtifactMetadataHash(artifact);
const privateArtifactFunctionTreeRoot = computeArtifactFunctionTreeRoot(artifact, FunctionType.SECRET);
const functionTreePath = computeArtifactFunctionTree(artifact, FunctionType.UNCONSTRAINED)!.getSiblingPath(
functionArtifactIndex,
);

const contractClassId = getContractClassFromArtifact(artifact).id;
const metadataHash = computeFunctionArtifactHash(functionArtifact);
const bytecode = bufferAsFields(
Buffer.from(functionArtifact.bytecode, 'hex'),
MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS,
);

const registerer = getRegistererContract(wallet);
return registerer.methods.broadcast_unconstrained_function(
contractClassId,
Fr.fromBufferReduce(artifactMetadataHash),
Fr.fromBufferReduce(privateArtifactFunctionTreeRoot),
padArrayEnd(functionTreePath.map(Fr.fromBufferReduce), Fr.ZERO, ARTIFACT_FUNCTION_TREE_MAX_HEIGHT),
// eslint-disable-next-line camelcase
{ selector, metadata_hash: Fr.fromBufferReduce(metadataHash), bytecode },
);
}
10 changes: 10 additions & 0 deletions yarn-project/aztec.js/src/deployment/protocol_contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getCanonicalClassRegisterer } from '@aztec/protocol-contracts/class-registerer';

import { UnsafeContract } from '../contract/unsafe_contract.js';
import { Wallet } from '../wallet/index.js';

/** Returns a Contract wrapper for the class registerer. */
export function getRegistererContract(wallet: Wallet) {
const { artifact, instance } = getCanonicalClassRegisterer();
return new UnsafeContract(instance, artifact, wallet);
}
15 changes: 15 additions & 0 deletions yarn-project/aztec.js/src/deployment/register_class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, getContractClassFromArtifact } from '@aztec/circuits.js';
import { ContractArtifact, bufferAsFields } from '@aztec/foundation/abi';

import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js';
import { Wallet } from '../wallet/index.js';
import { getRegistererContract } from './protocol_contracts.js';

/** Sets up a call to register a contract class given its artifact. */
export function registerContractClass(wallet: Wallet, artifact: ContractArtifact): ContractFunctionInteraction {
const { artifactHash, privateFunctionsRoot, publicBytecodeCommitment, packedBytecode } =
getContractClassFromArtifact(artifact);
const encodedBytecode = bufferAsFields(packedBytecode, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS);
const registerer = getRegistererContract(wallet);
return registerer.methods.register(artifactHash, privateFunctionsRoot, publicBytecodeCommitment, encodedBytecode);
}
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export {
DeploySentTx,
} from './contract/index.js';

export { ContractDeployer } from './contract_deployer/index.js';
export { ContractDeployer } from './deployment/index.js';

export {
generatePublicKey,
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/aztec.js/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
{
"path": "../foundation"
},
{
"path": "../protocol-contracts"
},
{
"path": "../types"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ exports[`ContractClass creates a contract class from a contract compilation arti
"isInternal": false
}
],
"id": "0x09ad0dad993129857629f13ec2f3463d0c15615bd35266d0fb26c8793c6ee050"
"id": "0x09ad0dad993129857629f13ec2f3463d0c15615bd35266d0fb26c8793c6ee050",
"privateFunctionsRoot": "0x05fa82a96814b6294d557d507151f7ccc12f70522ec4d9d0395a90e87e8087c6",
"publicBytecodeCommitment": "0x05a06c2ee54a742daa2fa43a111c03335df68ff89fd8e7ba938ca2efb225c885"
}"
`;
3 changes: 2 additions & 1 deletion yarn-project/circuits.js/src/contract/artifact_hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export function computeArtifactHash(artifact: ContractArtifact): Fr {
}

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.
// TODO(@spalladino): Should we use the sorted event selectors instead? They'd need to be unique for that.
const metadata = { name: artifact.name, events: artifact.events };
return sha256(Buffer.from(JSON.stringify(metadata), 'utf-8'));
}

Expand Down
7 changes: 3 additions & 4 deletions yarn-project/circuits.js/src/contract/contract_class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/fields';
import { ContractClass, ContractClassWithId } from '@aztec/types/contracts';

import { computeArtifactHash } from './artifact_hash.js';
import { computeContractClassId } from './contract_class_id.js';
import { ContractClassIdPreimage, computeContractClassIdWithPreimage } from './contract_class_id.js';
import { packBytecode } from './public_bytecode.js';

/** Contract artifact including its artifact hash */
Expand All @@ -12,7 +12,7 @@ type ContractArtifactWithHash = ContractArtifact & { artifactHash: Fr };
/** Creates a ContractClass from a contract compilation artifact. */
export function getContractClassFromArtifact(
artifact: ContractArtifact | ContractArtifactWithHash,
): ContractClassWithId {
): ContractClassWithId & ContractClassIdPreimage {
const artifactHash = 'artifactHash' in artifact ? artifact.artifactHash : computeArtifactHash(artifact);
const publicFunctions: ContractClass['publicFunctions'] = artifact.functions
.filter(f => f.functionType === FunctionType.OPEN)
Expand All @@ -36,8 +36,7 @@ export function getContractClassFromArtifact(
isInternal: f.isInternal,
})),
};
const id = computeContractClassId(contractClass);
return { ...contractClass, id };
return { ...contractClass, ...computeContractClassIdWithPreimage(contractClass) };
}

/**
Expand Down
27 changes: 18 additions & 9 deletions yarn-project/circuits.js/src/contract/contract_class_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,29 @@ import { computePrivateFunctionsRoot } from './private_function.js';
* @returns The identifier.
*/
export function computeContractClassId(contractClass: ContractClass | ContractClassIdPreimage): Fr {
const { artifactHash, privateFunctionsRoot, publicBytecodeCommitment } = isContractClassIdPreimage(contractClass)
? contractClass
: computeContractClassIdPreimage(contractClass);
return Fr.fromBuffer(
return computeContractClassIdWithPreimage(contractClass).id;
}

/** Computes a contract class id and returns it along with its preimage. */
export function computeContractClassIdWithPreimage(
contractClass: ContractClass | ContractClassIdPreimage,
): ContractClassIdPreimage & { id: Fr } {
const artifactHash = contractClass.artifactHash;
const privateFunctionsRoot =
'privateFunctionsRoot' in contractClass
? contractClass.privateFunctionsRoot
: computePrivateFunctionsRoot(contractClass.privateFunctions);
const publicBytecodeCommitment =
'publicBytecodeCommitment' in contractClass
? contractClass.publicBytecodeCommitment
: computePublicBytecodeCommitment(contractClass.packedBytecode);
const id = Fr.fromBuffer(
pedersenHash(
[artifactHash.toBuffer(), privateFunctionsRoot.toBuffer(), publicBytecodeCommitment.toBuffer()],
GeneratorIndex.CONTRACT_LEAF, // TODO(@spalladino): Review all generator indices in this file
),
);
return { id, artifactHash, privateFunctionsRoot, publicBytecodeCommitment };
}

/** Returns the preimage of a contract class id given a contract class. */
Expand All @@ -44,11 +58,6 @@ export type ContractClassIdPreimage = {
publicBytecodeCommitment: Fr;
};

/** Returns whether the given object looks like a ContractClassIdPreimage. */
function isContractClassIdPreimage(obj: any): obj is ContractClassIdPreimage {
return obj && obj.artifactHash && obj.privateFunctionsRoot && obj.publicBytecodeCommitment;
}

// TODO(@spalladino): Replace with actual implementation
export function computePublicBytecodeCommitment(bytecode: Buffer) {
return Fr.fromBufferReduce(sha256(bytecode));
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/cli/src/cmds/unbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const resolutions: { [key: string]: string } = {
'@aztec/bb.js': 'portal:.aztec-packages/barretenberg/ts',
'@aztec/circuit-types': 'portal:.aztec-packages/yarn-project/circuit-types',
'@aztec/ethereum': 'portal:.aztec-packages/yarn-project/ethereum',
'@aztec/protocol-contracts': 'portal:.aztec-packages/yarn-project/protocol-contracts',
'@aztec/types': 'portal:.aztec-packages/yarn-project/types',
};

Expand Down Expand Up @@ -71,6 +72,7 @@ function copyDependenciesToBox(dirName: string, destPath: string) {
'yarn-project/types',
'yarn-project/circuit-types',
'yarn-project/ethereum',
'yarn-project/protocol-contracts',
].forEach(path =>
cpSync(dirName + '/../../../../' + path, destPath + '/.aztec-packages/' + path, {
recursive: true,
Expand Down
Loading
Loading