Skip to content

Commit

Permalink
feat: auth registry (#7035)
Browse files Browse the repository at this point in the history
This pr alters how we are using the public authwits. Namely it will
update where they are stored AND how they are consumed.

Instead of storing them in every account contract, they are now stored
in a shared public auth registry. The registry essentially keep track of
a large map, that maps `account => action => authorized`. When allowing
something with a public authwit, the `authorized` is set to `true` and
when consuming, the value is set to `false`.

The reasoning behind the change is fairly simple. If it was required to
authorize an authwit in the non-revertible phase of a transaction (say
for fees) it needed to execute code on the account contract. Since this
phase as phase don't support reverts while still paying a fee, it
allowed for a DOS of the sequencer by inserting very computational tasks
into your account contract.

When it is now in a common registry, the sequencer will only need to
execute enqueued public calls to this registry, which is much easier to
verify since it is the same for all accounts and have little flexibility
for extra logic.

In the old model, we were emitting a nullifier when the authwit was
"consumed" (public or private did not matter here). This change is
instead updating the mapping to ensure that there are no duplicate
nullifiers, and to support for "squashing" of public authwits. For
example if you are inserting the authwit because you need to use it in
the same transaction it can be removed again adding no state diff, this
reduces the data overhead by 96 bytes for this case, at the cost of
increasing by 32 bytes if the approval happens in a separate
transaction.

While we have solved the DOS issue that our old design had, it also take
away some of the flexibility that we had. Before you could have that
certain accounts where allowed to more flexible things on your behalf by
having extra logic for it, and also allow mass revoking. To support the
mass revoking case, the registry includes a `reject_all` map, that is
keeping a flag for each account whether to reject authwits or not. This
allows a user that have made approvals to pages that have not yet been
spent to update this one flag, revoking all of his approvals at once.
Ideally there should not be a lot of outstanding authwits, as they
should be created on demand, but for applications such as
request-for-quote DEXes, there might be authwits created that are
unspent.

API wise, there should be no big surprises for consuming authwits in
your contracts. The `auth` library have received a slight update to call
into the registry instead of the account contracts.

Also there are some helper functions for adding the authwits from
contracts.

The `AccountWallet` have been extended slightly to address the logic
better and now have a more clear distinction for what is public.
  • Loading branch information
LHerskind authored Jun 14, 2024
1 parent 92b1349 commit cea0b3b
Show file tree
Hide file tree
Showing 46 changed files with 669 additions and 494 deletions.
13 changes: 9 additions & 4 deletions docs/docs/aztec/concepts/accounts/authwit.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,30 @@ The above flow could be re-entered at token transfer. It is mainly for show to i

### What about public

As noted earlier, we could use the ERC20 standard for public. But this seems like a waste when we have the ability to try righting some wrongs. Instead, we can expand our AuthWit scheme to also work in public. This is actually quite simple, instead of asking an oracle (which we can't do as easily because not private execution) we can just store the AuthWit in the account contract, and look it up when we need it. While this needs the storage to be updated ahead of time, we can quite easily do so by batching the AuthWit updates with the interaction - a benefit of Account Contracts.
As noted earlier, we could use the ERC20 standard for public. But this seems like a waste when we have the ability to try righting some wrongs. Instead, we can expand our AuthWit scheme to also work in public. This is actually quite simple, instead of asking an oracle (which we can't do as easily because not private execution) we can just store the AuthWit in a shared registry, and look it up when we need it. While this needs the storage to be updated ahead of time (can be same tx), we can quite easily do so by batching the AuthWit updates with the interaction - a benefit of Account Contracts. A shared registry is used such that execution from the sequencers point of view will be more straight forward and predictable. Furthermore, since we have the authorization data directly in public state, if they are both set and unset (authorized and then used) in the same transaction, there will be no state effect after the transaction for the authorization which saves gas ⛽.

```mermaid
sequenceDiagram
actor Alice
participant AC as Alice Account
participant AR as Auth Registry
participant Token
participant Defi
rect rgb(191, 223, 255)
note right of Alice: Alice sends a batch
Alice->>AC: Allow Defi to call transfer(Alice, Defi, 1000);
Alice->>AC: Authorize Defi to call transfer(Alice, Defi, 1000);
activate AC
Alice->>AC: Defi.deposit(Token, 1000);
end
AC->>AR: Authorize Defi to call transfer(Alice, Defi, 1000);
AR->>AR: add authorize to true
AC->>Defi: deposit(Token, 1000);
activate Defi
Defi->>Token: transfer(Alice, Defi, 1000);
activate Token
Token->>AC: Check if Defi may call transfer(Alice, Defi, 1000);
AC->>Token: AuthWit validity
Token->>AR: Check if Defi may call transfer(Alice, Defi, 1000);
AR->>AR: set authorize to false
AR->>Token: AuthWit validity
Token->>Token: throw if invalid AuthWit
Token->>Token: transfer(Alice, Defi, 1000);
Token->>Defi: success
Expand Down
19 changes: 8 additions & 11 deletions docs/docs/guides/smart_contracts/writing_contracts/authwit.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,15 @@ sequenceDiagram
Note in particular that the request for a witness is done by the token contract, and the user will have to provide it to the contract before it can continue execution. Since the request is made all the way into the contract where it is to be used, we don't need to pass it along as an extra input to the functions before it which gives us a cleaner interface.
:::

As part of `AuthWit` we are assuming that the `on_behalf_of` implements the private and/or public functions:
As part of `AuthWit` we are assuming that the `on_behalf_of` implements the private function:

```rust
#[aztec(private)]
fn spend_private_authwit(inner_hash: Field) -> Field;

#[aztec(public)]
fn spend_public_authwit(inner_hash: Field) -> Field;
```

For public authwit, we have a shared registry that is used, there we are using a `consume` function.

Both return the value `0xabf64ad4` (`IS_VALID` selector) for a successful authentication, and `0x00000000` for a failed authentication. You might be wondering why we are expecting the return value to be a selector instead of a boolean. This is mainly to account for a case of selector collisions where the same selector is used for different functions, and we don't want an account to mistakenly allow a different function to be called on its behalf - it is hard to return the selector by mistake, but you might have other functions returning a bool.

## The `AuthWit` library.
Expand Down Expand Up @@ -113,7 +112,7 @@ This is to cover the case where the `on_behalf_of` might implemented some functi

### Utilities for public calls

Very similar to the above, we have variations that work in the public domain. These functions are wrapped to give a similar flow for both cases, but behind the scenes the logic of the account contracts is slightly different since they cannot use the oracle as they are not in the private domain.
Very similar to the above, we have variations that work in the public domain (`assert_current_call_valid_authwit_public`). These functions are wrapped to give a similar flow for both cases, but behind the scenes the logic is slightly different since the public goes to the auth registry, while the private flow calls the account contract.

#### Example

Expand Down Expand Up @@ -167,7 +166,7 @@ With private functions covered, how can we use this in a public function? Well,

#### Authenticating an action in TypeScript

Authenticating an action in the public domain is quite similar to the private domain, with the difference that we are executing a function on the account contract to add the witness, if you recall, this is because we don't have access to the oracle in the public domain.
Authenticating an action in the public domain is slightly different from the private domain, since we are executing a function on the auth registry contract to add the witness flag. As you might recall, this was to ensure that we don't need to call into the account contract from public, which is a potential DOS vector.

In the snippet below, this is done as a separate contract call, but can also be done as part of a batch as mentioned in the [Accounts concepts](../../../aztec/concepts/accounts/authwit.md#what-about-public).

Expand All @@ -177,13 +176,11 @@ In the snippet below, this is done as a separate contract call, but can also be

We have cases where we need a non-wallet contract to approve an action to be executed by another contract. One of the cases could be when making more complex defi where funds are passed along. When doing so, we need the intermediate contracts to support approving of actions on their behalf.

To support this, we must implement the `spend_public_authwit` function as seen in the snippet below.

#include_code authwit_uniswap_get /noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr rust
This is fairly straight forward to do using the `auth` library which include logic for updating values in the public auth registry. Namely, you can prepare the `message_hash` using `compute_call_authwit_hash` and then simply feed it into the `set_authorized` function (both are in `auth` library) to update the value.

It also needs a way to update those storage values. Since we want the updates to be trustless, we can compute the action based on the function inputs, and then have the contract compute the key at which it must add a `true` to approve the action.
When another contract later is consuming the authwit using `assert_current_call_valid_authwit_public` it will be calling the registry, and spend that authwit.

An example of this would be our Uniswap example which performs a cross chain swap on L1. In here, we both do private and public auth witnesses, where the public is set by the uniswap L2 contract itself. In the below snippet, you can see that we compute the action hash, and then update an `approved_action` mapping with the hash as key and `true` as value. When we then call the `token_bridge` to execute afterwards, it reads this value, burns the tokens, and consumes the authentication.
An example of this would be our Uniswap example which performs a cross chain swap on L1. In here, we both do private and public auth witnesses, where the public is set by the uniswap L2 contract itself. In the below snippet, you can see that we compute the action hash and update the value in the registry. When we then call the `token_bridge` to execute afterwards, it reads this value, burns the tokens, and consumes the authentication.

#include_code authwit_uniswap_set /noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr rust

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ Aztec is in full-speed development. Literally every version breaks compatibility

The `limit` parameter in `NoteGetterOptions` and `NoteViewerOptions` is now required to be a compile-time constant. This allows performing loops over this value, which leads to reduced circuit gate counts when setting a `limit` value.

### [Aztec.nr] canonical public authwit registry

The public authwits are moved into a shared registry (auth registry) to make it easier for sequencers to approve for their non-revertible (setup phase) whitelist. Previously, it was possible to DOS a sequencer by having a very expensive authwit validation that fails at the end, now the whitelist simply need the registry.

Notable, this means that consuming a public authwit will no longer emit a nullifier in the account contract but instead update STORAGE in the public domain. This means that there is a larger difference between private and public again. However, it also means that if contracts need to approve, and use the approval in the same tx, it is transient and don't need to go to DA (saving 96 bytes).

For the typescript wallets this is handled so the APIs don't change, but account contracts should get rid of their current setup with `approved_actions`.

```diff
- let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
+ let actions = AccountActions::init(&mut context, is_valid_impl);
```

For contracts we have added a `set_authorized` function in the auth library that can be used to set values in the registry.

```diff
- storage.approved_action.at(message_hash).write(true);
+ set_authorized(&mut context, message_hash, true);
```

### [Aztec.nr] emit encrypted logs

Emitting or broadcasting encrypted notes are no longer done as part of the note creation, but must explicitly be either emitted or discarded instead.
Expand Down
2 changes: 2 additions & 0 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ library Constants {
uint256 internal constant FIXED_DA_GAS = 512;
uint256 internal constant CANONICAL_KEY_REGISTRY_ADDRESS =
2153455745675440165069577621832684870696142028027528497509357256345838682961;
uint256 internal constant CANONICAL_AUTH_REGISTRY_ADDRESS =
18091885756106795278141309801070173692350235742979924147720536894670507925831;
uint256 internal constant DEPLOYER_CONTRACT_ADDRESS =
19511485909966796736993840362353440247573331327062358513665772226446629198132;
uint256 internal constant REGISTERER_CONTRACT_ADDRESS =
Expand Down
46 changes: 2 additions & 44 deletions noir-projects/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use dep::aztec::context::{PrivateContext, PublicContext};
use dep::aztec::state_vars::{Map, PublicMutable};
use dep::aztec::protocol_types::{address::AztecAddress, abis::function_selector::FunctionSelector, hash::pedersen_hash};

use crate::entrypoint::{app::AppPayload, fee::FeePayload};
Expand All @@ -8,26 +7,11 @@ use crate::auth::{IS_VALID_SELECTOR, compute_outer_authwit_hash};
struct AccountActions<Context> {
context: Context,
is_valid_impl: fn(&mut PrivateContext, Field) -> bool,
approved_action: Map<Field, PublicMutable<bool, Context>, Context>,
}

impl<Context> AccountActions<Context> {
pub fn init(
context: Context,
approved_action_storage_slot: Field,
is_valid_impl: fn(&mut PrivateContext, Field) -> bool
) -> Self {
AccountActions {
context,
is_valid_impl,
approved_action: Map::new(
context,
approved_action_storage_slot,
|context, slot| {
PublicMutable::new(context, slot)
}
)
}
pub fn init(context: Context, is_valid_impl: fn(&mut PrivateContext, Field) -> bool) -> Self {
AccountActions { context, is_valid_impl }
}
}

Expand Down Expand Up @@ -65,29 +49,3 @@ impl AccountActions<&mut PrivateContext> {
}
// docs:end:spend_private_authwit
}

impl AccountActions<&mut PublicContext> {
// docs:start:spend_public_authwit
pub fn spend_public_authwit(self, inner_hash: Field) -> Field {
// The `inner_hash` is "siloed" with the `msg_sender` to ensure that only it can
// consume the message.
// This ensures that contracts cannot consume messages that are not intended for them.
let message_hash = compute_outer_authwit_hash(
self.context.msg_sender(),
self.context.chain_id(),
self.context.version(),
inner_hash
);
let is_valid = self.approved_action.at(message_hash).read();
assert(is_valid == true, "Message not authorized by account");
self.context.push_new_nullifier(message_hash, 0);
IS_VALID_SELECTOR
}
// docs:end:spend_public_authwit

// docs:start:approve_public_authwit
pub fn approve_public_authwit(self, message_hash: Field) {
self.approved_action.at(message_hash).write(true);
}
// docs:end:approve_public_authwit
}
40 changes: 35 additions & 5 deletions noir-projects/aztec-nr/authwit/src/auth.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use dep::aztec::protocol_types::{
abis::function_selector::FunctionSelector, address::AztecAddress,
constants::{GENERATOR_INDEX__AUTHWIT_INNER, GENERATOR_INDEX__AUTHWIT_OUTER}, hash::pedersen_hash
constants::{GENERATOR_INDEX__AUTHWIT_INNER, GENERATOR_INDEX__AUTHWIT_OUTER, CANONICAL_AUTH_REGISTRY_ADDRESS},
hash::pedersen_hash
};
use dep::aztec::{prelude::Deserialize, context::{PrivateContext, PublicContext, gas::GasOpts}, hash::hash_args_array};

Expand All @@ -19,14 +20,14 @@ pub fn assert_current_call_valid_authwit(context: &mut PrivateContext, on_behalf
// docs:start:assert_current_call_valid_authwit_public
// Assert that `on_behalf_of` have authorized the current call in a public context
pub fn assert_current_call_valid_authwit_public(context: &mut PublicContext, on_behalf_of: AztecAddress) {
let function_selector = FunctionSelector::from_signature("spend_public_authwit(Field)");
let inner_hash = compute_inner_authwit_hash(
[(*context).msg_sender().to_field(), (*context).selector().to_field(), (*context).get_args_hash()]
);

let result: Field = context.call_public_function(
on_behalf_of,
function_selector,
[inner_hash].as_slice(),
AztecAddress::from_field(CANONICAL_AUTH_REGISTRY_ADDRESS),
FunctionSelector::from_signature("consume((Field),Field)"),
[on_behalf_of.to_field(), inner_hash].as_slice(),
GasOpts::default()
).deserialize_into();
assert(result == IS_VALID_SELECTOR, "Message not authorized by account");
Expand Down Expand Up @@ -69,3 +70,32 @@ pub fn compute_outer_authwit_hash(
GENERATOR_INDEX__AUTHWIT_OUTER
)
}

/**
* Helper function to set the authorization status of a message hash
*
* @param message_hash The hash of the message to authorize
* @param authorize True if the message should be authorized, false if it should be revoked
*/
pub fn set_authorized(context: &mut PublicContext, message_hash: Field, authorize: bool) {
context.call_public_function(
AztecAddress::from_field(CANONICAL_AUTH_REGISTRY_ADDRESS),
FunctionSelector::from_signature("set_authorized(Field,bool)"),
[message_hash, authorize as Field].as_slice(),
GasOpts::default()
).assert_empty();
}

/**
* Helper function to reject all authwits
*
* @param reject True if all authwits should be rejected, false otherwise
*/
pub fn set_reject_all(context: &mut PublicContext, reject: bool) {
context.call_public_function(
AztecAddress::from_field(CANONICAL_AUTH_REGISTRY_ADDRESS),
FunctionSelector::from_signature("set_reject_all(bool)"),
[context.this_address().to_field(), reject as Field].as_slice(),
GasOpts::default()
).assert_empty();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl UnconstrainedContext {
self.block_number
}

fn contract_address(self) -> AztecAddress {
fn this_address(self) -> AztecAddress {
self.contract_address
}
}
Expand Down
1 change: 1 addition & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"contracts/app_subscription_contract",
"contracts/auth_contract",
"contracts/auth_registry_contract",
"contracts/avm_initializer_test_contract",
"contracts/avm_test_contract",
"contracts/fpc_contract",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract AppSubscription {
encrypted_logs::encrypted_note_emission::encode_and_encrypt,
protocol_types::{traits::is_empty, grumpkin_point::GrumpkinPoint}
},
authwit::{account::AccountActions, auth_witness::get_auth_witness, auth::assert_current_call_valid_authwit},
authwit::{auth_witness::get_auth_witness, auth::assert_current_call_valid_authwit},
gas_token::GasToken, token::Token
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "auth_registry_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
authwit = { path = "../../../aztec-nr/authwit" }
Loading

0 comments on commit cea0b3b

Please sign in to comment.