Skip to content

Commit 3746717

Browse files
committed
refactor(authwit): split into common/private/public
1 parent 05683f8 commit 3746717

23 files changed

Lines changed: 462 additions & 514 deletions

File tree

docs/docs-developers/docs/aztec-nr/framework-description/authentication_witnesses.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `aztec` library includes authwit functionality. Import the necessary compone
2121

2222
```rust
2323
use aztec::{
24-
authwit::auth::{compute_authwit_message_hash_from_call, set_authorized},
24+
authwit::{common::compute_authwit_message_hash_from_call, public::set_authorized},
2525
macros::functions::authorize_once,
2626
};
2727
```

docs/examples/contracts/example_uniswap/src/main.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use aztec::macros::aztec;
99
#[aztec]
1010
pub contract ExampleUniswap {
1111
use aztec::{
12-
authwit::auth::{
13-
assert_current_call_valid_authwit_public, compute_authwit_message_hash_from_call,
14-
set_authorized,
12+
authwit::{
13+
common::compute_authwit_message_hash_from_call,
14+
public::{assert_current_call_valid_authwit_public, set_authorized},
1515
},
1616
macros::{functions::{external, initializer, only_self}, storage::storage},
1717
protocol::{

noir-projects/aztec-nr/aztec/src/authwit/account.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::context::PrivateContext;
22

33
use crate::protocol::{constants::DOM_SEP__TX_NULLIFIER, hash::poseidon2_hash_with_separator, traits::Hash};
44

5-
use crate::authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR};
5+
use crate::authwit::common::{compute_authwit_message_hash, IS_VALID_SELECTOR};
66
use crate::authwit::entrypoint::app::AppPayload;
77

88
pub struct AccountActions<Context> {

noir-projects/aztec-nr/aztec/src/authwit/auth.nr

Lines changed: 0 additions & 423 deletions
This file was deleted.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use crate::{
2+
authwit::{authorization_interface::AuthorizationInterface, AuthorizationSelector},
3+
hash::hash_args,
4+
macros::authorization::authorization,
5+
oracle::{execution_cache::load, offchain_effect::emit_offchain_effect},
6+
};
7+
use crate::protocol::{
8+
abis::function_selector::FunctionSelector,
9+
address::AztecAddress,
10+
constants::{DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER},
11+
hash::poseidon2_hash_with_separator,
12+
traits::{Serialize, ToField},
13+
};
14+
15+
/// Authentication witness helper library
16+
///
17+
/// Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (e.g.
18+
/// protocols or other users) to execute an action on their behalf.
19+
///
20+
/// This library provides helper functions to manage such witnesses. The authentication witness, is some "witness"
21+
/// (data) that authenticates a `message_hash`. The simplest example of an authentication witness, is a signature. The
22+
/// signature is the "evidence", that the signer has seen the message, agrees with it, and has allowed it. It does not
23+
/// need to be a signature. It could be any kind of "proof" that the message is allowed. Another proof could be knowing
24+
/// some kind of secret, or having some kind of "token" that allows the message.
25+
///
26+
/// The `message_hash` is a hash of the following structure: hash(consumer, chain_id, version, inner_hash)
27+
/// - consumer: the address of the contract that is "consuming" the message,
28+
/// - chain_id: the chain id of the chain that the message is being consumed on,
29+
/// - version: the version of the chain that the message is being consumed on,
30+
/// - inner_hash: the hash of the "inner" message that is being consumed, this is the "actual" message or action.
31+
///
32+
/// While the `inner_hash` could be anything, such as showing you signed a specific message, it will often be a hash of
33+
/// the "action" to approve, along with who made the call. As part of this library, we provide a few helper functions
34+
/// to deal with such messages.
35+
///
36+
/// For example, we provide helper function that is used for checking that the message is an encoding of the current
37+
/// call. This can be used to let some contract "allow" another contract to act on its behalf, as long as it can show
38+
/// that it is acting on behalf of the contract.
39+
///
40+
/// If we take a case of allowing a contract to transfer tokens on behalf of an account, the `inner_hash` can be
41+
/// derived as: inner_hash = hash(caller, "transfer", hash(to, amount))
42+
///
43+
/// Where the `caller` would be the address of the contract that is trying to transfer the tokens, and `to` and
44+
/// `amount` the arguments for the transfer.
45+
///
46+
/// Note that we have both a `caller` and a `consumer`, the `consumer` will be the contract that is consuming the
47+
/// message, in the case of the transfer, it would be the `Token` contract itself, while the caller, will be the actor
48+
/// that is allowed to transfer the tokens.
49+
///
50+
/// The authentication mechanism works differently in public and private contexts. In private, we recall that
51+
/// everything is executed on the user's device, so we can use `oracles` to "ask" the user (not contract) for
52+
/// information. In public we cannot do this, since it is executed by the sequencer (someone else). Therefore we can
53+
/// instead use a "registry" to store the messages that we have approved. See `authwit::private` and `authwit::public`
54+
/// for the corresponding flow-specific helpers; the symbols defined here are address-independent crypto utilities
55+
/// shared by both paths.
56+
///
57+
/// --- FAQ ---
58+
/// Q: Why are we using a success flag of `poseidon2_hash_bytes("IS_VALID()")` instead of just returning a boolean?
59+
/// A: We want to make sure that we don't accidentally return `true` if there is a collision in the function
60+
/// selector. By returning a hash of `IS_VALID()`, it becomes very unlikely that there is both a collision and we
61+
/// return a success flag.
62+
///
63+
/// Q: Why are we using static calls?
64+
/// A: We are using static calls to ensure that the account contract cannot re-enter. If it was a normal call, it
65+
/// could make a new call and do a re-entry attack. Using a static ensures that it cannot update any state.
66+
///
67+
/// Q: Would it not be cheaper to use a nullifier instead of updating state in public?
68+
/// A: At a quick glance, a public state update + nullifier is 96 bytes, but two state updates are 128, so it would
69+
/// be cheaper to use a nullifier, if this is the way it would always be done. However, if both the approval and the
70+
/// consumption is done in the same transaction, then we will be able to squash the updates (only final tx state diff
71+
/// is posted to DA), and now it is cheaper.
72+
///
73+
/// Q: Why is the chain id and the version part of the message hash?
74+
/// A: The chain id and the version is part of the message hash to ensure that the message is only valid on a
75+
/// specific chain to avoid a case where the same message could be used across multiple chains.
76+
77+
pub global IS_VALID_SELECTOR: Field = 0x47dacd73; // 4 last bytes of
78+
// poseidon2_hash_bytes("IS_VALID()")
79+
80+
/// A struct that represents a contract call the user can authorize. It's associated identifier is generated by
81+
/// serializing and hashing it. The user is expected to sign this hash to signal the contract call can be performed on
82+
/// their behalf
83+
#[authorization]
84+
pub struct CallAuthorization {
85+
pub msg_sender: AztecAddress,
86+
pub selector: FunctionSelector,
87+
pub args_hash: Field,
88+
}
89+
90+
/// A struct that represents a request to authorize a call, which is used to emit an offchain effect so the user/wallet
91+
/// can understand what they are being asked to sign. It is generated from a CallAuthorization by adding metadata to
92+
/// it, such as the selector for the authorization, the inner hash, and the actual arguments that are being passed to
93+
/// the function call.
94+
#[derive(Serialize)]
95+
pub struct CallAuthorizationRequest {
96+
pub selector: AuthorizationSelector,
97+
pub inner_hash: Field,
98+
pub on_behalf_of: AztecAddress,
99+
pub msg_sender: AztecAddress,
100+
pub fn_selector: FunctionSelector,
101+
pub args_hash: Field,
102+
}
103+
104+
pub(crate) unconstrained fn emit_authorization_as_offchain_effect<let N: u32>(
105+
authorization: CallAuthorization,
106+
inner_hash: Field,
107+
on_behalf_of: AztecAddress,
108+
) {
109+
let args: [Field; N] = load(authorization.args_hash);
110+
let authorization_request = CallAuthorizationRequest {
111+
selector: authorization.get_authorization_selector(),
112+
inner_hash: inner_hash,
113+
on_behalf_of: on_behalf_of,
114+
msg_sender: authorization.msg_sender,
115+
fn_selector: authorization.selector,
116+
args_hash: authorization.args_hash,
117+
};
118+
emit_offchain_effect(authorization_request.serialize().concat(args))
119+
}
120+
121+
/// Compute the `message_hash` from a function call to be used by an authentication witness
122+
///
123+
/// Useful for when you need a non-account contract to approve during execution. For example if you need a contract to
124+
/// make a call to nested contract, e.g., contract A wants to exit token T to L1 using bridge B, so it needs to allow B
125+
/// to transfer T on its behalf.
126+
///
127+
/// @param caller The address of the contract that is calling the function, in the example above, this would be B
128+
/// @param consumer The address of the contract that is consuming the message, in the example above, this would be T
129+
/// @param chain_id The chain id of the chain that the message is being consumed on @param version The version of the
130+
/// chain that the message is being consumed on @param selector The function selector of the function that is being
131+
/// called @param args The arguments of the function that is being called
132+
pub fn compute_authwit_message_hash_from_call<let N: u32>(
133+
caller: AztecAddress,
134+
consumer: AztecAddress,
135+
chain_id: Field,
136+
version: Field,
137+
selector: FunctionSelector,
138+
args: [Field; N],
139+
) -> Field {
140+
let args_hash = hash_args(args);
141+
let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]);
142+
compute_authwit_message_hash(consumer, chain_id, version, inner_hash)
143+
}
144+
145+
/// Computes the `inner_hash` of the authentication witness
146+
///
147+
/// This is used internally, but also useful in cases where you want to compute the `inner_hash` for a specific message
148+
/// that is not necessarily a call, but just some "bytes" or text.
149+
///
150+
/// @param args The arguments to hash
151+
pub fn compute_inner_authwit_hash<let N: u32>(args: [Field; N]) -> Field {
152+
poseidon2_hash_with_separator(args, DOM_SEP__AUTHWIT_INNER)
153+
}
154+
155+
/// Computes the `authwit_nullifier` for a specific `on_behalf_of` and `inner_hash`
156+
///
157+
/// Using the `on_behalf_of` and the `inner_hash` to ensure that the nullifier is siloed for a specific `on_behalf_of`.
158+
///
159+
/// @param on_behalf_of The address that has authorized the `inner_hash` @param inner_hash The hash of the message to
160+
/// authorize
161+
pub fn compute_authwit_nullifier(on_behalf_of: AztecAddress, inner_hash: Field) -> Field {
162+
poseidon2_hash_with_separator(
163+
[on_behalf_of.to_field(), inner_hash],
164+
DOM_SEP__AUTHWIT_NULLIFIER,
165+
)
166+
}
167+
168+
/// Computes the `message_hash` for the authentication witness
169+
///
170+
/// @param consumer The address of the contract that is consuming the message @param chain_id The chain id of the chain
171+
/// that the message is being consumed on @param version The version of the chain that the message is being consumed on
172+
/// @param inner_hash The hash of the "inner" message that is being consumed
173+
pub fn compute_authwit_message_hash(
174+
consumer: AztecAddress,
175+
chain_id: Field,
176+
version: Field,
177+
inner_hash: Field,
178+
) -> Field {
179+
poseidon2_hash_with_separator(
180+
[consumer.to_field(), chain_id, version, inner_hash],
181+
DOM_SEP__AUTHWIT_OUTER,
182+
)
183+
}

noir-projects/aztec-nr/aztec/src/authwit/mod.nr

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ pub mod account;
44
pub mod authorization_interface;
55
mod authorization_selector;
66
pub use authorization_selector::AuthorizationSelector;
7-
pub mod auth;
7+
pub mod common;
8+
pub mod private;
9+
pub mod public;
810
pub mod entrypoint;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use crate::{
2+
authwit::common::{
3+
CallAuthorization, compute_authwit_nullifier, compute_inner_authwit_hash, emit_authorization_as_offchain_effect,
4+
IS_VALID_SELECTOR,
5+
},
6+
context::PrivateContext,
7+
};
8+
use crate::protocol::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize};
9+
10+
/// Private-flow authwit helpers.
11+
///
12+
/// Say that a user `Alice` wants to deposit some tokens into a DeFi protocol (say a DEX). `Alice` would make a
13+
/// `deposit` transaction, that she is executing using her account contract. The account would call the `DeFi`
14+
/// contract to execute `deposit`, which would try to pull funds from the `Token` contract. Since the `DeFi` contract
15+
/// is trying to pull funds from an account that is not its own, it needs to convince the `Token` contract that it is
16+
/// allowed to do so.
17+
///
18+
/// This is where the authentication witness comes in. The `Token` contract computes a `message_hash` from the
19+
/// `transfer` call, and then asks `Alice Account` contract to verify that the `DeFi` contract is allowed to execute
20+
/// that call.
21+
///
22+
/// `Alice Account` contract can then ask `Alice` if she wants to allow the `DeFi` contract to pull funds from her
23+
/// account. If she does, she will sign the `message_hash` and return the signature to the `Alice Account` which will
24+
/// validate it and return success to the `Token` contract which will then allow the `DeFi` contract to pull funds
25+
/// from `Alice`.
26+
///
27+
/// To ensure that the same "approval" cannot be used multiple times, we also compute a `nullifier` for the
28+
/// authentication witness, and emit it from the `Token` contract (consumer).
29+
///
30+
/// Note that we can do this flow as we are in private where we can do oracle calls out from contracts.
31+
///
32+
/// Person Contract Contract Contract
33+
/// Alice Alice Account Token DeFi
34+
/// | | | |
35+
/// | Defi.deposit(Token, 1000) | |
36+
/// |----------------->| | |
37+
/// | | deposit(Token, 1000) |
38+
/// | |---------------------------------------->|
39+
/// | | | |
40+
/// | | | transfer(Alice, Defi, 1000)
41+
/// | | |<---------------------|
42+
/// | | | |
43+
/// | | Check if Defi may call transfer(Alice, Defi, 1000)
44+
/// | |<-----------------| |
45+
/// | | | |
46+
/// | Please give me AuthWit for DeFi | |
47+
/// | calling transfer(Alice, Defi, 1000) | |
48+
/// |<-----------------| | |
49+
/// | | | |
50+
/// | | | |
51+
/// | AuthWit for transfer(Alice, Defi, 1000) |
52+
/// |----------------->| | |
53+
/// | | AuthWit validity | |
54+
/// | |----------------->| |
55+
/// | | | |
56+
/// | | throw if invalid AuthWit |
57+
/// | | | |
58+
/// | | emit AuthWit nullifier |
59+
/// | | | |
60+
/// | | transfer(Alice, Defi, 1000) |
61+
/// | | | |
62+
/// | | | |
63+
/// | | | success |
64+
/// | | |--------------------->|
65+
/// | | | |
66+
/// | | | |
67+
/// | | | deposit(Token, 1000)
68+
/// | | | |
69+
70+
/// Assert that `on_behalf_of` has authorized the current call with a valid authentication witness
71+
///
72+
/// Compute the `inner_hash` using the `msg_sender`, `selector` and `args_hash` and then make a call out to the
73+
/// `on_behalf_of` contract to verify that the `inner_hash` is valid.
74+
///
75+
/// Additionally, this function emits the identifying information of the call as an offchain effect so PXE can rely the
76+
/// information to the user/wallet in a readable way. To that effect, it is generic over N, where N is the number of
77+
/// arguments the authorized functions takes. This is used to load the arguments from the execution cache. This
78+
/// function is intended to be called via a macro, which will use the turbofish operator to specify the number of
79+
/// arguments.
80+
///
81+
/// @param on_behalf_of The address that has allegedly authorized the current call
82+
pub fn assert_current_call_valid_authwit<let N: u32>(context: &mut PrivateContext, on_behalf_of: AztecAddress) {
83+
let args_hash: Field = context.get_args_hash();
84+
85+
let authorization =
86+
CallAuthorization { msg_sender: context.maybe_msg_sender().unwrap(), selector: context.selector(), args_hash };
87+
let inner_hash = compute_inner_authwit_hash(authorization.serialize());
88+
// Safety: Offchain effects are by definition unconstrained. They are emitted via an oracle which we don't use for
89+
// anything besides its side effects, therefore this is safe to call.
90+
unsafe { emit_authorization_as_offchain_effect::<N>(authorization, inner_hash, on_behalf_of) };
91+
92+
assert_inner_hash_valid_authwit(context, on_behalf_of, inner_hash);
93+
}
94+
95+
/// Assert that a specific `inner_hash` is valid for the `on_behalf_of` address
96+
///
97+
/// Used as an internal function for `assert_current_call_valid_authwit` and can be used as a standalone function when
98+
/// the `inner_hash` is from a different source, e.g., say a block of text etc.
99+
///
100+
/// @param on_behalf_of The address that has allegedly authorized the current call @param inner_hash The hash of the
101+
/// message to authorize
102+
pub fn assert_inner_hash_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress, inner_hash: Field) {
103+
// We perform a static call here and not a standard one to ensure that the account contract cannot re-enter.
104+
let result: Field = context
105+
.static_call_private_function(
106+
on_behalf_of,
107+
comptime { FunctionSelector::from_signature("verify_private_authwit(Field)") },
108+
[inner_hash],
109+
)
110+
.get_preimage();
111+
assert(result == IS_VALID_SELECTOR, "Message not authorized by account");
112+
// Compute the nullifier, similar computation to the outer hash, but without the chain_id and version. Those should
113+
// already be handled in the verification, so we just need something to nullify, that allows the same inner_hash
114+
// for multiple actors.
115+
let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
116+
context.push_nullifier(nullifier);
117+
}

0 commit comments

Comments
 (0)