From cf3a745612d2a49f289e877368a7241826612b94 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 14 Sep 2023 16:29:47 -0300 Subject: [PATCH] Update docs --- .../docs/concepts/foundation/accounts/main.md | 10 +++++ docs/docs/dev_docs/wallets/architecture.md | 10 ++--- docs/docs/dev_docs/wallets/main.md | 7 ++++ .../wallets/writing_an_account_contract.md | 40 +++++++------------ yarn-project/aztec-nr/aztec/src/account.nr | 2 + .../aztec.js/src/account/contract/index.ts | 2 + .../aztec.js/src/account/interface.ts | 6 ++- .../src/e2e_lending_contract.test.ts | 16 ++------ .../end-to-end/src/e2e_token_contract.test.ts | 19 ++++----- .../writing_an_account_contract.test.ts | 10 ++--- .../src/main.nr | 4 +- 11 files changed, 61 insertions(+), 65 deletions(-) diff --git a/docs/docs/concepts/foundation/accounts/main.md b/docs/docs/concepts/foundation/accounts/main.md index 8666a33eff9..56875ba9220 100644 --- a/docs/docs/concepts/foundation/accounts/main.md +++ b/docs/docs/concepts/foundation/accounts/main.md @@ -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. diff --git a/docs/docs/dev_docs/wallets/architecture.md b/docs/docs/dev_docs/wallets/architecture.md index 2d81c594655..58723ffc32e 100644 --- a/docs/docs/dev_docs/wallets/architecture.md +++ b/docs/docs/dev_docs/wallets/architecture.md @@ -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 diff --git a/docs/docs/dev_docs/wallets/main.md b/docs/docs/dev_docs/wallets/main.md index f1bd2832720..931a8901532 100644 --- a/docs/docs/dev_docs/wallets/main.md +++ b/docs/docs/dev_docs/wallets/main.md @@ -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. diff --git a/docs/docs/dev_docs/wallets/writing_an_account_contract.md b/docs/docs/dev_docs/wallets/writing_an_account_contract.md index 4e42ae43efa..127bea48e44 100644 --- a/docs/docs/dev_docs/wallets/writing_an_account_contract.md +++ b/docs/docs/dev_docs/wallets/writing_an_account_contract.md @@ -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. -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 diff --git a/yarn-project/aztec-nr/aztec/src/account.nr b/yarn-project/aztec-nr/aztec/src/account.nr index c6cac6bf100..13ebb36ae0e 100644 --- a/yarn-project/aztec-nr/aztec/src/account.nr +++ b/yarn-project/aztec-nr/aztec/src/account.nr @@ -34,6 +34,7 @@ impl AccountActions { AccountActions::init(Context::public(context), approved_action_storage_slot, is_valid_impl) } + // docs:start:entrypoint fn entrypoint(self, payload: EntrypointPayload) { let message_hash = payload.hash(); let valid_fn = self.is_valid_impl; @@ -41,6 +42,7 @@ impl AccountActions { assert(valid_fn(private_context, message_hash)); payload.execute_calls(private_context); } + // docs:end:entrypoint fn is_valid(self, message_hash: Field) -> Field { let valid_fn = self.is_valid_impl; diff --git a/yarn-project/aztec.js/src/account/contract/index.ts b/yarn-project/aztec.js/src/account/contract/index.ts index 7c950ceda5e..12b3f083e27 100644 --- a/yarn-project/aztec.js/src/account/contract/index.ts +++ b/yarn-project/aztec.js/src/account/contract/index.ts @@ -26,6 +26,8 @@ export interface AccountContract { /** * Returns the account interface for this account contract given a deployment at the provided address. + * The account interface is responsible for assembling tx requests given requested function calls, and + * for creating signed auth witnesses given action identifiers (message hashes). * @param address - Address where this account contract is deployed. * @param nodeInfo - Info on the chain where it is deployed. * @returns An account interface instance for creating tx requests and authorising actions. diff --git a/yarn-project/aztec.js/src/account/interface.ts b/yarn-project/aztec.js/src/account/interface.ts index ffb89b2844f..d12f3ae2d89 100644 --- a/yarn-project/aztec.js/src/account/interface.ts +++ b/yarn-project/aztec.js/src/account/interface.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/circuits.js'; import { AuthWitness, CompleteAddress, FunctionCall, TxExecutionRequest } from '@aztec/types'; +// docs:start:account-interface /** Creates authorisation witnesses. */ export interface AuthWitnessProvider { /** @@ -26,6 +27,9 @@ export interface EntrypointInterface { * requests and authorise actions for its corresponding account. */ export interface AccountInterface extends AuthWitnessProvider, EntrypointInterface { - /** Returns the complete address for this account. */ + /** + * Returns the complete address for this account. + */ getCompleteAddress(): CompleteAddress; } +// docs:end:account-interface diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 60679c24530..52ea53bca00 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -1,20 +1,10 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { - AccountWallet, - CheatCodes, - Fr, - SentTx, - computeMessageSecretHash -} from '@aztec/aztec.js'; +import { AccountWallet, CheatCodes, Fr, SentTx, computeMessageSecretHash } from '@aztec/aztec.js'; import { CircuitsWasm, CompleteAddress, FunctionSelector, GeneratorIndex } from '@aztec/circuits.js'; import { pedersenPlookupCompressWithHashIndex } from '@aztec/circuits.js/barretenberg'; import { DebugLogger } from '@aztec/foundation/log'; -import { - LendingContract, - PriceFeedContract, - TokenContract, -} from '@aztec/noir-contracts/types'; +import { LendingContract, PriceFeedContract, TokenContract } from '@aztec/noir-contracts/types'; import { AztecRPC, TxStatus } from '@aztec/types'; import { jest } from '@jest/globals'; @@ -93,7 +83,7 @@ describe('e2e_lending_contract', () => { beforeAll(async () => { ({ aztecNode, aztecRpcServer, logger, cheatCodes: cc, wallet, accounts } = await setup(1)); ({ lendingContract, priceFeedContract, collateralAsset, stableCoin } = await deployContracts()); - + lendingAccount = new LendingAccount(accounts[0].address, new Fr(42)); // Also specified in `noir-contracts/src/contracts/lending_contract/src/main.nr` diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts index 4ea706aae60..7a72c3cafc1 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract.test.ts @@ -1,16 +1,7 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecRPCServer } from '@aztec/aztec-rpc'; -import { - AccountWallet, - computeMessageSecretHash -} from '@aztec/aztec.js'; -import { - CircuitsWasm, - CompleteAddress, - Fr, - FunctionSelector, - GeneratorIndex -} from '@aztec/circuits.js'; +import { AccountWallet, computeMessageSecretHash } from '@aztec/aztec.js'; +import { CircuitsWasm, CompleteAddress, Fr, FunctionSelector, GeneratorIndex } from '@aztec/circuits.js'; import { pedersenPlookupCompressWithHashIndex } from '@aztec/circuits.js/barretenberg'; import { DebugLogger } from '@aztec/foundation/log'; import { TokenContract } from '@aztec/noir-contracts/types'; @@ -49,7 +40,11 @@ describe('e2e_token_contract', () => { asset = await TokenContract.deploy(wallets[0]).send().deployed(); logger(`Token deployed to ${asset.address}`); - tokenSim = new TokenSimulator(asset, logger, accounts.map(a => a.address)); + tokenSim = new TokenSimulator( + asset, + logger, + accounts.map(a => a.address), + ); await asset.methods._initialize({ address: accounts[0].address }).send().wait(); expect(await asset.methods.admin().view()).toBe(accounts[0].address.toBigInt()); diff --git a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts index f770b5f71e1..999be757003 100644 --- a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts +++ b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts @@ -69,16 +69,12 @@ describe('guides/writing_an_account_contract', () => { expect(balance).toEqual(150n); // docs:start:account-contract-fails + const walletAddress = wallet.getCompleteAddress(); const wrongKey = GrumpkinScalar.random(); const wrongAccountContract = new SchnorrHardcodedKeyAccountContract(wrongKey); - const wrongAccount = new AccountManager( - rpc, - encryptionPrivateKey, - wrongAccountContract, - wallet.getCompleteAddress(), - ); + const wrongAccount = new AccountManager(rpc, encryptionPrivateKey, wrongAccountContract, walletAddress); const wrongWallet = await wrongAccount.getWallet(); - const tokenWithWrongWallet = await PrivateTokenContract.at(token.address, wrongWallet); + const tokenWithWrongWallet = token.withWallet(wrongWallet); try { await tokenWithWrongWallet.methods.mint(200, address).simulate(); diff --git a/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr index 17f51a77263..abae13c0002 100644 --- a/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/schnorr_hardcoded_account_contract/src/main.nr @@ -8,8 +8,8 @@ global public_key_y: Field = 0x29155934ffaa105323695b5f91faadd84acc21f4a8bda2fad // Declared outside the contract to work around Noir error // "entered unreachable code: ICE: Params to the program should only contains numbers and arrays" +// docs:start:is-valid fn is_valid_impl(_context: &mut PrivateContext, message_hash: Field) -> bool { - // docs:start:entrypoint-auth // TODO: Workaround for https://github.com/noir-lang/noir/issues/2421 let message_bytes_slice = message_hash.to_be_bytes(32); let mut message_bytes: [u8; 32] = [0; 32]; @@ -22,10 +22,10 @@ fn is_valid_impl(_context: &mut PrivateContext, message_hash: Field) -> bool { let verification = std::schnorr::verify_signature(public_key_x, public_key_y, signature, message_bytes); assert(verification == true); - // docs:end:entrypoint-auth true } +// docs:end:is-valid // Account contract that uses Schnorr signatures for authentication using a hardcoded public key. contract SchnorrHardcodedAccount {