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: Migrate accounts to auth witness #2281

Merged
merged 15 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
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.

### Authorising actions

Account contracts are also expected, though not required by the protocol, to implement a set of methods for authorising actions on behalf of the user. During a transaction, a contract may call into the account contract and request the user authorisation 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 authorisation 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 authorisations in public functions, account contracts should save in contract storage what actions have been pre-authorised 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
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#authorising-actions) for authorising 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
7 changes: 7 additions & 0 deletions 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.
:::

## Authorising actions

Account contracts in Aztec expose an interface for other contracts to validate [whether an action is authorised by the account or not](../../concepts/foundation/accounts/main.md#authorising-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 authorised 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 authorisations, prompting the user when they are requested by an application. Authorisations 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, authorisations 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 Down
40 changes: 15 additions & 25 deletions docs/docs/dev_docs/wallets/writing_an_account_contract.md
Original file line number Diff line number Diff line change
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 authorise, request the corresponding auth witness from the oracle, and validate it against our hardcoded public key. If the signature is correct, we authorise 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
16 changes: 16 additions & 0 deletions yarn-project/acir-simulator/src/client/client_execution_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { computeUniqueCommitment, siloCommitment } from '@aztec/circuits.js/abis
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr, Point } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { AuthWitness } from '@aztec/types';

import {
ACVMField,
Expand Down Expand Up @@ -54,6 +55,8 @@ export class ClientTxExecutionContext {
/** The cache of packed arguments */
public packedArgsCache: PackedArgsCache,
private noteCache: ExecutionNoteCache,
/** List of transient auth witnesses to be used during this simulation */
private authWitnesses: AuthWitness[],
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
private log = createDebugLogger('aztec:simulator:client_execution_context'),
) {}

Expand All @@ -68,6 +71,19 @@ export class ClientTxExecutionContext {
this.historicBlockData,
this.packedArgsCache,
this.noteCache,
this.authWitnesses,
);
}

/**
* Returns an auth witness for the given message hash. Checks on the list of transient witnesses
* for this transaction first, and falls back to the local database if not found.
* @param messageHash - Hash of the message to authenticate.
* @returns Authentication witness for the requested message hash.
*/
public getAuthWitness(messageHash: Fr): Promise<Fr[] | undefined> {
return Promise.resolve(
this.authWitnesses.find(w => w.requestHash.equals(messageHash))?.witness ?? this.db.getAuthWitness(messageHash),
);
}

Expand Down
4 changes: 2 additions & 2 deletions yarn-project/acir-simulator/src/client/db_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ export interface DBOracle extends CommitmentsDB {

/**
* Retrieve the auth witness for a given message hash.
* @param message_hash - The message hash.
* @param messageHash - The message hash.
* @returns A Promise that resolves to an array of field elements representing the auth witness.
*/
getAuthWitness(message_hash: Fr): Promise<Fr[]>;
getAuthWitness(messageHash: Fr): Promise<Fr[]>;

/**
* Retrieve the secret key associated with a specific public key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('Private Execution test suite', () => {
functionData,
txContext: TxContext.from({ ...txContextFields, ...txContext }),
packedArguments: [packedArguments],
authWitnesses: [],
});

return acirSimulator.run(
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/acir-simulator/src/client/private_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export class PrivateFunctionExecution {
return toACVMField(await this.context.packedArgsCache.pack(args.map(fromACVMField)));
},
getAuthWitness: async ([messageHash]) => {
return (await this.context.db.getAuthWitness(fromACVMField(messageHash))).map(toACVMField);
const witness = await this.context.getAuthWitness(fromACVMField(messageHash));
Copy link
Contributor

Choose a reason for hiding this comment

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

if (!witness) throw new Error(`Authorisation not found for message hash ${fromACVMField(messageHash)}`);
return witness.map(toACVMField);
},
getSecretKey: ([ownerX], [ownerY]) => this.context.getSecretKey(this.contractAddress, ownerX, ownerY),
getPublicKey: async ([acvmAddress]) => {
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class AcirSimulator {
historicBlockData,
await PackedArgsCache.create(request.packedArguments),
new ExecutionNoteCache(),
request.authWitnesses,
),
entryPointABI,
contractAddress,
Expand Down Expand Up @@ -145,6 +146,7 @@ export class AcirSimulator {
historicBlockData,
await PackedArgsCache.create([]),
new ExecutionNoteCache(),
[],
),
entryPointABI,
contractAddress,
Expand Down
Loading