Skip to content

Commit

Permalink
feat: Migrate accounts to auth witness (#2281)
Browse files Browse the repository at this point in the history
Changes all account contracts to use auth witnesses for authentication

**Noir**

- Auth witnesses now have variable length.
- Introduces an `AccountActions` module in Noir that abstracts most of
the account contract requirements, leaving only `is_valid` to be
implemented by the developer.
- Removes the `AccountInterface` helper in Noir in favor of two
`assert_valid` functions.

**Accounts**

- Introduces a `BaseAccountContract` in ts, such that an account dev
only needs to provide the abi, deploy args, and an `AuthWitnessProvider`
that is basically a signer. Removes all the different entrypoint
implementations, since we now have just a single one.
- Adds a set of `AuthWitness`es to a `TxExecutionRequest`, which is used
by the simulator during that tx run, so it doesn't need to be persisted
in the local db.
- Renames `Account` to `AccountManager`.
- Renames `Entrypoint` to `AccountInterface`.

**Wallet**
- Adds a single `createAuthWitness` method to the `Wallet` interface
that creates and registers the witness.
- Adds a convenience `setPublicAuth` to the `AccountWallet`
implementation for crafting the tx that sets a public auth (I think we
can do better here though, it should be easy for the dev to create an
action where they also include several public auths as part of a batch,
but we can push that for later).
- Kills `EntrypointWallet` and `AuthWitnessEntrypointWallet` in favor of
a single `AccountWallet`.

**Docs**
- Yes.

Fixes #2043
  • Loading branch information
spalladino authored Sep 15, 2023
1 parent f85ecd9 commit 91152af
Show file tree
Hide file tree
Showing 85 changed files with 2,696 additions and 2,040 deletions.
2 changes: 1 addition & 1 deletion docs/docs/about_aztec/roadmap/engineering_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Also, we do a lot of sha256-compressing in our kernel and rollup circuits for da
### Ultra -> Standard Squisher Circuit

## Authentication: access to private data
- Private data must not be returned to an app, unless the user authorises it.
- Private data must not be returned to an app, unless the user authorizes it.

## Validation: preventing execution of malicious bytecode
- A node should check that the bytecode provided by an application for a given app matches the leaf in the contract tree to ensure that user doesn't execute unplanned code which might access their notes.
Expand Down
10 changes: 10 additions & 0 deletions docs/docs/concepts/foundation/accounts/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ The protocol requires that every account is a contract for the purposes of sendi

However, this is not required when sitting on the receiving end. A user can deterministically derive their address from their encryption public key and the account contract they intend to deploy, and share this address with other users that want to interact with them _before_ they deploy the account contract.

### Authorizing actions

Account contracts are also expected, though not required by the protocol, to implement a set of methods for authorizing actions on behalf of the user. During a transaction, a contract may call into the account contract and request the user authorization for a given action, identified by a hash. This pattern is used, for instance, for transferring tokens from an account that is not the caller.

When executing a private function, this authorization is checked by requesting an _auth witness_ from the execution oracle, which is usually a signed message. The RPC Server is responsible for storing these auth witnesses and returning them to the requesting account contract. Auth witnesses can belong to the current user executing the local transaction, or to another user who shared it out-of-band.

However, during a public function execution, it is not possible to retrieve a value from the local oracle. To support authorizations in public functions, account contracts should save in contract storage what actions have been pre-authorized by their owner.

These two patterns combined allow an account contract to answer whether an action `is_valid` for a given user both in private and public contexts.

### Encryption and nullifying keys

Aztec requires users to define [encryption and nullifying keys](./keys.md) that are needed for receiving and spending private notes. Unlike transaction signing, encryption and nullifying is enshrined at the protocol. This means that there is a single scheme used for encryption and nullifying. These keys are derived from a master public key. This master public key, in turn, is used when deterministically deriving the account's address.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/dev_docs/limitations/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ This pattern is likely to develop in Aztec as well, except there's a problem: pr

### No private data authentication

Private data should not be returned to an app, unless the user authorises such access to the app. An authorisation layer is not-yet in place.
Private data should not be returned to an app, unless the user authorizes such access to the app. An authorization layer is not-yet in place.

#### What are the consequences?

Expand Down
10 changes: 5 additions & 5 deletions docs/docs/dev_docs/wallets/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ Architecture-wise, a wallet is an instance of an **Aztec RPC Server** which mana

Additionally, a wallet must be able to handle one or more [account contract implementations](../../concepts/foundation/accounts/main.md#account-contracts-and-wallets). When a user creates a new account, the account is represented on-chain by an account contract. The wallet is responsible for deploying and interacting with this contract. A wallet may support multiple flavours of accounts, such as an account that uses ECDSA signatures, or one that relies on WebAuthn, or one that requires multi-factor authentication. For a user, the choice of what account implementation to use is then determined by the wallet they interact with.

In code, this translates to a wallet implementing an **Entrypoint** interface that defines [how to create an _execution request_ out of an array of _function calls_](./main.md#transaction-lifecycle) for the specific implementation of an account contract. Think of the entrypoint interface as the Javascript counterpart of an account contract, or the piece of code that knows how to format and authenticate a transaction based on the rules defined in Aztec.nr by the user's account.
In code, this translates to a wallet implementing an **AccountInterface** interface that defines [how to create an _execution request_ out of an array of _function calls_](./main.md#transaction-lifecycle) for the specific implementation of an account contract and [how to generate an _auth witness_](./main.md#authorizing-actions) for authorizing actions on behalf of the user. Think of this interface as the Javascript counterpart of an account contract, or the piece of code that knows how to format a transaction and authenticate an action based on the rules defined by the user's account contract implementation.

## Entrypoint interface
## Account interface

The entrypoint interface is used for creating an _execution request_ out of one or more _function calls_ requested by a dapp. Account contracts are expected to handle multiple function calls per transaction, since dapps may choose to batch multiple actions into a single request to the wallet.
The account interface is used for creating an _execution request_ out of one or more _function calls_ requested by a dapp, as well as creating an _auth witness_ for a given message hash. Account contracts are expected to handle multiple function calls per transaction, since dapps may choose to batch multiple actions into a single request to the wallet.

#include_code entrypoint-interface /yarn-project/aztec.js/src/account/entrypoint/index.ts typescript
#include_code account-interface yarn-project/aztec.js/src/account/interface.ts typescript

Refer to the page on [writing an account contract](./writing_an_account_contract.md) for an example on how to implement this interface.

## RPC interface

A wallet exposes the RPC interface to dapps by running an [Aztec RPC Server instance](https://github.com/AztecProtocol/aztec-packages/blob/95d1350b23b6205ff2a7d3de41a37e0bc9ee7640/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts). The Aztec RPC Server requires a keystore and a database implementation for storing keys, private state, and recipient encryption public keys.
A wallet exposes the RPC interface to dapps by running an Aztec RPC Server instance. The Aztec RPC Server requires a keystore and a database implementation for storing keys, private state, and recipient encryption public keys.

#include_code rpc-interface /yarn-project/types/src/interfaces/aztec_rpc.ts typescript

Expand Down
9 changes: 8 additions & 1 deletion docs/docs/dev_docs/wallets/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ Finally, the wallet **sends** the resulting _transaction_ object, which includes
:::warning
There are no proofs generated as of the Sandbox release. This will be included in a future release before testnet.
:::

## Authorizing actions

Account contracts in Aztec expose an interface for other contracts to validate [whether an action is authorized by the account or not](../../concepts/foundation/accounts/main.md#authorizing-actions). For example, an application contract may want to transfer tokens on behalf of a user, in which case the token contract will check with the account contract whether the application is authorized to do so. These actions may be carried out in private or in public functions, and in transactions originated by the user or by someone else.

Wallets should manage these authorizations, prompting the user when they are requested by an application. Authorizations in private executions come in the form of _auth witnesses_, which are usually signatures over an identifier for an action. Applications can request the wallet to produce an auth witness via the `createAuthWitness` call. In public functions, authorizations are pre-stored in the account contract storage, which is handled by a call to an internal function in the account contract implementation.

## Key management

As in EVM-based chains, wallets are expected to manage user keys, or provide an interface to hardware wallets or alternative key stores. Keep in mind that in Aztec each account requires [two sets of keys](../../concepts/foundation/accounts/keys.md): privacy keys and authentication keys. Privacy keys are mandated by the protocol and used for encryption and nullification, whereas authentication keys are dependent on the account contract implementation rolled out by the wallet. Should the account contract support it, wallets must provide the user with the means to rotate or recover their authentication keys.
Expand All @@ -56,5 +63,5 @@ At the time of this writing, all private state is encrypted and broadcasted thro

Encrypted data blobs do not carry any public information as to whom their recipient is. Therefore, it is not possible for a remote node to identify the notes that belong to a user, and it is not possible for a wallet to query a remote node for its private state. As such, wallets need to keep a local database of their accounts private state, in order to be able to answer any queries on their private state.

Dapps may require access to the user's private state, in order to show information relevant to the current application. For instance, a dapp for a token may require access to the user's private notes in the token contract in order to display the user's balance. It is responsibility of the wallet to require authorisation from the user before disclosing private state to a dapp.
Dapps may require access to the user's private state, in order to show information relevant to the current application. For instance, a dapp for a token may require access to the user's private notes in the token contract in order to display the user's balance. It is responsibility of the wallet to require authorization from the user before disclosing private state to a dapp.

42 changes: 16 additions & 26 deletions docs/docs/dev_docs/wallets/writing_an_account_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This tutorial will take you through the process of writing your own account contract in Noir, along with the Typescript glue code required for using it within a [wallet](./main.md).

Writing your own account contract allows you to define the rules by which user transactions are authorised and paid for, as well as how user keys are managed (including key rotation and recovery). In other words, writing an account contract lets you make the most out of [account abstraction](../../concepts/foundation/accounts/main.md#what-is-account-abstraction) in the Aztec network.
Writing your own account contract allows you to define the rules by which user transactions are authorized and paid for, as well as how user keys are managed (including key rotation and recovery). In other words, writing an account contract lets you make the most out of [account abstraction](../../concepts/foundation/accounts/main.md#what-is-account-abstraction) in the Aztec network.

It is highly recommended that you understand how an [account](../../concepts/foundation/accounts/main.md) is defined in Aztec, as well as the differences between privacy and authentication [keys](../../concepts/foundation/accounts/keys.md). You will also need to know how to write a [contract in Noir](../contracts/main.md), as well as some basic [Typescript](https://www.typescriptlang.org/).

Expand Down Expand Up @@ -30,58 +30,48 @@ Public Key: 0x0ede151adaef1cfcc1b3e152ea39f00c5cda3f3857cef00decb049d283672dc71
```
:::

The important part of this contract is the `entrypoint` function, which will be the first function executed in any transaction originated from this account. This function has two main responsibilities: 1) authenticating the transaction and 2) executing calls. It receives a `payload` with the list of function calls to execute, as well as a signature over that payload.
The important part of this contract is the `entrypoint` function, which will be the first function executed in any transaction originated from this account. This function has two main responsibilities: authenticating the transaction and executing calls. It receives a `payload` with the list of function calls to execute, and requests a corresponding auth witness from an oracle to validate it. You will find this logic implemented in the `AccountActions` module, which uses the `EntrypointPayload` struct:

#include_code entrypoint yarn-project/aztec-nr/aztec/src/account.nr rust

#include_code entrypoint-struct yarn-project/aztec-nr/aztec/src/entrypoint.nr rust

:::info
Using the `EntrypointPayload` struct is not mandatory. You can package the instructions to be carried out by your account contract however you want. However, the entrypoint payload already provides a set of helper functions, both in Noir and Typescript, that can save you a lot of time when writing a new account contract.
Using the `AccountActions` module and the `EntrypointPayload` struct is not mandatory. You can package the instructions to be carried out by your account contract however you want. However, using these modules can save you a lot of time when writing a new account contract, both in Noir and in Typescript.
:::

Let's go step by step into what the `entrypoint` function is doing:

#include_code entrypoint-auth yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr rust

We authenticate the transaction. To do this, we serialise and Pedersen-hash the payload, which contains the instructions to be carried out along with a nonce. We then assert that the signature verifies against the resulting hash and the contract public key. This makes a transaction with an invalid signature unprovable.

#include_code entrypoint-auth yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr rust

Last, we execute the calls in the payload struct. The `execute_calls` helper function runs through the private and public calls included in the entrypoint payload and executes them:
The `AccountActions` module provides default implementations for most of the account contract methods needed, but it requires a function for validating an auth witness. In this function you will customise how your account validates an action: whether it is using a specific signature scheme, a multi-party approval, a password, etc.

#include_code entrypoint-execute-calls yarn-project/aztec-nr/aztec/src/entrypoint.nr rust

Note the usage of the `_with_packed_args` variant of [`call_public_function` and `call_private_function`](../contracts/functions.md#calling-functions). Due to Noir limitations, we cannot include more than a small number of arguments in a function call. However, we can bypass this restriction by using a hash of the arguments in a function call, which gets automatically expanded to the full set of arguments when the nested call is executed. We call this _argument packing_.
#include_code is-valid yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr rust

For our account contract, we will take the hash of the action to authorize, request the corresponding auth witness from the oracle, and validate it against our hardcoded public key. If the signature is correct, we authorize the action.
## The typescript side of things

Now that we have a valid Aztec.nr account contract, we need to write the typescript glue code that will take care of formatting and authenticating transactions so they can be processed by our contract, as well as deploying the contract during account setup. This takes the form of implementing the `AccountContract` interface:
Now that we have a valid account contract, we need to write the typescript glue code that will take care of formatting and authenticating transactions so they can be processed by our contract, as well as deploying the contract during account setup. This takes the form of implementing the `AccountContract` interface:

#include_code account-contract-interface yarn-project/aztec.js/src/account/contract/index.ts typescript

The most interesting bit here is creating an `Entrypoint`, which is the piece of code that converts from a list of function calls requested by the user into a transaction execution request that can be simulated and proven:

#include_code entrypoint-interface yarn-project/aztec.js/src/account/entrypoint/index.ts typescript

For our account contract, we need to assemble the function calls into a payload, sign it using Schnorr, and encode these arguments for our `entrypoint` function. Let's see how it would look like:
However, if you are using the default `AccountActions` module, then you can leverage the `BaseAccountContract` class and just implement the logic for generating an auth witness that matches the one you wrote in Noir:

#include_code account-contract yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts typescript

Note that we are using the `buildPayload` and `hashPayload` helpers for assembling and Pedersen-hashing the `EntrypointPayload` struct for our `entrypoint` function. As mentioned, this is not required, and you can define your own structure for instructing your account contract what functions to run.
As you can see in the snippet above, to fill in this base class, we need to define three things:
- The build artifact for the corresponding account contract.
- The deployment arguments.
- How to create an auth witness.

Then, we are using the `Schnorr` signer from the `@aztec/circuits.js` package to sign over the payload hash. This signer maps to exactly the same signing scheme that Noir's standard library expects in `schnorr::verify_signature`.
In our case, the auth witness will be generated by Schnorr-signing over the message identifier using the hardcoded key. To do this, we are using the `Schnorr` signer from the `@aztec/circuits.js` package to sign over the payload hash. This signer maps to exactly the same signing scheme that Noir's standard library expects in `schnorr::verify_signature`.

:::info
More signing schemes are available in case you want to experiment with other types of keys. Check out Noir's [documentation on cryptographic primitives](https://noir-lang.org/standard_library/cryptographic_primitives).
:::

Last, we use the `buildTxExecutionRequest` helper function to assemble the transaction execution request from the arguments and entrypoint. Note that we are also including the set of packed arguments that map to each of the nested calls: these are required for unpacking the arguments in functions calls executed via `_with_packed_args`.

## Trying it out

Let's try creating a new account backed by our account contract, and interact with a simple token contract to test it works.

<!-- TODO: Link to docs showing how to get an instance of Aztec RPC server -->
To create and deploy the account, we will use the `Account` class, which takes an instance of an Aztec RPC server, a [privacy private key](../../concepts/foundation/accounts/keys.md#privacy-keys), and an instance of our `AccountContract` class:
To create and deploy the account, we will use the `AccountManager` class, which takes an instance of an Aztec RPC server, a [privacy private key](../../concepts/foundation/accounts/keys.md#privacy-keys), and an instance of our `AccountContract` class:

#include_code account-contract-deploy yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts typescript

Expand Down
Loading

0 comments on commit 91152af

Please sign in to comment.