Summary
A description of how nonces are currently used in authwits, with a design pattern for app developers to take full leverage of them being present at the token transfer interface.
Comparisons
Considering that nonce
could be popped out within the token transfer(from,to,amt)
method, the main difference with adding it in the interface instead transfer(from,to,amt,**nonce**)
, the reason for this, is that we have the nonce
being passed from contract to contract, so that each one can perform a check on it, and provide another layer of security to the authorization witnesses the user signs.
Non-interface implementation
fn transfer(from: AztecAddress, to: AztecAddress, amount: U128) {
if (!from.eq(context.msg_sender())) {
let nonce: Field = unsafe { get_nonce_for_transfer() }
assert_current_call_valid_authwit(&mut context, from, nonce)
// ...
In-interface implementation
fn transfer(from: AztecAddress, to: AztecAddress, amount: U128, nonce: Field) {
if (!from.eq(context.msg_sender())) {
// NOTE: nonce is already part of the args_hash
assert_current_call_valid_authwit(&mut context, from)
// ...
Details
Authwits are an Aztec developed pattern in which an Account Contract may grant through the usage of signatures an extra step of security in the nullification of Notes, within the Token Contract (or other contracts that may have an “ownership” mechanism). We’ll use this document to outline the full lifespan of them and to propose improvements in their implementation.
#[private]
fn transfer_in_private(from: AztecAddress, to: AztecAddress, amount: U128, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
When the sender isn’t the owner of the Notes (within the Token Contract scope), a call to the sender needs to be performed such that he’ll approve the spending of them, by validating the following datapoints (accessible within the Token Contract context):
// token contract
#[contract_library_method]
pub fn assert_current_call_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress) {
let inner_hash = compute_inner_authwit_hash([
context.msg_sender().to_field(), // spender
context.selector().to_field(), // method being called (transfer_in_private)
context.args_hash, // call arguments (from, to, amount, nonce)
]);
assert_inner_hash_valid_authwit(context, on_behalf_of, inner_hash);
}
// account contract
#[private]
pub fn verify_private_authwit(self, inner_hash: Field) -> Field {
let message_hash = compute_authwit_message_hash(
self.context.msg_sender(), // authwit consumer (avoids replays in other tokens)
self.context.chain_id(), // chain-id (avoids replays in other chains)
self.context.version(), // verion (avoids replays in other version)
inner_hash, // H([spender, transfer_in_private, from, to, amount, nonce])
);
assert(is_valid_impl(context, message_hash), "Message not authorized by account");
IS_VALID_SELECTOR // return value (if assertion passes)
}
// schnorr account contract implementation
#[contract_library_method]
fn is_valid_impl(_context: &mut PrivateContext, outer_hash: Field) -> bool {
// Gets auth_witness from PXE
let witness = unsafe { get_auth_witness(outer_hash) };
// Verify signature using public key
schnorr::verify_signature(public_key, witness, outer_hash.to_be_bytes::<32>())
}
// ecdsa account contract implementation
#[contract_library_method]
fn is_valid_impl(context: &mut PrivateContext, outer_hash: Field) -> bool {
// Gets auth_witness from PXE
let witness = unsafe { get_auth_witness(outer_hash) };
// Verify payload signature using Ethereum's signing scheme
let hashed_message = std::hash::sha256(outer_hash.to_be_bytes());
std::ecdsa_secp256r1::verify_signature(
public_key.x,
public_key.y,
witness,
hashed_message,
)
}
Most of the information being validated comes from the context
object, or the storage public_key
within the account contract to validate that the witness
object injected from the PXE is indeed correct, through different signing mechanisms (Schnorr, ECDSA, etc).
The information that’s being validated is:
- Within the Token Contract (depends on the token implementation):
- spender: who pulls funds
- method being called: which method is the spender calling
- arguments hash: with which arguments is the method being called (this information is hashed, the preimage cannot be processed as to natively provide -i.e.- “expiration”)
- Within the Account Contract (depends on the account implementation):
- consumer: who is requesting the authorization witness (in this case, the token contract)
- chain id and version: defence against replaying intents in different versions of Aztec
- signature: each account contract uses a signing mechanism for authentication
Leveraging not-so-random nonces
The nonce
being inserted needs to be known in advance by the user, in order to provide a way for his account contract to return 0x47dacd73
(H(”IS_VALID()”)
last 4 bytes)to a verify_private_authwit(Field)
call.
Because of this, the nonce
needs to be passed around to the spender contract, that needs to input it on the transfer_in_private
method in the Token contract, to be hashed and passed to the Account Contract for authentication.
If we consider that the nonce
is a random number (within a finite field), that has to be passed within the call, such as:
- Action being performed: (i.e.) swap Token A for Token B (and not C)
- Caducity of the action: expire if timestamp > some specified
// amm contract
fn swap_exact_tokens_for_tokens(
token_in: AztecAddress,
token_out: AztecAddress,
amount_in: U128,
amount_out_min: U128,
nonce: Field,
) {
assert(
extract_bytes(nonce, offset: 0, length: 4) // method being called flag
== SWAP_EXACT_TOKENS_FOR_TOKENS // last 4 bytes of H(”SWAP_EXACT_TOKENS_FOR_TOKENS()”)
);
assert(
extract_bytes(nonce, offset: 4, length: 8) // caducity flag
<= context.get_block_number()
);
Implementation
- Add to the Field library a way of extracting (as bytes) data starting from an offset, with a length, in order to input the data into the circuits of the contracts that may wanna leverage this technology
- Add both in Noir and in JS a way of replacing bytes into a Field, so that the auth witness generation can use this not-so-random nonces for testing