Skip to content

Commit

Permalink
feat: purge account storage
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind committed Jun 12, 2024
1 parent fc6fa68 commit 47088cf
Show file tree
Hide file tree
Showing 15 changed files with 107 additions and 195 deletions.
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 introduction of an auth registry where all the authwit responses sits were made to make the job easier for sequences in relation to avoid being dossed. Essentially, with the registry, it is not required to execute code on the account contract itself in most public cases!

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 API's 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
49 changes: 2 additions & 47 deletions noir-projects/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
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};
use crate::auth::{IS_VALID_SELECTOR, compute_outer_authwit_hash};

// @todo Remove the `approved_action`
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 @@ -66,31 +49,3 @@ impl AccountActions<&mut PrivateContext> {
}
// docs:end:spend_private_authwit
}


// @todo We can entirely remove this one when the other is working.
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
}
29 changes: 29 additions & 0 deletions noir-projects/aztec-nr/authwit/src/auth.nr
Original file line number Diff line number Diff line change
Expand Up @@ -70,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 @@ -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
Expand Up @@ -21,8 +21,6 @@ contract EcdsaAccount {
public_key: PrivateImmutable<EcdsaPublicKeyNote>,
}

global ACCOUNT_ACTIONS_STORAGE_SLOT = 2;

// Creates a new account out of an ECDSA public key to use for signature verification
#[aztec(private)]
#[aztec(initializer)]
Expand All @@ -41,14 +39,14 @@ contract EcdsaAccount {
// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
}

