Skip to content

Commit

Permalink
docs(yellow-paper): Contract deployment (AztecProtocol#3624)
Browse files Browse the repository at this point in the history
Adds section on contract deployment, using nullifier trees and full
constructor abstraction.

Fixes AztecProtocol#3104
  • Loading branch information
spalladino committed Jan 2, 2024
1 parent 56992ae commit b282867
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
72 changes: 72 additions & 0 deletions yellow-paper/docs/contract-deployment/classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Contract classes

A contract class is a collection of related unconstrained, private, and public functions. Contract classes don't have state, they just define code.

## Rationale

Contract classes simplify the process of reusing code by enshrining implementations as a first-class citizen at the protocol. Given multiple contract instances that rely on the same class, the class needs to be declared only once, reducing the deployment cost for all contract instances. Classes also simplify the process of upgradeability. Classes decouple state from code, making it easier for an instance to switch to different code while retaining its state.

:::info
Read the following discussions for additional context:
- [Abstracting contract deployment](https://forum.aztec.network/t/proposal-abstracting-contract-deployment/2576)
- [Implementing contract upgrades](https://forum.aztec.network/t/implementing-contract-upgrades/2570)
- [Contract classes, upgrades, and default accounts](https://forum.aztec.network/t/contract-classes-upgrades-and-default-accounts/433)
:::

## Structure

The structure of a contract class is defined as:

| Field | Type | Description |
|----------|----------|----------|
| version | u8 | Version identifier. Initially one, bumped for any changes to the contract class struct. |
| registerer_address | AztecAddress | Address of the canonical contract used for registering this class. |
| artifact_hash | Field | Hash of the entire contract artifact, including compiler information, proving backend, and all generated ACIR and Brillig. The specification of this hash is left to the compiler and not enforced by the protocol. |
| constructor_function | PrivateFunction | PublicFunction | Constructor for instances of this class. |
| private_functions | PrivateFunction[] | List of private functions. |
| public_functions | PublicFunction[] | List of public functions. |
| unconstrained_functions | UnconstrainedFunction[] | List of unconstrained functions. |

<!-- TODO: Do we need the artifact hash, if we're including the artifact hash of each individual function? -->
<!-- NOTE: I'm deliberately omitting the portal bytecode hash here. -->

### Private Function

| Field | Type | Description |
|----------|----------|----------|
| function_selector | u32 | Selector of the function. Calculated as the hash of the method name and arguments. |
| vk_hash | Field | Hash of the verification key associated to this private function. |
| salt | Field | Optional value for salting the bytecode of a function. |
| bytecode_hash | Field | Hash of the compiled function artifact, including all generated ACIR and Brillig. |
| optional_bytecode | Buffer | Optional bytecode for the function. Private function bytecode can be kept private if desired and only broadcasted to participants of the contract. |

### Public and Unconstrained Function

| Field | Type | Description |
|----------|----------|----------|
| function_selector | u32 | Selector of the function. Calculated as the hash of the method name and arguments. |
| bytecode_hash | Field | Hash of the compiled function artifact, including all generated Brillig code. |
| bytecode | Buffer | Full bytecode for the function. Must hash to the `artifact_hash`. |

<!-- TODO: Expand on the bytecode commitment scheme and bytecode_hash, both here and for private fns. -->

### Class Identifier

The class identifier is computed by merkleizing the lists of private, public, and unconstrained functions separately, replacing the functions lists in the contract class struct with their respective tree roots, and then hashing the resulting struct.

## Registration

A contract class is registered by calling a private `register` function in a canonical `ClassRegisterer` contract. The `register` function receives a `ContractClass` struct as defined above, except for the `registerer_address`, and performs the following checks:

- `version` is 1 for the initial release
- `bytecode` for each function hashes to the `bytecode_hash`

The `register` function then:

- Emits the `ContractClass` struct as unencrypted events.
- Computes the class identifier as the hash of the `ContractClass` object.
- Emits the computed class identifier as a nullifier.

Upon seeing a new contract class registration event in a mined transaction, nodes are expected to store the contract class, so they can retrieve it when executing a public function for that class.

Note that emitting the class identifier as a nullifier, instead of as an entry in the note hashes tree, allows public functions to prove non-existence of a class, which is required to support public contract instance deployments.
11 changes: 11 additions & 0 deletions yellow-paper/docs/contract-deployment/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Contract Deployment
---

# Contract Deployment

Contracts in Aztec are deployed as _instances_ of a contract _class_. Deploying a new contract then requires first registering the _class_, if it has not been registered before, and then creating an _instance_ that references the class. Both classes and instances are committed to in the nullifier tree in the global state, and are created via a call to a canonical class registry or instance deployer contract respectively.

import DocCardList from '@theme/DocCardList';

<DocCardList />
86 changes: 86 additions & 0 deletions yellow-paper/docs/contract-deployment/instances.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Contract instances

A contract instance is a concrete deployment of a [contract class](./classes.md). A contract instance has state, both private and public, as well as an address that acts as identifier, and can be called into. A contract instance always references a contract class, that dictates what code it executes when called.

## Requirements

- Users must be able to precompute the address of a given deployment. This allows users to precompute their account contract addresses and receive funds before interacting with the chain, and also allows counterfactual deployments.
- An address must be linked to its deployer address. This allows simple diversified and stealth account contracts. Related, a precomputed deployment may or may not be restricted to be executed by a given address.
- A user calling into an address must be able to prove that it has not been deployed. This allows the executor to prove that a given call in a transaction is unsatisfiable and revert accordingly.
- A user should be able to privately call into a contract without publicly deploying it. This allows private applications to deploy contracts without leaking information about their usage.

## Structure

The structure of a contract instance is defined as:

| Field | Type | Description |
|----------|----------|----------|
| version | u8 | Version identifier. Initially one, bumped for any changes to the contract instance struct. |
| deployer_address | AztecAddress | Address of the canonical deployer contract that creates this instance. |
| salt | Field | User-generated pseudorandom value for uniqueness. |
| contract_class_id | Field | Identifier of the contract class for this instance. |
| contract_args_hash | Field | Hash of the arguments to the constructor. |
| portal_contract_address | EthereumAddress | Optional address of the L1 portal contract. |
| public_keys_hash | Field | Optional hash of the struct of public keys used for encryption and nullifying by this contract. |

<!-- TODO: Ensure the spec above matches the one described in Addresses and Keys. -->

## Address

The address of the contract instance is computed as the hash of all elements in the structure above, as defined in the addresses and keys section. This computation is deterministic, which allows any user to precompute the expected deployment address of their contract, including account contracts.

## Statuses

A contract instance at a given address can be in any of the following statuses:
- **Undeployed**: The instance has not yet been deployed. A user who knows the preimage of the address can issue a private call into the contract, as long as it does not require initialization. Public function calls to this address will fail.
- **Privately deployed**: The instance constructor has been executed, but its class identifier has not been broadcasted. A user who knows the preimage of the address can issue a private call into the contract. Public function calls to this address will fail. Private deployments are signalled by emitting an initialization nullifier when the constructor runs.
- **Publicly deployed**: The instance constructor has been executed, and the address preimage has been broadcasted. All function calls to the address, private or public, are valid. Public deployments are signalled by emitting a public deployment nullifier.

<!-- TODO: Validate the need for private deployments. They seem cool, and do not incur in extra protocol complexity, but they require having two nullifiers per contract: one to signal initialization, and one to signal broadcasting of the deployment. -->

## Constructors

Contract constructors are not enshrined in the protocol, but handled at the application circuit level. A contract must satisfy the following requirements:
- The constructor must be invoked exactly once
- The constructor must be invoked with the arguments in the address preimage
- Functions that depend on contract initialization cannot be invoked until the constructor is run

These checks can be embedded in the application circuit itself. The constructor emits a standardized initialization nullifier when it is invoked, which prevents it from being called more than once. The constructor code must also check that the arguments hash it received matches the ones in the address preimage, supplied via an oracle call. All other functions in the contract must include a merkle membership proof for the nullifier, to prevent them from being called before the constructor is invoked. Note that a contract may choose to allow some functions to be called before initialization.

The checks listed above should not be manually implemented by a contract developer, but rather included as part of the Aztec macros for Noir.

Constructors may be either private or public functions. Contracts with private constructors can be privately or publicly deployed, contracts with public constructors can only be publicly deployed.

Removing constructors from the protocol itself simplifies the kernel circuit. Separating initialization from public deployment also allows to implement private deployments, since a private deployment is equivalent to just invoking the constructor function at a given address.

## Public Deployment

A new contract instance can be _publicly deployed_ by calling a `deploy` function in a canonical `Deployer` contract. This function receives the arguments for a `ContractInstance` struct as described above, except for the `deployer_address` which is the deployer's own address, and executes the following actions:

- Validates the referenced `contract_class_id` exists.
- Mixes in the `msg_sender` with the user-provided `salt` by hashing them together, ensuring that the deployment is unique for the requester.
- Computes the resulting contract address.
- Emits the resulting address as a nullifier to signal the public deployment, so callers can prove that the contract has or has not been publicly deployed.
- Emits an unencrypted event with the address preimage, excluding the `deployer_address` which is already part of the log.
- Either proves the corresponding class identifier has no constructor, or calls the constructor with the preimage of the supplied `contract_args_hash`, or proves that the constructor nullifier has already been emitted so a previously privately-deployed contract can be publicly deployed.

Upon seeing a new contract deployed event from the canonical deployer contract, nodes are expected to store the address and preimage, to verify executed code during public code execution as described in the next section.

The `Deployer` contract provides two implementations of the `deploy` function: a private and a public one. Contracts with a private constructor are expected to use the former, and contracts with public constructors expected to use the latter. Contracts with no constructors or that have already been privately-deployed can use either.

Additionally, the `Deployer` contract provides two `universal_deploy` functions, a private and a public one, with the same arguments as the `deploy` one, that just forwards the call to the `deploy` contract. This makes `msg_sender` in the `deploy` contract to be the `Deployer` contract itself, and allows for universal deployments that are semantically decoupled from their deployer, and can be permissionlessly invoked by any user who knows the address preimage.

## Verification of Executed Code

The kernel circuit, both private and public, is responsible for verifying that the code loaded for a given function execution matches the expected one. This requires the following checks:
- The contract class identifier of the address called is the expected one, verified by hashing the address preimage that includes the class id.
- The function identifier being executed is part of the class id, verified via a merkle membership proof.
- The function code executed matches the commitment in the function identifier, verified via a merkle membership proof and a bytecode commitment proof.

Note that, since constructors are handled at the application level, the kernel circuit is not required to check that a constructor has been run before executing code.

The public kernel circuit, additionally, needs to check that the corresponding contract instance has been publicly deployed, or prove that it hasn't. This is done via a merkle (non-)membership proof of the public deployment nullifier.

## Discarded Approaches

Earlier versions of the protocol relied on a dedicated contract tree, which required dedicated kernel code to process deployments, which had to be enshrined as new outputs from the application circuits. By abstracting contract deployment and storing deployments as nullifiers, the interface between the application and kernel circuits is simplified, and the kernel circuit has fewer responsibilities.

0 comments on commit b282867

Please sign in to comment.