Skip to content

Commit

Permalink
feat: optimizing contract with config pattern (#11756)
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan authored Feb 6, 2025
1 parent fe2b666 commit 7820cb7
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,15 @@ Note - you could also create a note and send it to the user. The problem is ther

### Reading public storage in private

You can't read public storage in private domain. But nevertheless reading public storage is desirable. There are two ways to achieve the desired effect:
You can read public storage in private domain by leveraging the private getters of `PublicImmutable` (for values that never change) and `SharedMutable` (for values that change infrequently, see [shared state](../../../../reference/smart_contract_reference/storage/shared_state.md) for details) state variables.
Values that change frequently (`PublicMutable`) cannot be read in private as for those we need access to the tip of the chain and only a sequencer has access to that (and sequencer executes only public functions).

1. For public values that change infrequently, you can use [shared state](../../../../reference/smart_contract_reference/storage/shared_state.md).

1. You pass the data as a parameter to your private method and later assert in public that the data is correct. E.g.:
E.g. when using `PublicImmutable`

```rust
#[storage]
struct Storage {
token: PublicMutable<Field>,
config: PublicImmutable<Config, Context>,
}

contract Bridge {
Expand All @@ -59,9 +58,8 @@ contract Bridge {
amount: Field,
) -> Field {
...
#include_code call_assert_token_is_same /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr raw
}
#include_code assert_token_is_same /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr raw
}
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ We also use a `_withCaller` parameter to determine the appropriate party that ca

We call this pattern _designed caller_ which enables a new paradigm **where we can construct other such portals that talk to the token portal and therefore create more seamless crosschain legos** between L1 and L2.

Before we can compile and use the contract, we need to add two additional functions.
Before we can compile and use the contract, we need to add 1 additional function.

We need a function that lets us read the token value. Paste this into `main.nr`:

#include_code get_token /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust
#include_code get_config /noir-projects/noir-contracts/contracts/token_bridge_contract/src/main.nr rust

## Compile code

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: L2 Contracts (Aztec)
sidebar_position: 1
---

This page goes over the code in the L2 contract for Uniswap, which works alongside a [token bridge (codealong tutorial)](../token_bridge/index.md).
This page goes over the code in the L2 contract for Uniswap, which works alongside a [token bridge (codealong tutorial)](../token_bridge/index.md).

## Main.nr

Expand Down Expand Up @@ -57,12 +57,6 @@ Both public and private swap functions call this function:

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

### Assertions

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

This is a simple function that asserts that the token passed in to the function is the one that the bridge is associated with.

## Utils

### Compute content hash for public
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ Aztec is in full-speed development. Literally every version breaks compatibility

### TBD

### Changes to `TokenBridge` interface

`get_token` and `get_portal_address` functions got merged into a single `get_config` function that returns a struct containing both the token and portal addresses.

### [Aztec.nr] Introduction of `WithHash<T>`

`WithHash<T>` is a struct that allows for efficient reading of value `T` from public storage in private.
This is achieved by storing the value with its hash, then obtaining the values via an oracle and verifying them against the hash.
This results in in a fewer tree inclusion proofs for values `T` that are packed into more than a single field.
Expand Down
19 changes: 19 additions & 0 deletions noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::Pack
///
/// This is valuable when T packs to multiple fields, as it maintains "almost constant" verification overhead
/// regardless of the original data size.
///
/// # Optimizing private reads in your contract
/// Given that reading T from public immutable in private has "almost constant" constraints cost for different sizes
/// of T it is recommended to group multiple values into a single struct when they are being read together. This can
/// typically be some kind of configuration set up during contract initialization. E.g.:
///
/// ```noir
/// use dep::aztec::protocol_types::{address::AztecAddress, traits::Packable};
/// use std::meta::derive;
///
/// #[derive(Eq, Packable)]
/// pub struct Config \{
/// pub address_1: AztecAddress,
/// pub value_1: U128,
/// pub value_2: u64,
/// ...
/// }
/// ```
///
// docs:start:public_immutable_struct
pub struct PublicImmutable<T, Context> {
context: Context,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Packable, Serialize}};
use std::meta::derive;

/// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single
/// We store the tokens of the pool in a struct such that to load it from PublicImmutable asserts only a single
/// merkle proof.
/// (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022).
#[derive(Deserialize, Eq, Packable, Serialize)]
pub struct Config {
pub token0: AztecAddress,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::Packable};
use std::meta::derive;

// PublicImmutable has constant read cost in private regardless of the size of what it stores, so we put all immutable
// values in a single struct
#[derive(Eq, Packable)]
pub struct Config {
pub target_address: AztecAddress,
pub subscription_token_address: AztecAddress,
pub subscription_recipient_address: AztecAddress,
pub subscription_price: U128,
pub fee_juice_limit_per_tx: Field,
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
mod config;
mod subscription_note;
mod dapp_payload;

use dep::aztec::macros::aztec;

#[aztec]
pub contract AppSubscription {
use crate::{dapp_payload::DAppPayload, subscription_note::SubscriptionNote};
use crate::{config::Config, dapp_payload::DAppPayload, subscription_note::SubscriptionNote};

use authwit::auth::assert_current_call_valid_authwit;
use aztec::{
Expand All @@ -18,15 +19,10 @@ pub contract AppSubscription {
use router::utils::privately_check_block_number;
use token::Token;

// TODO: This can be optimized by storing the values in Config struct in 1 PublicImmutable (less merkle proofs).
#[storage]
struct Storage<Context> {
target_address: PublicImmutable<AztecAddress, Context>,
subscription_token_address: PublicImmutable<AztecAddress, Context>,
subscription_recipient_address: PublicImmutable<AztecAddress, Context>,
subscription_price: PublicImmutable<U128, Context>,
config: PublicImmutable<Config, Context>,
subscriptions: Map<AztecAddress, PrivateMutable<SubscriptionNote, Context>, Context>,
fee_juice_limit_per_tx: PublicImmutable<Field, Context>,
}

global SUBSCRIPTION_DURATION_IN_BLOCKS: Field = 5;
Expand All @@ -51,16 +47,18 @@ pub contract AppSubscription {

context.set_as_fee_payer();

let config = storage.config.read();

// TODO(palla/gas) Assert fee_juice_limit_per_tx is less than this tx gas_limit
let _gas_limit = storage.fee_juice_limit_per_tx.read();
let _gas_limit = config.fee_juice_limit_per_tx;

context.end_setup();

// We check that the note is not expired. We do that via the router contract to conceal which contract
// is performing the check.
privately_check_block_number(Comparator.LT, note.expiry_block_number, &mut context);

payload.execute_calls(&mut context, storage.target_address.read());
payload.execute_calls(&mut context, config.target_address);
}

#[public]
Expand All @@ -72,11 +70,15 @@ pub contract AppSubscription {
subscription_price: U128,
fee_juice_limit_per_tx: Field,
) {
storage.target_address.initialize(target_address);
storage.subscription_token_address.initialize(subscription_token_address);
storage.subscription_recipient_address.initialize(subscription_recipient_address);
storage.subscription_price.initialize(subscription_price);
storage.fee_juice_limit_per_tx.initialize(fee_juice_limit_per_tx);
storage.config.initialize(
Config {
target_address,
subscription_recipient_address,
subscription_token_address,
subscription_price,
fee_juice_limit_per_tx,
},
);
}

#[private]
Expand All @@ -88,11 +90,13 @@ pub contract AppSubscription {
) {
assert(tx_count as u64 <= SUBSCRIPTION_TXS as u64);

Token::at(storage.subscription_token_address.read())
let config = storage.config.read();

Token::at(config.subscription_token_address)
.transfer_in_private(
context.msg_sender(),
storage.subscription_recipient_address.read(),
storage.subscription_price.read(),
config.subscription_recipient_address,
config.subscription_price,
nonce,
)
.call(&mut context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::Packable};
use std::meta::derive;

// PublicImmutable has constant read cost in private regardless of the size of what it stores, so we put all immutable
// values in a single struct
#[derive(Eq, Packable)]
pub struct Config {
pub donation_token: AztecAddress, // Token used for donations (e.g. DAI)
pub operator: AztecAddress, // Crowdfunding campaign operator
pub deadline: u64, // End of the crowdfunding campaign after which no more donations are accepted
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
mod config;

// docs:start:empty-contract
use dep::aztec::macros::aztec;

#[aztec]
pub contract Crowdfunding {
// docs:end:empty-contract
use crate::config::Config;

// docs:start:all-deps
use dep::aztec::{
Expand Down Expand Up @@ -31,16 +34,10 @@ pub contract Crowdfunding {
amount: U128,
}

// TODO: This can be optimized by storing the values in Config struct in 1 PublicImmutable (less merkle proofs).
// docs:start:storage
#[storage]
struct Storage<Context> {
// Token used for donations (e.g. DAI)
donation_token: PublicImmutable<AztecAddress, Context>,
// Crowdfunding campaign operator
operator: PublicImmutable<AztecAddress, Context>,
// End of the crowdfunding campaign after which no more donations are accepted
deadline: PublicImmutable<u64, Context>,
config: PublicImmutable<Config, Context>,
// Notes emitted to donors when they donate (can be used as proof to obtain rewards, eg in Claim contracts)
donation_receipts: PrivateSet<UintNote, Context>,
}
Expand All @@ -56,28 +53,29 @@ pub contract Crowdfunding {
fn init(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
// docs:end:init-header
// docs:end:init-header-error
storage.donation_token.initialize(donation_token);
storage.operator.initialize(operator);
storage.deadline.initialize(deadline);
storage.config.initialize(Config { donation_token, operator, deadline });
}
// docs:end:init

// docs:start:donate
#[private]
fn donate(amount: U128) {
let config = storage.config.read();

// 1) Check that the deadline has not passed --> we do that via the router contract to conceal which contract
// is performing the check.
// docs:start:call-check-deadline
let deadline = storage.deadline.read();
privately_check_timestamp(Comparator.LT, deadline, &mut context);
privately_check_timestamp(Comparator.LT, config.deadline, &mut context);
// docs:end:call-check-deadline

// docs:start:do-transfer
// 2) Transfer the donation tokens from donor to this contract
let donor = context.msg_sender();
Token::at(storage.donation_token.read())
Token::at(config.donation_token)
.transfer_in_private(donor, context.this_address(), amount, 0)
.call(&mut context);
// docs:end:do-transfer

// 3) Create a value note for the donor so that he can later on claim a rewards token in the Claim
// contract by proving that the hash of this note exists in the note hash tree.
let mut note = UintNote::new(amount, donor);
Expand All @@ -94,14 +92,14 @@ pub contract Crowdfunding {
// Withdraws balance to the operator. Requires that msg_sender() is the operator.
#[private]
fn withdraw(amount: U128) {
let config = storage.config.read();
let operator_address = config.operator;

// 1) Check that msg_sender() is the operator
let operator_address = storage.operator.read();
assert(context.msg_sender() == operator_address, "Not an operator");

// 2) Transfer the donation tokens from this contract to the operator
Token::at(storage.donation_token.read()).transfer(operator_address, amount).call(
&mut context,
);
Token::at(config.donation_token).transfer(operator_address, amount).call(&mut context);
// 3) Emit a public event so that anyone can audit how much the operator has withdrawn
Crowdfunding::at(context.this_address())
._publish_donation_receipts(amount, operator_address)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Packable, Serialize}};
use std::meta::derive;

/// We store the addresses in a struct such that to load it from PublicImmutable asserts only a single
/// merkle proof.
#[derive(Deserialize, Eq, Packable, Serialize)]
pub struct Config {
pub accepted_asset: AztecAddress, // Asset the FPC accepts (denoted as AA below)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use dep::aztec::protocol_types::{
address::{AztecAddress, EthAddress},
traits::{Deserialize, Packable, Serialize},
};
use std::meta::derive;

// PublicImmutable has constant read cost in private regardless of the size of what it stores, so we put all immutable
// values in a single struct
#[derive(Deserialize, Eq, Packable, Serialize)]
pub struct Config {
pub token: AztecAddress,
pub portal: EthAddress,
}
Loading

0 comments on commit 7820cb7

Please sign in to comment.