#[aztec(private)]
#[aztec(noinitcheck)]
fn spend_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.spend_private_authwit(inner_hash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ contract SchnorrAccount {
use dep::std;

use dep::aztec::prelude::{AztecAddress, FunctionSelector, NoteHeader, PrivateContext, PrivateImmutable};
use dep::aztec::state_vars::{Map, PublicMutable};
use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt;
use dep::authwit::{
entrypoint::{app::AppPayload, fee::FeePayload}, account::AccountActions,
Expand All @@ -22,7 +21,6 @@ contract SchnorrAccount {
// docs:start:storage
signing_public_key: PrivateImmutable<PublicKeyNote>,
// docs:end:storage
approved_actions: Map<Field, PublicMutable<bool>>,
}

// Constructs the contract
Expand All @@ -46,22 +44,14 @@ contract SchnorrAccount {
#[aztec(private)]
#[aztec(noinitcheck)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
let actions = AccountActions::init(
&mut context,
storage.approved_actions.storage_slot,
is_valid_impl
);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
}

#[aztec(private)]
#[aztec(noinitcheck)]
fn spend_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::init(
&mut context,
storage.approved_actions.storage_slot,
is_valid_impl
);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.spend_private_authwit(inner_hash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@ contract SchnorrHardcodedAccount {
global public_key_x: Field = 0x0ede3d33c920df8fdf43f3e39ed38b0882c25b056620ef52fd016fe811aa2443;
global public_key_y: Field = 0x29155934ffaa105323695b5f91faadd84acc21f4a8bda2fad760f992d692bc7f;

global ACCOUNT_ACTIONS_STORAGE_SLOT = 1;

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
}

#[aztec(private)]
fn spend_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.spend_private_authwit(inner_hash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@ contract SchnorrSingleKeyAccount {

use crate::{util::recover_address, auth_oracle::get_auth_witness};

global ACCOUNT_ACTIONS_STORAGE_SLOT = 1;

// Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts
#[aztec(private)]
fn entrypoint(app_payload: AppPayload, fee_payload: FeePayload) {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.entrypoint(app_payload, fee_payload);
}

#[aztec(private)]
fn spend_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
let actions = AccountActions::init(&mut context, is_valid_impl);
actions.spend_private_authwit(inner_hash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ contract Uniswap {

use dep::authwit::auth::{
IS_VALID_SELECTOR, assert_current_call_valid_authwit_public, compute_call_authwit_hash,
compute_outer_authwit_hash
compute_outer_authwit_hash, set_authorized
};

use dep::token::Token;
Expand All @@ -19,11 +19,6 @@ contract Uniswap {

#[aztec(storage)]
struct Storage {
// like with account contracts, stores the approval message on a slot and tracks if they are active
approved_action: Map<Field, PublicMutable<bool>>,
// tracks the nonce used to create the approval message for burning funds
// gets incremented each time after use to prevent replay attacks
nonce_for_burn_approval: PublicMutable<Field>,
portal_address: SharedImmutable<EthAddress>,
}
// docs:end:uniswap_setup
Expand Down Expand Up @@ -153,28 +148,6 @@ contract Uniswap {
}
// docs:end:swap_private

// docs:start:authwit_uniswap_get
// Since the token bridge burns funds on behalf of this contract, this contract has to tell the token contract if the signature is valid
// implementation is similar to how account contracts validate public approvals.
// if valid, it returns the IS_VALID selector which is expected by token contract
#[aztec(public)]
fn spend_public_authwit(inner_hash: Field) -> Field {
let message_hash = compute_outer_authwit_hash(
context.msg_sender(),
context.chain_id(),
context.version(),
inner_hash
);
let value = storage.approved_action.at(message_hash).read();
if (value) {
context.push_new_nullifier(message_hash, 0);
IS_VALID_SELECTOR
} else {
0
}
}
// docs:end:authwit_uniswap_get

// docs:start:authwit_uniswap_set
// This helper method approves the bridge to burn this contract's funds and exits the input asset to L1
// Assumes contract already has funds.
Expand All @@ -188,30 +161,26 @@ contract Uniswap {
token_bridge: AztecAddress,
amount: Field
) {
// approve bridge to burn this contract's funds (required when exiting on L1, as it burns funds on L2):
let nonce_for_burn_approval = storage.nonce_for_burn_approval.read();
// Since we will authorize and instantly spend the funds, all in public, we can use the same nonce
// every interaction. In practice, the authwit should be squashed, so this is also cheap!
let nonce = 0xdeadbeef;

let selector = FunctionSelector::from_signature("burn_public((Field),Field,Field)");
let message_hash = compute_call_authwit_hash(
token_bridge,
token,
context.chain_id(),
context.version(),
selector,
[context.this_address().to_field(), amount, nonce_for_burn_approval]
[context.this_address().to_field(), amount, nonce]
);
storage.approved_action.at(message_hash).write(true);

// increment nonce_for_burn_approval so it won't be used again
storage.nonce_for_burn_approval.write(nonce_for_burn_approval + 1);
// We need to make a call to update it.
set_authorized(&mut context, message_hash, true);

let this_portal_address = storage.portal_address.read_public();
// Exit to L1 Uniswap Portal !
TokenBridge::at(token_bridge).exit_to_l1_public(
this_portal_address,
amount,
this_portal_address,
nonce_for_burn_approval
).call(&mut context)
TokenBridge::at(token_bridge).exit_to_l1_public(this_portal_address, amount, this_portal_address, nonce).call(&mut context)
}
// docs:end:authwit_uniswap_set

Expand All @@ -224,12 +193,5 @@ contract Uniswap {
token.eq(TokenBridge::at(token_bridge).get_token().view(&mut context)), "input_asset address is not the same as seen in the bridge contract"
);
}

// /// Unconstrained ///

// this method exists solely for e2e tests to test that nonce gets incremented each time.
unconstrained fn nonce_for_burn_approval() -> pub Field {
storage.nonce_for_burn_approval.read()
}
// docs:end:assert_token_is_same
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ describe('e2e_blacklist_token_contract burn', () => {

tokenSim.burnPublic(wallets[0].getAddress(), amount);

// Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer.
const txReplay = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).send();
await expect(txReplay.wait()).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR);
await expect(
asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).simulate(),
).rejects.toThrow(/unauthorized/);
});

describe('failure cases', () => {
Expand All @@ -68,7 +68,7 @@ describe('e2e_blacklist_token_contract burn', () => {
const amount = balance0 - 1n;
expect(amount).toBeGreaterThan(0n);
const nonce = 1;
await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow(
await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).simulate()).rejects.toThrow(
'Assertion failed: invalid nonce',
);
});
Expand All @@ -78,8 +78,8 @@ describe('e2e_blacklist_token_contract burn', () => {
const amount = balance0 + 1n;
const nonce = Fr.random();
await expect(
asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(),
).rejects.toThrow('Assertion failed: Message not authorized by account');
asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).simulate(),
).rejects.toThrow(/unauthorized/);
});

it('burn more than balance on behalf of other', async () => {
Expand All @@ -106,8 +106,8 @@ describe('e2e_blacklist_token_contract burn', () => {
await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait();

await expect(
asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(),
).rejects.toThrow('Assertion failed: Message not authorized by account');
asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).simulate(),
).rejects.toThrow(/unauthorized/);
});

it('burn from blacklisted account', async () => {
Expand Down
Loading

0 comments on commit 47088cf

Please sign in to comment.