diff --git a/docs/docs-developers/docs/aztec-nr/debugging.md b/docs/docs-developers/docs/aztec-nr/debugging.md index 2ee121f5af81..703e9d8138d0 100644 --- a/docs/docs-developers/docs/aztec-nr/debugging.md +++ b/docs/docs-developers/docs/aztec-nr/debugging.md @@ -33,7 +33,7 @@ Log values from your contract using `debug_log`: ```rust // Import debug logging -use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format, debug_log_field, debug_log_array }; +use dep::aztec::oracle::debug_log::{ debug_log, debug_log_format }; // Log simple messages debug_log("checkpoint reached"); @@ -41,11 +41,11 @@ debug_log("checkpoint reached"); // Log field values with context debug_log_format("slot:{0}, hash:{1}", [storage_slot, note_hash]); -// Log single field -debug_log_field(my_field); +// Log a single value +debug_log_format("my_field: {0}", [my_field]); -// Log arrays -debug_log_array(my_array); +// Log multiple values +debug_log_format("values: {0}, {1}, {2}", [val1, val2, val3]); ``` :::note @@ -230,5 +230,5 @@ Check hex errors against [Errors.sol](https://github.com/AztecProtocol/aztec-pac ## Next steps - [Circuit Architecture](../foundational-topics/advanced/circuits/index.md) -- [Private-Public Execution](./framework-description/functions/public_private_calls.md) +- [Call Types](../foundational-topics/call_types.md) - [Aztec.nr Dependencies](./framework-description/dependencies.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md index 389051fdf68d..ec1f0eec7a55 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_profile_transactions.md @@ -1,256 +1,126 @@ --- -title: Profiling and Optimizing Contracts +title: Profiling Transactions sidebar_position: 2 -tags: [contracts, profiling, optimization] -description: Step-by-step guide to profiling Aztec transactions and optimizing contract performance for efficient proof generation. +tags: [contracts, profiling] +description: How to profile Aztec transactions and identify performance bottlenecks. --- -This guide shows you how to profile your Aztec transactions to identify bottlenecks and optimize gas usage. +This guide shows you how to profile Aztec transactions to understand gate counts and identify optimization opportunities. ## Prerequisites -- `aztec` command installed ([see installation](../../../aztec-cli/local-network-reference.md)) -- `aztec-wallet` installed -- Aztec contract deployed and ready to test -- Basic understanding of proving and gate counts +- Aztec sandbox running ([installation guide](../../../aztec-cli/local-network-reference.md)) +- A deployed contract to profile ## Profile with aztec-wallet -### Step 1: Import test accounts +Use the `profile` command instead of `send` to get detailed gate counts: ```bash +# Import test accounts aztec-wallet import-test-accounts -``` - -### Step 2: Deploy your contract -```bash +# Deploy your contract aztec-wallet deploy MyContractArtifact \ --from accounts:test0 \ - --args \ + --args [CONSTRUCTOR_ARGS] \ -a mycontract -``` - -### Step 3: Set up initial state - -```bash -aztec-wallet send setup_state \ - -ca mycontract \ - --args \ - -f test0 -``` - -### Step 4: Profile a transaction -Instead of `send`, use `profile` with the same parameters: - -```bash -aztec-wallet profile private_function \ +# Profile a function call +aztec-wallet profile my_function \ -ca mycontract \ - --args \ + --args [FUNCTION_ARGS] \ -f accounts:test0 ``` -### Step 5: Analyze the output +### Reading the output -```bash -Gate count per circuit: - SchnorrAccount:entrypoint Gates: 21,724 Acc: 21,724 - private_kernel_init Gates: 45,351 Acc: 67,075 - MyContract:private_function Gates: 31,559 Acc: 98,634 - private_kernel_inner Gates: 78,452 Acc: 177,086 - private_kernel_reset Gates: 91,444 Acc: 268,530 - private_kernel_tail Gates: 31,201 Acc: 299,731 - -Total gates: 299,731 -``` +The profile command outputs a per-circuit breakdown: -The output shows: +```text +Per circuit breakdown: -- Gate count per circuit component -- Accumulated gate count -- Total gates for the entire transaction - -## Profile with aztec.js + Function name Time Gates Subtotal +-------------------------------------------------------------------------------- + - SchnorrAccount:entrypoint 12.34ms 21,724 21,724 + - private_kernel_init 23.45ms 45,351 67,075 + - MyContract:my_function 15.67ms 31,559 98,634 + - private_kernel_inner 34.56ms 78,452 177,086 -:::tip Profile Modes - -- `gates`: Shows gate counts per circuit -- `execution-steps`: Detailed execution trace -- `full`: Complete profiling information - -::: - -### Step 1: Profile a transaction - -```javascript -const result = await contract.methods - .my_function(args) - .profile({ - from: address, - profileMode: 'gates', - skipProofGeneration: false - }); - -console.log('Gate count:', result.gateCount); -``` - -### Step 2: Profile deployment - -```javascript -const deploy = await Contract.deploy(args).profile({ from: address, profileMode: 'full' }); +Total gates: 177,086 (Biggest circuit: private_kernel_inner -> 78,452) ``` -:::warning Experimental -Flamegraph generation is experimental and may not be available in all versions. -::: +Key metrics: -## Generate flamegraphs (if available) - -### Generate and view - -```bash -# Compile first -aztec compile - -# Generate flamegraph -aztec flamegraph target/contract.json function_name - -# Serve locally -SERVE=1 aztec flamegraph target/contract.json function_name -``` +- **Gates**: Circuit complexity for each function +- **Subtotal**: Accumulated gate count +- **Time**: Execution time per circuit -:::info Reading Flamegraphs - -- **Width** = Time in operation -- **Height** = Call depth -- **Wide sections** = Optimization targets - -::: - -## Common optimizations - -:::info Key Metrics - -- **Gate count**: Circuit complexity -- **Kernel overhead**: Per-function cost -- **Storage access**: Read/write operations - -::: - -:::tip Optimization Pattern -Batch operations to reduce kernel circuit overhead. -::: +## Profile with aztec.js -```rust -// ❌ Multiple kernel invocations -for i in 0..3 { - transfer_single(amounts[i], recipients[i]); -} +```typescript +const result = await contract.methods.my_function(args).profile({ + from: walletAddress, + profileMode: "full", + skipProofGeneration: true, +}); -// ✅ Single kernel invocation -for i in 0..3 { - let note = Note::new(amounts[i], recipients[i]); - storage.notes.at(recipients[i]).insert(note); +// Access gate counts from execution steps +for (const step of result.executionSteps) { + console.log(`${step.functionName}: ${step.gateCount} gates`); } -``` - -:::tip Storage Optimization -Group storage reads to reduce overhead. -::: -```rust -// Read once, use multiple times -let values = [storage.v1.get(), storage.v2.get(), storage.v3.get()]; -for v in values { - assert(v > 0); -} +// Access timing information +console.log("Total time:", result.stats.timings.total, "ms"); ``` -### Minimize note operations - -:::tip Note Aggregation -Combine multiple small notes into fewer larger ones to reduce proving overhead. -::: +### Profile modes -```rust -// ❌ Many small notes = high overhead -for value in values { - storage.notes.insert(Note::new(value, owner)); -} +- `gates`: Gate counts per circuit +- `execution-steps`: Detailed execution trace with bytecode and witnesses +- `full`: Complete profiling information (gates + execution steps) -// ✅ Single aggregated note = lower overhead -let total = values.reduce(|a, b| a + b); -storage.notes.insert(Note::new(total, owner)); -``` +Set `skipProofGeneration: true` for faster iteration when you only need gate counts. -## Profile different scenarios +## Generate flamegraphs with noir-profiler -### Profile with different inputs +For deeper analysis of individual contract functions, use the Noir profiler to generate interactive flamegraphs. The profiler is installed automatically with Nargo (starting noirup v0.1.4). ```bash -# Small values -aztec-wallet profile function -ca mycontract --args 10 -f test0 - -# Large values -aztec-wallet profile function -ca mycontract --args 1000000 -f test0 -``` - -### Profile execution modes - -```javascript -// Profile gates only -await contract.methods.function().profile({ profileMode: 'gates' }); - -// Profile execution steps -await contract.methods.function().profile({ profileMode: 'execution-steps' }); - -// Full profile -await contract.methods.function().profile({ profileMode: 'full' }); -``` +# Compile your contract first +aztec compile -### Skip proof generation for faster iteration +# Generate a gates flamegraph (requires bb backend) +noir-profiler gates \ + --artifact-path ./target/my_contract-MyContract.json \ + --backend-path bb \ + --output ./target -```javascript -await contract.methods.function().profile({ - profileMode: 'gates', - skipProofGeneration: true // Faster but less accurate -}); +# Generate an ACIR opcodes flamegraph +noir-profiler opcodes \ + --artifact-path ./target/my_contract-MyContract.json \ + --output ./target ``` -## Interpret profiling results - -### Gate count guidelines - -- **< 50,000 gates**: Excellent performance -- **50,000 - 200,000 gates**: Acceptable for most use cases -- **200,000 - 500,000 gates**: May cause delays, consider optimizing -- **> 500,000 gates**: Requires optimization for production - -### Common optimization targets - -1. **private_kernel_inner** - Reduce nested function calls -2. **private_kernel_reset** - Minimize note nullifications -3. **Contract functions** - Optimize computation logic -4. **private_kernel_tail** - Reduce public function calls - -## Best practices +Open the generated `.svg` file in a browser for an interactive view where: -### Development workflow +- **Width** represents gate count or opcode count +- **Height** represents call stack depth +- **Wide sections** indicate optimization targets -1. **Profile early** - Establish baseline metrics -2. **Profile often** - Check impact of changes -3. **Profile realistically** - Use production-like data -4. **Document findings** - Track optimization progress +For detailed usage, see the [Noir profiler documentation](https://noir-lang.org/docs/tooling/profiler). -### Optimization priorities +## Gate count guidelines -1. **User-facing functions** - Optimize most-used features first -2. **Critical paths** - Focus on transaction bottlenecks -3. **Batch operations** - Combine related operations -4. **Cache calculations** - Store reusable results +| Gate Count | Assessment | +| ----------------- | --------------------- | +| < 50,000 | Excellent | +| 50,000 - 200,000 | Acceptable | +| 200,000 - 500,000 | Consider optimizing | +| > 500,000 | Requires optimization | ## Next steps -- Learn about [gas optimization techniques](../../../foundational-topics/transactions.md) -- Review [benchmarking best practices](../../how_to_test_contracts.md) +- [Writing efficient contracts](./writing_efficient_contracts.md) - optimization strategies and examples +- [Transaction lifecycle](../../../foundational-topics/transactions.md) +- [Testing contracts](../../how_to_test_contracts.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md index 532052dc7d1b..2f0a4d5fd95d 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md @@ -9,80 +9,105 @@ This guide shows you how to prove historical state transitions and note inclusio ## Prerequisites -- An Aztec contract project set up with `aztec-nr` dependency +- An Aztec contract project set up - Understanding of Aztec's note and nullifier system -- Knowledge of Merkle tree concepts -## Understand what you can prove +## What you can prove You can create proofs for these elements at any past block height: -- Note inclusion/exclusion -- Nullifier inclusion/exclusion -- Note validity (included and not nullified) -- Public value existence -- Contract deployment +- **Note inclusion** - prove a note existed in the note hash tree +- **Note validity** - prove a note existed and wasn't nullified at a specific block +- **Nullifier inclusion/non-inclusion** - prove a nullifier was or wasn't in the nullifier tree +- **Contract deployment** - prove a contract was deployed or initialized -Use cases include: +Common use cases: +- Verify ownership of an asset from another contract without revealing which specific note +- Prove eligibility based on historical state (e.g., "owned tokens at block X") +- Claim rewards based on past contributions (see the [claim contract](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr) for a complete example) -- Timestamp verification in private contexts -- Eligibility verification based on historical note ownership -- Item ownership verification -- Public data existence proofs -- Contract deployment verification +## Prove note inclusion -:::info Historical Proofs -Prove state at any past block using the Archive tree. Useful for timestamps, eligibility checks, and ownership verification. -::: +Import the trait: -## Retrieve notes for proofs +#include_code history_import noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr rust -```rust -use aztec::note::note_getter_options::NoteGetterOptions; +Prove a note exists in the note hash tree: -let options = NoteGetterOptions::new(); -let notes = storage.notes.at(owner).get_notes(options); +#include_code prove_note_inclusion noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr rust -// Access first note as retrieved_note -let retrieved_note = notes.get(0); -``` +## Prove note validity -## Prove note inclusion +To prove a note was valid (existed AND wasn't nullified) at a historical block: ```rust use dep::aztec::history::note_validity::ProveNoteValidity; -// Get block header for historical proof -let header = context.get_block_header(); +let header = self.context.get_anchor_block_header(); +header.prove_note_validity(retrieved_note, &mut self.context); +``` + +This verifies both: +1. The note was included in the note hash tree +2. The note's nullifier was not in the nullifier tree + +## Prove at a specific historical block -// Prove note existed and wasn't nullified -// Requires: RetrievedNote, storage_slot, context -header.prove_note_validity(retrieved_note, storage_slot, &mut context); +To prove against state at a specific past block (not just the anchor block): + +```rust +let historical_header = self.context.get_block_header_at(block_number); +historical_header.prove_note_inclusion(retrieved_note); ``` -:::tip -Use `prove_note_validity` to verify both inclusion and non-nullification in one call. +:::warning +Using `get_block_header_at` adds ~3k constraints to prove Archive tree membership. The anchor block header is effectively free since it's verified once per transaction. ::: -## Prove nullifier inclusion +## Prove a note was nullified + +To prove a note has been spent/nullified: ```rust -use dep::aztec::history::nullifier_inclusion::ProveNullifierInclusion; -use dep::aztec::protocol_types::hash::compute_siloed_nullifier; +use dep::aztec::history::nullifier_inclusion::ProveNoteIsNullified; + +let header = self.context.get_anchor_block_header(); +header.prove_note_is_nullified(retrieved_note, &mut self.context); +``` + +## Prove contract deployment + +To prove a contract was deployed at a historical block: -// Compute nullifier (requires note hash) -let nullifier = note.compute_nullifier(&mut context, note_hash_for_nullification); -let siloed_nullifier = compute_siloed_nullifier(context.this_address(), nullifier); +```rust +use dep::aztec::history::contract_inclusion::ProveContractDeployment; -// Prove nullifier was included -context.get_block_header().prove_nullifier_inclusion(siloed_nullifier); +let header = self.context.get_anchor_block_header(); +header.prove_contract_deployment(contract_address); ``` -:::info Additional Proofs +You can also prove a contract was initialized (constructor was called): -Other available proofs: -- Note inclusion without validity check -- Nullifier non-inclusion (prove something wasn't nullified) -- Public data inclusion at historical blocks +```rust +use dep::aztec::history::contract_inclusion::ProveContractInitialization; -::: +let header = self.context.get_anchor_block_header(); +header.prove_contract_initialization(contract_address); +``` + +## Available proof traits + +The `aztec::history` module provides these traits: + +| Trait | Purpose | +|-------|---------| +| `ProveNoteInclusion` | Prove note exists in note hash tree | +| `ProveNoteValidity` | Prove note exists and is not nullified | +| `ProveNoteIsNullified` | Prove note's nullifier is in nullifier tree | +| `ProveNoteNotNullified` | Prove note's nullifier is not in nullifier tree | +| `ProveNullifierInclusion` | Prove a raw nullifier exists | +| `ProveNullifierNonInclusion` | Prove a raw nullifier does not exist | +| `ProveContractDeployment` | Prove a contract was deployed | +| `ProveContractNonDeployment` | Prove a contract was not deployed | +| `ProveContractInitialization` | Prove a contract was initialized | +| `ProveContractNonInitialization` | Prove a contract was not initialized | diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md index ad2a247bb443..d2b6e3c41d09 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_retrieve_filter_notes.md @@ -11,7 +11,13 @@ This guide shows you how to retrieve and filter notes from private storage using - Aztec contract with note storage - Understanding of note structure and properties -- Familiarity with PropertySelector and Comparator + +## Required imports + +```rust +use dep::aztec::note::note_getter_options::{NoteGetterOptions, NoteStatus, SortOrder}; +use dep::aztec::utils::comparison::Comparator; +``` ## Set up basic note retrieval @@ -26,9 +32,21 @@ This returns up to `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` notes without filterin ### Step 2: Retrieve notes from storage ```rust -let notes = storage.my_notes.at(owner).get_notes(options); +// Returns BoundedVec, ...> +let retrieved_notes = storage.my_notes.at(owner).get_notes(options); ``` +:::tip get_notes vs pop_notes + +- `get_notes`: Retrieves notes without nullifying. Use when you need to read note data. +- `pop_notes`: Retrieves AND nullifies notes in one operation. Use when consuming notes (e.g., spending tokens). More efficient than calling `get_notes` followed by manual nullification. + +::: + +Here's an example of `pop_notes` with filtering from the NFT contract: + +#include_code pop_notes noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust + ## Filter notes by properties ### Step 1: Select notes with specific field values @@ -77,81 +95,50 @@ Database `select` is more efficient than custom filters. Use custom filters only ### Create and use a custom filter +#include_code custom_filter noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr rust + +Then use it with `NoteGetterOptions`: + ```rust -fn filter_above_threshold( - notes: [Option>; MAX_NOTES], - min: Field, -) -> [Option>; MAX_NOTES] { - let mut result = [Option::none(); MAX_NOTES]; - let mut count = 0; - - for note in notes { - if note.is_some() & (note.unwrap().note.value >= min) { - result[count] = note; - count += 1; - } - } - result -} - -// Use the filter -let options = NoteGetterOptions::with_filter(filter_above_threshold, min_value); +let options = NoteGetterOptions::with_filter(filter_notes_min_sum, min_value); ``` :::warning Note Limits -Maximum notes per call: `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` (currently 128) +Maximum notes per call: `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` (currently 16) ::: :::info Available Comparators -- `EQ`: Equal to -- `NEQ`: Not equal to -- `LT`: Less than -- `LTE`: Less than or equal -- `GT`: Greater than -- `GTE`: Greater than or equal +- `Comparator.EQ`: Equal to +- `Comparator.NEQ`: Not equal to +- `Comparator.LT`: Less than +- `Comparator.LTE`: Less than or equal +- `Comparator.GT`: Greater than +- `Comparator.GTE`: Greater than or equal ::: -## Use comparators effectively - -### Available comparators - -```rust -// Equal to -options.select(MyNote::properties().value, Comparator.EQ, target_value) - -// Greater than or equal -options.select(MyNote::properties().value, Comparator.GTE, min_value) - -// Less than -options.select(MyNote::properties().value, Comparator.LT, max_value) -``` +## Call from TypeScript -### Call from TypeScript with comparator +You can pass comparator values from TypeScript to your contract functions: ```typescript -// Pass comparator from client -contract.methods.read_notes(Comparator.GTE, 5).simulate({ from: defaultAddress }) +import { Comparator } from '@aztec/aztec.js/note'; + +// Pass comparator to a contract function that accepts it as a parameter +await contract.methods.read_notes(Comparator.GTE, 5).simulate({ from: senderAddress }); ``` ## View notes without constraints -```rust -use dep::aztec::note::note_viewer_options::NoteViewerOptions; - -#[external("utility")] -unconstrained fn view_notes(comparator: u8, value: Field) -> auto { - let mut options = NoteViewerOptions::new(); - options = options.select(MyNote::properties().value, comparator, value); - storage.my_notes.view_notes(options) -} -``` +Use `NoteViewerOptions` in unconstrained utility functions to query notes without generating proofs: + +#include_code view_notes noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust :::tip Viewer vs Getter -- `NoteGetterOptions`: For constrained functions (private/public) -- `NoteViewerOptions`: For unconstrained viewing (utilities) +- `NoteGetterOptions`: For constrained private functions with proof generation (max 16 notes) +- `NoteViewerOptions`: For unconstrained utility functions, no proofs (max 10 notes per page via `MAX_NOTES_PER_PAGE`) ::: @@ -161,7 +148,7 @@ unconstrained fn view_notes(comparator: u8, value: Field) -> auto { ```rust let mut options = NoteGetterOptions::new(); -options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); +options = options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); ``` :::info Note Status Options @@ -171,34 +158,8 @@ options.set_status(NoteStatus.ACTIVE_OR_NULLIFIED); ::: -## Optimize note retrieval - -:::tip Best Practices - -1. **Use select over filter** - Database-level filtering is more efficient -2. **Set limits early** - Reduce unnecessary note processing -3. **Sort before limiting** - Get the most relevant notes first -4. **Batch operations** - Retrieve all needed notes in one call - -::: - -### Example: Optimized retrieval - -```rust -// Get highest value note for owner -let mut options = NoteGetterOptions::new(); -options = options - .select(MyNote::properties().owner, Comparator.EQ, owner) - .sort(MyNote::properties().value, SortOrder.DESC) - .set_limit(1); - -let notes = storage.my_notes.at(owner).get_notes(options); -assert(notes.len() > 0, "No notes found"); -let highest_note = notes.get(0); -``` - ## Next steps - Learn about [custom note implementations](../how_to_implement_custom_notes.md) - Explore [note discovery mechanisms](../../../foundational-topics/advanced/storage/note_discovery.md) -- Understand [note lifecycle](../../../foundational-topics/advanced/storage/indexed_merkle_tree.mdx) +- Understand [partial notes](./partial_notes.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md index ca0b541ac1ac..15311ce830b8 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules.md @@ -2,95 +2,70 @@ title: Using Capsules sidebar_position: 17 tags: [functions, oracles] -description: Learn how to use capsules to add data to the private execution environment for use in your Aztec smart contracts. +description: Learn how to use capsules for per-contract non-volatile storage in the PXE. --- -:::info What are Capsules? -Capsules provide per-contract non-volatile storage in the PXE. Data is: - -- Stored locally (not onchain) -- Scoped per contract address -- Persistent until explicitly deleted -- Useful for caching computation results - -::: - -## Available functions - -- `store` - Store data at a slot -- `load` - Retrieve data from a slot -- `delete` - Remove data at a slot -- `copy` - Copy contiguous entries between slots +Capsules provide per-contract non-volatile storage in the PXE. Data is stored locally (not onchain), scoped per contract address, and persists until explicitly deleted. ## Basic usage ```rust -use dep::aztec::oracle::capsules; +use aztec::oracle::capsules; + +// Inside a contract function, use context.this_address() for contract_address +let contract_address: AztecAddress = context.this_address(); +let slot: Field = 1; -// Store data at a slot -unconstrained fn store_data(context: &mut PrivateContext) { - capsules::store(context.this_address(), slot, value); -} +// Store data at a slot (overwrites existing data) +capsules::store(contract_address, slot, value); // Load data (returns Option) -unconstrained fn load_data(context: &mut PrivateContext) -> Option { - capsules::load(context.this_address(), slot) -} +let result: Option = capsules::load(contract_address, slot); // Delete data at a slot -unconstrained fn delete_data(context: &mut PrivateContext) { - capsules::delete(context.this_address(), slot); -} - -// Copy multiple contiguous slots -unconstrained fn copy_data(context: &mut PrivateContext) { - // Copy 3 slots from src_slot to dst_slot - capsules::copy(context.this_address(), src_slot, dst_slot, 3); -} +capsules::delete(contract_address, slot); + +// Copy contiguous slots (supports overlapping regions) +// copy(contract_address, src_slot, dst_slot, num_entries: u32) +capsules::copy(contract_address, src_slot, dst_slot, 3); ``` -:::warning Safety -All capsule operations are `unconstrained`. Data loaded from capsules should be validated in constrained contexts. Contracts can only access their own capsules - attempts to access other contracts' capsules will fail. +Types must implement `Serialize` and `Deserialize` traits. + +:::warning +All capsule operations are `unconstrained`. Data loaded from capsules should be validated in constrained contexts. Contracts can only access their own capsules. ::: -## CapsuleArray for dynamic storage +## CapsuleArray + +`CapsuleArray` provides dynamic array storage backed by capsules: ```rust -use dep::aztec::capsules::CapsuleArray; - -unconstrained fn manage_array(context: &mut PrivateContext) { - // Create/access array at base_slot - let array = CapsuleArray::at(context.this_address(), base_slot); - - // Array operations - array.push(value); // Append to end - let value = array.get(index); // Read at index - let length = array.len(); // Get current size - array.remove(index); // Delete & shift elements - - // Iterate over all elements - array.for_each(|index, value| { - // Process each element - if some_condition(value) { - array.remove(index); // Safe to remove current element - } - }); -} -``` +use aztec::capsules::CapsuleArray; +use protocol_types::hash::sha256_to_field; -:::tip Use Cases +// Use a hash for base_slot to avoid collisions with other storage +global BASE_SLOT: Field = sha256_to_field("MY_CONTRACT::MY_ARRAY".as_bytes()); -- Caching expensive computations between simulation and execution -- Storing intermediate proof data -- Managing dynamic task lists -- Persisting data across multiple transactions +let array: CapsuleArray = CapsuleArray::at(contract_address, BASE_SLOT); + +array.push(value); // Append to end +let value = array.get(index); // Read at index (throws if out of bounds) +let length = array.len(); // Get current size (returns u32) +array.remove(index); // Delete & shift elements (index is u32) + +// Iterate and optionally remove elements +array.for_each(|index, value| { + if some_condition(value) { + array.remove(index); // Safe to remove current element only + } +}); +``` +:::warning `for_each` Safety +It is safe to remove the current element during `for_each`, but **do not push new elements** during iteration. ::: :::info Storage Layout -CapsuleArray stores the length at the base slot, with elements in consecutive slots: -- Slot N: array length -- Slot N+1: element at index 0 -- Slot N+2: element at index 1 -- And so on... +CapsuleArray stores length at the base slot, with elements in consecutive slots (base+1 for index 0, base+2 for index 1, etc.). Ensure sufficient space between different array base slots. ::: diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md index cbcf7260e044..2b96eddc1ecc 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/partial_notes.md @@ -2,153 +2,90 @@ title: Partial Notes sidebar_position: 1 tags: [Developers, Contracts, Notes] -description: How partial notes work and how they can be used. +description: "Learn how partial notes enable private-to-public value transfers when data depends on onchain state." --- import Image from "@theme/IdealImage"; -## What are Partial Notes? +Partial notes are notes created with incomplete data during private execution, which are completed later with additional information that becomes available during public execution. -Partial notes are notes created with incomplete data, usually during private execution, which can be completed with additional information that becomes available later, usually during public execution. +## Prerequisites -Let's say, for example, we have a `UintNote`: +- Understanding of [notes and private state](../how_to_implement_custom_notes.md) +- Familiarity with [private and public function execution](../../../foundational-topics/call_types.md) -```rust title="uint_note_def" showLineNumbers -#[derive(Deserialize, Eq, Serialize, Packable)] -#[custom_note] -pub struct UintNote { - /// The number stored in the note. - pub value: u128, -} -``` -> Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L27-L34 +## Overview +Consider a `UintNote`: -The `UintNote` struct itself only contains the `value` field. Additional fields including `owner`, `randomness`, and `storage_slot` are passed as parameters during note hash computation. +#include_code uint_note_def noir-projects/aztec-nr/uint-note/src/uint_note.nr rust -When creating the note locally during private execution, the `owner` and `storage_slot` are known, but the `value` potentially is not (e.g., it depends on some onchain dynamic variable). First, a **partial note** can be created during private execution that commits to the `owner`, `randomness`, and `storage_slot`, and then the note is *"completed"* to create a full note by later adding the `value` field, usually during public execution. +The struct only contains the `value` field. Additional fields (`owner`, `randomness`, `storage_slot`) are passed as parameters during note hash computation. + +When creating a note in private, the `owner` and `storage_slot` are known, but the `value` may not be (e.g., it depends on onchain state). A **partial note** commits to the private fields first, then is _completed_ by adding the `value` field during public execution. ## Use Cases -Partial notes are useful when a e.g., part of the note struct is a value that depends on dynamic, public onchain data that isn't available during private execution, such as: +Partial notes are useful when part of the note depends on dynamic, public onchain data unavailable during private execution: - AMM swap prices - Current gas prices - Time-dependent interest accrual -## Implementation - -All notes in Aztec use the partial note format internally. This ensures that notes produce identical note hashes regardless of whether they were created as complete notes (with all fields known in private) or as partial notes (completed later in public). By having all notes follow the same two-phase hash commitment process, the protocol maintains consistency and allows notes created through different flows to behave identically. - -### Note Structure Example - -The `UintNote` struct contains only the `value` field: - -```rust title="uint_note_def" showLineNumbers -#[derive(Deserialize, Eq, Serialize, Packable)] -#[custom_note] -pub struct UintNote { - /// The number stored in the note. - pub value: u128, -} -``` -> Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L27-L34 - +## Two-Phase Commitment Process -### Two-Phase Commitment Process +All notes in Aztec use the partial note format internally. This ensures identical note hashes regardless of whether notes were created complete (all fields known in private) or as partial notes (completed later in public). -**Phase 1: Partial Commitment (Private Execution)** +### Phase 1: Partial Commitment (Private Execution) -The private fields (`owner`, `randomness`, and `storage_slot`) are committed during local, private execution: +The private fields (`owner`, `randomness`, `storage_slot`) are committed during private execution, creating a `PartialUintNote`: -```rust title="compute_partial_commitment" showLineNumbers -fn compute_partial_commitment( - owner: AztecAddress, - storage_slot: Field, - randomness: Field, -) -> Field { - poseidon2_hash_with_separator( - [owner.to_field(), storage_slot, randomness], - DOM_SEP__NOTE_HASH, - ) -} -``` -> Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L160-L171 +#include_code partial_uint_note_def noir-projects/aztec-nr/uint-note/src/uint_note.nr rust +The commitment is computed as: -This creates a partial note commitment: +#include_code compute_partial_commitment noir-projects/aztec-nr/uint-note/src/uint_note.nr rust -``` -partial_commitment = H(owner, storage_slot, randomness) -``` +This produces: `partial_commitment = H(owner, storage_slot, randomness)` -**Phase 2: Note Completion (Public Execution)** +### Phase 2: Note Completion (Public Execution) The note is completed by hashing the partial commitment with the public value: -```rust title="compute_complete_note_hash" showLineNumbers -fn compute_complete_note_hash(self, value: u128) -> Field { - // Here we finalize the note hash by including the (public) value into the partial note commitment. Note that we - // use the same generator index as we used for the first round of poseidon - this is not an issue. - poseidon2_hash_with_separator( - [self.commitment, value.to_field()], - DOM_SEP__NOTE_HASH, - ) -} -``` -> Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L284-L293 +#include_code compute_complete_note_hash noir-projects/aztec-nr/uint-note/src/uint_note.nr rust +The resulting note hash is: `H(partial_commitment, value)` -The resulting structure is a nested commitment: +### Complete Notes Use the Same Format -``` -note_hash = H(H(owner, storage_slot, randomness), value) - = H(partial_commitment, value) -``` +When a note is created with all fields known, it still follows the same two-phase process internally: -## Universal Note Format +#include_code compute_note_hash noir-projects/aztec-nr/uint-note/src/uint_note.nr rust -All notes in Aztec use the partial note format internally, even when all data is known during private execution. This ensures consistent note hash computation regardless of how the note was created. +This ensures notes with identical field values produce identical note hashes, regardless of whether they were created as partial or complete notes. -When a note is created with all fields known (including `owner`, `storage_slot`, `randomness`, and `value`): +## Using Partial Notes -1. A partial commitment is computed from the private fields (`owner`, `storage_slot`, `randomness`) -2. The partial commitment is immediately completed with the `value` field +The typical workflow involves two function calls. The [Token contract](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr) demonstrates this pattern: -```rust title="compute_note_hash" showLineNumbers -fn compute_note_hash( - self, - owner: AztecAddress, - storage_slot: Field, - randomness: Field, -) -> Field { - // Partial notes can be implemented by having the note hash be either the result of multiscalar multiplication - // (MSM), or two rounds of poseidon. MSM results in more constraints and is only required when multiple variants - // of partial notes are supported. Because UintNote has just one variant (where the value is public), we use - // poseidon instead. +**1. Private function**: Create the partial note using `UintNote::partial()`: - // We must compute the same note hash as would be produced by a partial note created and completed with the same - // values, so that notes all behave the same way regardless of how they were created. To achieve this, we - // perform both steps of the partial note computation. +#include_code prepare_private_balance_increase noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust - // First we create the partial note from a commitment to the private content (including storage slot). - let partial_note = PartialUintNote { - commitment: compute_partial_commitment(owner, storage_slot, randomness), - }; +**2. Public function**: Complete the note with the now-known value: - // Then compute the completion note hash. In a real partial note this step would be performed in public. - partial_note.compute_complete_note_hash(self.value) -} -``` -> Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L37-L61 +#include_code finalize_transfer_to_private noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust +The `completer` parameter ensures only the authorized address can finalize the note, preventing front-running attacks. -This two-step process ensures that notes with identical field values produce identical note hashes, regardless of whether they were created as partial notes or complete notes. +## Example: AMM Contract - +The [AMM contract](https://github.com/AztecProtocol/aztec-packages/tree/next/noir-projects/noir-contracts/contracts/app/amm_contract) uses partial notes for token swaps. Since the exchange rate is only known onchain, a partial note is created for the recipient in private, then completed during public execution once the output amount is calculated. -## Partial Notes in Practice +## Next Steps -To understand how to use partial notes in practice, [this AMM contract](https://github.com/AztecProtocol/aztec-packages/tree/next/noir-projects/noir-contracts/contracts/app/amm_contract) uses partial notes to initiate and complete the swap of `token1` to `token2`. Since the exchange rate is onchain, it cannot be known ahead of time while executing in private so a full note cannot be created. Instead, a partial note is created for the `owner` swapping the tokens. This partial note is then completed during public execution once the exchange rate can be read. +- [Implement custom notes](../how_to_implement_custom_notes.md) - Learn about note structure and lifecycle +- [Private and public execution](../../../foundational-topics/call_types.md) - Understand the execution model +- [Token contract tutorial](../../../tutorials/contract_tutorials/token_contract.md) - See partial notes in a complete example diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md index 7426b092d303..dc3f05c0fba2 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/protocol_oracles.md @@ -29,7 +29,7 @@ Oracles introduce **non-determinism** into a circuit, and thus are `unconstraine - [`auth_witness`](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/authwit/src/auth_witness.nr) - Provides a way to fetch the authentication witness for a given address. This is useful when building account contracts to support approve-like functionality. - [`get_l1_to_l2_message`](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/aztec/src/oracle/get_l1_to_l2_message.nr) - Useful for application that receive messages from L1 to be consumed on L2, such as token bridges or other cross-chain applications. - [`notes`](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/aztec/src/oracle/notes.nr) - Provides a lot of functions related to notes, such as fetches notes from storage etc, used behind the scenes for value notes and other pre-build note implementations. -- [`logs`](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/aztec/src/oracle/logs.nr) - Provides the to log encrypted and unencrypted data. +- [`logs`](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/aztec/src/oracle/logs.nr) - Provides functions to log encrypted and unencrypted data. Find a full list [on GitHub](https://github.com/AztecProtocol/aztec-packages/tree/master/noir-projects/aztec-nr/aztec/src/oracle). diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md index 52814a100935..5f8d3f34f5ed 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/writing_efficient_contracts.md @@ -68,11 +68,19 @@ For data availability, blobs are utilized since data storage is often cheaper he After the first section about generating a flamegraph for an Aztec function, each section shows an example of different optimisation techniques. -### Inspecting with Flamegraph +### Inspecting with flamegraphs -You can see the params for the Aztec's flamegraph using: `aztec help flamegraph` +Use the Noir profiler to generate flamegraphs for your contract functions. The profiler is installed automatically with Nargo (starting noirup v0.1.4). -For example, the resulting flamegraph (as an .svg file) of a counter's increment function can be generated and served with: `SERVE=1 aztec flamegraph target/counter-Counter.json increment` +```bash +# Generate a gates flamegraph (requires bb backend) +noir-profiler gates \ + --artifact-path ./target/counter-Counter.json \ + --backend-path bb \ + --output ./target +``` + +Open the generated `.svg` file in a browser for an interactive view. For more details, see the [profiling guide](./how_to_profile_transactions.md). @@ -227,11 +235,16 @@ unconstrained fn sqrt_unconstrained(number: Field) -> Field { The two implementations after the contract differ in one being constrained vs unconstrained, as well as the loop implementation (which has other design considerations). Measuring the two, we find the `sqrt_inefficient` to require around 1500 extra gates compared to `sqrt_efficient`. -To see each flamegraph: +To generate flamegraphs for each function: + +```bash +noir-profiler gates \ + --artifact-path ./target/optimisation_example-OptimisationExample.json \ + --backend-path bb \ + --output ./target +``` -- `SERVE=1 aztec flamegraph target/optimisation_example-OptimisationExample.json sqrt_inefficient` -- `SERVE=1 aztec flamegraph target/optimisation_example-OptimisationExample.json sqrt_efficient` -- (if you make changes to the code, you will need to compile and regenerate the flamegraph, then refresh in your browser to use the latest svg file) +If you make changes to the code, recompile and regenerate the flamegraph, then refresh the `.svg` file in your browser. Note: this is largely a factor of the loop size choice based on the maximum size of `number` you are required to be calculating the square root of. For larger numbers, the loop would have to be much larger, so perform in an unconstrained way (then constraining the result) is much more efficient. @@ -285,7 +298,7 @@ unconstrained fn sort_array(array: [u32; ARRAY_SIZE]) -> [u32; ARRAY_SIZE] { ``` -Like before, the flamegraph command can be used to present the gate counts of the private functions, highlighting that 953 gates could be saved. +Like before, `noir-profiler` can be used to visualize the gate counts of the private functions, highlighting that 953 gates could be saved. Note: The stdlib provides a highly optimized version of sort on arrays, `array.sort()`, which saves even more gates. diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md b/docs/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md index d2fe84011164..b67dd0e2e9ba 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/contract_artifact.md @@ -5,108 +5,79 @@ tags: [contracts] sidebar_position: 13 --- -After compiling a contract you'll get a Contract Artifact file, that contains the data needed to interact with a specific contract, including its name, functions that can be executed, and the interface and code of those functions. Since private functions are not published in the Aztec network, you'll need this artifact file to be able to call private functions of contracts. +Compiling an Aztec contract produces a contract artifact file (`.json`) containing everything needed to interact with that contract: its name, functions, their interfaces, and compiled bytecode. Since private function bytecode is never published to the network, you need this artifact file to call private functions. -The artifact file can be used with `aztec.js` to instantiate contract objects and interact with them. +:::tip Most developers don't need this +When you [compile a contract](../how_to_compile_contract.md) and use [`aztec codegen`](../../aztec-js/how_to_deploy_contract.md#generate-typescript-bindings), you get type-safe TypeScript classes that handle artifacts automatically. This page is useful if you're: +- Building custom tooling around Aztec contracts +- Debugging compilation or deployment issues +- Understanding what data is available in artifacts +::: -## Contract Artifact Structure +## Where to Find Artifacts -The structure of a contract artifact is as follows: -```json -{ - "name": "CardGame", - "functions": [ - { - "name": "constructor", - "functionType": "private", - "isInternal": false, - "parameters": [], - "returnTypes": [], - "bytecode": "...", - "verificationKey": "..." - }, - { - "name": "on_card_played", - "functionType": "public", - "isInternal": true, - "parameters": [ - { - "name": "game", - "type": { - "kind": "integer", - "sign": "unsigned", - "width": 32 - }, - "visibility": "private" - }, - { - "name": "player", - "type": { - "kind": "field" - }, - "visibility": "private" - }, - { - "name": "card_as_field", - "type": { - "kind": "field" - }, - "visibility": "private" - } - ], - "returnTypes": [ - ... - ], - "bytecode": "...", - "verificationKey": "..." - }, - ... - ] -} +After running `aztec compile`, artifacts are output to the `target/` directory: +``` +target/ +└── my_contract-MyContract.json # Contract artifact ``` -### `name` -It is a simple string that matches the name that the contract developer used for this contract in noir. It's used for logs and errors. +Use `aztec codegen` to generate TypeScript bindings from these artifacts for type-safe contract interaction. + +## Contract Artifact Structure -### `functions` -A contract is a collection of several functions that can be called. Each function has the following properties: +A contract artifact contains: -#### `function.name` -A simple string that matches the name that the contract developer used for this function in noir. For logging and debugging purposes. +- **`name`**: The contract name as defined in Noir +- **`functions`**: Array of function artifacts (private, public dispatch, and utility functions) +- **`nonDispatchPublicFunctions`**: Public function ABIs (excluding the dispatch function) +- **`outputs`**: Exported structs and globals from the contract +- **`storageLayout`**: Storage slot mappings for contract state +- **`fileMap`**: Source file mappings for debugging -#### `function.functionType` -The function type can have one of the following values: +## Function Properties -- Private: The function is ran and proved locally by the clients, and its bytecode not published to the network. -- Public: The function is ran and proved by the sequencer, and its bytecode is published to the network. -- Utility: The function is ran locally by the clients to generate digested information useful for the user. It cannot be called in a transaction. +Each function in the artifact includes: -#### `function.isInternal` -The is internal property is a boolean that indicates whether the function is internal to the contract and cannot be called from outside. +| Property | Description | +|----------|-------------| +| `name` | Function name as defined in Noir | +| `functionType` | One of `private`, `public`, or `utility` | +| `isOnlySelf` | If `true`, function can only be called from within the same contract | +| `isStatic` | If `true`, function cannot alter state | +| `isInitializer` | If `true`, function can be used as a constructor | +| `parameters` | Array of input parameters with name, type, and visibility | +| `returnTypes` | Array of return value types | +| `errorTypes` | Custom error types the function can throw | +| `bytecode` | Compiled ACIR bytecode (base64 encoded) | +| `verificationKey` | Verification key for private functions (optional) | +| `debugSymbols` | Compressed debug information linking to source code | -#### `function.parameters` -Each function can have multiple parameters that are arguments to execute the function. Parameters have a name, and type (like integers, strings, or complex types like arrays and structures). +### Function Types -#### `function.returnTypes` -The return types property defines the types of values that the function returns after execution. +- **`private`**: Executed and proved locally by the client. Bytecode is not published to the network. +- **`public`**: Executed and proved by the sequencer. Bytecode is published to the network. +- **`utility`**: Executed locally to compute information (e.g., view functions). Cannot be called in transactions. -#### `function.bytecode` -The bytecode is a string representing the compiled ACIR of the function, ready for execution on the network. +## Parameter and Return Types -#### `function.verificationKey` -The verification key is an optional property that contains the verification key of the function. This key is used to verify the proof of the function execution. +Parameters and return values use these type definitions: -### `debug` (Optional) -Although not significant for non-developer users, it is worth mentioning that there is a debug section in the contract artifact which helps contract developers to debug and test their contracts. This section mainly contains debug symbols and file maps that link back to the original source code. +| Type | Description | +|------|-------------| +| `field` | A field element in the BN254 curve's scalar field | +| `boolean` | True/false value | +| `integer` | Whole number with `sign` (`signed`/`unsigned`) and `width` (bits) | +| `array` | Collection of elements with `length` and element `type` | +| `string` | Character sequence with fixed `length` | +| `struct` | Composite type with named `fields` and a `path` identifier | +| `tuple` | Unnamed composite type with ordered `fields` | -## Understanding Parameter and Return Types -To make the most of the functions, it's essential to understand the types of parameters and return values. Here are some common types you might encounter: +Parameter visibility can be `public`, `private`, or `databus`. - - `field`: A basic type representing a field element in the finite field of the curve used in the Aztec protocol. - - `boolean`: A simple true/false value. - - `integer`: Represents whole numbers. It has attributes defining its sign (positive or negative) and width (the number of bits representing the integer). - - `array`: Represents a collection of elements, all of the same type. It has attributes defining its length and the type of elements it holds. - - `string`: Represents a sequence of characters with a specified length. - - `struct`: A complex type representing a structure with various fields, each having a specific type and name. +## Next Steps +- [Compile contracts](../how_to_compile_contract.md) to generate artifacts +- [Deploy contracts](../../aztec-js/how_to_deploy_contract.md) using generated TypeScript bindings +- [Send transactions](../../aztec-js/how_to_send_transaction.md) to interact with deployed contracts diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md b/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md index 0ea6ee0106f4..998800676b4d 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md @@ -5,23 +5,15 @@ tags: [contracts] description: Learn the fundamental structure of Aztec smart contracts including the contract keyword, directory layout, and how contracts manage state and functions. --- -A contract is a collection of persistent state variables and [functions](./functions/index.md) which may manipulate these variables. Functions and state variables within a contract's scope are said to belong to that contract. A contract can only access and modify its own state. If a contract wishes to access or modify another contract's state, it must make a call to an external function of the other contract. For anything to happen on the Aztec network, an external function of a contract needs to be called. +A contract is a collection of persistent [state variables](./how_to_define_storage.md) and [functions](./functions/index.md) which may manipulate these variables. Functions and state variables within a contract's scope are said to belong to that contract. -## Contract - -A contract may be declared and given a name using the `contract` keyword (see snippet below). By convention, contracts are named in `PascalCase`. +A contract can only access and modify its own state. If a contract wishes to access or modify another contract's state, it must make a call to an external function of the other contract. For anything to happen on the Aztec network, an external function of a contract needs to be called. -```rust title="contract keyword" -// highlight-next-line -contract MyContract { +## Contract - // Imports +A contract is declared using the `#[aztec]` attribute and the `contract` keyword. By convention, contracts are named in `PascalCase`. - // Storage - - // Functions -} -``` +#include_code setup /docs/examples/contracts/counter_contract/src/main.nr rust :::info A note for vanilla Noir devs There is no [`main()`](https://noir-lang.org/docs/getting_started/project_breakdown/#mainnr) function within a Noir `contract` scope. More than one function can be an entrypoint. @@ -31,11 +23,17 @@ There is no [`main()`](https://noir-lang.org/docs/getting_started/project_breakd Here's a common layout for a basic Aztec.nr Contract project: -```title="layout of an aztec contract project" +```text title="layout of an aztec contract project" ─── my_aztec_contract_project ├── src - │ ├── main.nr <-- your contract + │ └── main.nr <-- your contract └── Nargo.toml <-- package and dependency management ``` -- See the vanilla Noir docs for [more info on packages](https://noir-lang.org/docs/noir/modules_packages_crates/crates_and_packages). +See the vanilla Noir docs for [more info on packages](https://noir-lang.org/docs/noir/modules_packages_crates/crates_and_packages). + +## Next steps + +- [Define functions](./functions/index.md) - Learn about private, public, and utility functions +- [Define storage](./how_to_define_storage.md) - Work with persistent state variables +- [Compile your contract](../how_to_compile_contract.md) - Build your contract artifact diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md b/docs/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md index fee477370508..74801937dace 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/contract_upgrades.md @@ -5,182 +5,133 @@ tags: [contracts] description: Understand contract upgrade patterns in Aztec and how to implement upgradeable contracts. --- -For familiarity we've used terminology like "deploying a contract instance of a contract class". When considering how it works with contract upgrades it helps to be more specific. +Each contract instance refers to a contract class ID for its code. Upgrading a contract's implementation involves updating its current class ID to a new class ID, while retaining the original class ID for address verification. -Each contract instance refers to a class id for its code. Upgrading a contract's implementation is achieved by updating its current class id to a new class id, whilst retaining the "original class id" for reasons explained below. +## Original class ID -## Original class id +A contract stores the original contract class it was instantiated with. This original class ID is used when calculating and verifying the contract's [address](../../foundational-topics/contract_creation#instance-address) and remains unchanged even if a contract is upgraded. -A contract keeps track of the original contract class that it was instantiated with, which is the "original" class id. It is this original class that is used when calculating and verifying the contract's [address](../../foundational-topics/contract_creation#instance-address). -This variable remains unchanged even if a contract is upgraded. +## Current class ID -## Current class id - -When a contract is first deployed, its current class ID is set equal to its original class ID. The current class ID determines which code implementation the contract actually executes. +When a contract is first deployed, its current class ID equals its original class ID. The current class ID determines which code implementation the contract executes. During an upgrade: - The original class ID remains unchanged -- The current class ID is updated to refer to the new implementation -- All contract state/data is preserved +- The current class ID is updated to the new implementation +- All contract state and data are preserved ## How to upgrade -Contract upgrades in Aztec have to be initiated by the contract that wishes to be upgraded calling the `ContractInstanceRegistry`: +Contract upgrades must be initiated by the contract itself calling the `ContractInstanceRegistry`: ```rust -use dep::aztec::protocol_types::contract_class_id::ContractClassId; +use aztec::protocol_types::{ + constants::CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, + contract_class_id::ContractClassId, +}; use contract_instance_registry::ContractInstanceRegistry; #[external("private")] fn update_to(new_class_id: ContractClassId) { - ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) - .update(new_class_id) - .enqueue(&mut context); + self.enqueue( + ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) + .update(new_class_id) + ); } ``` -The `update` function in the registry is a public function, so you can enqueue it from a private function like the example or call it from a public function directly. +The `update` function in the registry is a public function, so you can enqueue it from a private function (as shown above) or call it directly from a public function. -:::note -Recall that `#[external("private")]` means calling this function preserves privacy, and it still CAN be called externally by anyone. -So the `update_to` function above allows anyone to update the contract that implements it. A more complete implementation should have a proper authorization systems to secure contracts from malicious upgrades. +:::warning[Access Control] +The example `update_to` function above has no access control, meaning anyone could call it to upgrade your contract. Production contracts should implement proper authorization checks to secure against malicious upgrades. ::: -Contract upgrades are implemented using a DelayedPublicMutable storage variable in the `ContractInstanceRegistry`, since the upgrade applies to both public and private functions. -This means that they have a delay before entering into effect. The default delay is `86400` seconds (one day) but can be configured by the contract: +Contract upgrades use a `DelayedPublicMutable` storage variable in the `ContractInstanceRegistry`, applying to both public and private functions. Upgrades have a delay before taking effect. The default delay is `86400` seconds (one day) but can be configured: ```rust -use dep::aztec::protocol_types::contract_class_id::ContractClassId; -use contract_instance_registry::ContractInstanceRegistry; - #[external("private")] fn set_update_delay(new_delay: u64) { - ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) - .set_update_delay(new_delay) - .enqueue(&mut context); + self.enqueue( + ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) + .set_update_delay(new_delay) + ); } ``` -Where `new_delay` is denominated in seconds. However, take into account that changing the update delay also has as its delay that is the previous delay. So the first delay change will take `86400` seconds to take into effect. +The `new_delay` parameter is in seconds. Changing the update delay is also subject to the previous delay, so the first delay change takes `86400` seconds to take effect. :::info -The update delay cannot be set lower than `600` seconds +The minimum update delay is `600` seconds. ::: -When sending a transaction, the expiration timestamp of your tx will be the timestamp of the current block number you're simulating with + the minimum of the update delays that you're interacting with. -If your tx interacts with a contract that can be upgraded in 1000 seconds and another one that can be upgraded in 10000 seconds, the expiration timestamp (include_by_timestamp property on the tx) will be current block timestamp + 1000. -Note that this can be even lower if there is an upgrade pending in one of the contracts you're interacting with. -If the contract you interacted with will upgrade in 100 seconds, the expiration timestamp of your tx will be current block timestamp + 99 seconds. -Other DelayedPublicMutable storage variables read in your tx might reduce this expiration timestamp further. +### Transaction expiration + +When sending a transaction, the expiration timestamp is calculated as the current block timestamp plus the minimum update delay of all contracts you interact with. For example: + +- If you interact with contracts having delays of 1000 and 10000 seconds, expiration is current timestamp + 1000 seconds +- If a contract has a pending upgrade in 100 seconds, expiration would be current timestamp + 99 seconds + +Other `DelayedPublicMutable` storage variables in your transaction may reduce the expiration timestamp further. :::note -Only deployed contract instances can upgrade or change its upgrade delay currently. This restriction might be lifted in the future. +Only deployed contract instances can upgrade or change their upgrade delay. This restriction may be lifted in the future. ::: -### Upgrade Process +### Upgrade process -1. **Register New Implementation** +1. **Register the new implementation**: Register the new contract class if it contains public functions. The new implementation must maintain state variable compatibility with the original contract. - - First, register the new contract class if it contains public functions - - The new implementation must maintain state variable compatibility with the original contract +2. **Perform the upgrade**: Call the update function with the new contract class ID. The contract's original class ID remains unchanged while the current class ID updates to the new implementation. -2. **Perform Upgrade** +3. **Wait for the delay**: The upgrade takes effect after the configured delay period. - - Call the update function with the new contract class ID - - The contract's original class ID remains unchanged - - The current class ID is updated to the new implementation - - All contract state and data are preserved +4. **Verify the upgrade**: After the delay, the contract executes functions from the new implementation. The contract address remains the same since it's based on the original class ID. -3. **Verify Upgrade** - - After upgrade, the contract will execute functions from the new implementation - - The contract's address remains the same since it's based on the original class ID - - Existing state variables and their values are preserved +### Interacting with an upgraded contract -### How to interact with an upgraded contract +The PXE stores contract instances and classes locally. After a contract upgrades, you must register the new artifact with the wallet before interacting with it: -The PXE in the wallet stores the contract instances and classes in a local database. When a contract is updated, in order to interact with it we need to pass the new artifact to the PXE in the wallet, since the protocol doesn't publish artifacts. -Consider this contract as an example: +```typescript +import { getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; +import { publishContractClass } from '@aztec/aztec.js/deployment'; -```rust -#[aztec] -contract Updatable { -... - - #[external("private")] - fn update_to(new_class_id: ContractClassId) { - ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS).update(new_class_id).enqueue( - &mut context, - ); - } -... -``` +// Deploy the original contract (use .wait() to get both contract and instance) +const { contract, instance } = await UpdatableContract.deploy(wallet, ...args) + .send({ from: accountAddress }) + .wait(); -You'd upgrade it in aztec.js doing something similar to this: +// Publish the new contract class (required before upgrading) +await (await publishContractClass(wallet, UpdatedContractArtifact)) + .send({ from: accountAddress }) + .wait(); -```typescript -const contract = await UpdatableContract.deploy(wallet, ...args) - .send() - .deployed(); +// Get the new contract class ID const updatedContractClassId = ( await getContractClassFromArtifact(UpdatedContractArtifact) ).id; -await contract.methods.update_to(updatedContractClassId).send().wait(); -``` -Now, when the update has happened, calling `at` with the new contract artifact will automatically update the contract instance in the wallet if it's outdated: +// Trigger the upgrade +await contract.methods + .update_to(updatedContractClassId) + .send({ from: accountAddress }) + .wait(); -```typescript -// 'at' will call wallet updateContract if outdated -const updatedContract = await UpdatedContract.at(address, wallet); -``` +// Wait for the upgrade delay to pass... -If you try to call `at` with a different contract that is not the current version, it'll fail +// Register the new artifact with the wallet +await wallet.registerContract(instance, UpdatedContract.artifact); -```typescript -// throws when trying to update the wallet instance to RandomContract -// since the current one is UpdatedContract -await RandomContract.at(address, wallet); +// Create a contract instance with the new artifact +const updatedContract = UpdatedContract.at(contract.address, wallet); ``` -### Security Considerations - -1. **Access Control** - - - Implement proper access controls for upgrade functions - - Consider customizing the upgrades delay for your needs using `set_update_delay` - -2. **State Compatibility** - - - Ensure new implementation is compatible with existing state - - Maintain the same storage layout to prevent data corruption - -3. **Testing** +If you try to register a contract artifact that doesn't match the current contract class, the registration will fail. - - Test upgrades thoroughly in a development environment - - Verify all existing functionality works with the new implementation +### Security considerations -### Example +1. **Access control**: Implement proper access controls for upgrade functions. Consider using `set_update_delay` to customize the delay for your security requirements. -```rust -contract Updatable { - #[external("private")] - fn update_to(new_class_id: ContractClassId) { - // TODO: Add access control - assert(context.msg_sender() == owner, "Unauthorized"); - - // Perform the upgrade - ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) - .update(new_class_id) - .enqueue(&mut context); - } +2. **State compatibility**: Ensure the new implementation is compatible with existing state. Maintain the same storage layout to prevent data corruption. - #[external("private")] - fn set_update_delay(new_delay: u64) { - // TODO: Add access control - ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS) - .set_update_delay(new_delay) - .enqueue(&mut context); - } -} -``` +3. **Testing**: Test upgrades thoroughly in a development environment. Verify all existing functionality works with the new implementation. diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/dependencies.md b/docs/docs-developers/docs/aztec-nr/framework-description/dependencies.md index ba87208b0e04..a19c421bf9a8 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/dependencies.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/dependencies.md @@ -1,46 +1,78 @@ --- -title: Importing Aztec.nr -description: Learn how to manage dependencies in your Aztec smart contract projects. +title: Aztec.nr Dependencies +description: Reference list of available Aztec.nr libraries and their Nargo.toml dependency paths. tags: [contracts] sidebar_position: 2 --- -On this page you will find information about Aztec.nr libraries and up-to-date paths for use in your `Nargo.toml`. +This page lists the available Aztec.nr libraries. Add dependencies to the `[dependencies]` section of your `Nargo.toml`: -## Aztec +```toml +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="aztec" } +# Add other libraries as needed +``` + +## Core + +### Aztec (required) ```toml aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/aztec" } ``` -This is the core Aztec library that is required for every Aztec.nr smart contract. +The core Aztec library required for every Aztec.nr smart contract. + +### Protocol Types + +```toml +protocol_types = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-protocol-circuits/crates/types"} +``` + +Contains types used in the Aztec protocol (addresses, constants, hashes, etc.). -## Address note +## Note Types + +### Address Note ```toml address_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/address-note" } ``` -This is a library for utilizing notes that hold addresses. Find it on [GitHub](https://github.com/AztecProtocol/aztec-packages/tree/master/noir-projects/aztec-nr/address-note/src). +Provides `AddressNote`, a note type for storing `AztecAddress` values. -## Easy private state +### Field Note ```toml -easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/easy-private-state" } +field_note = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="field-note" } ``` -This is an abstraction library for using private variables like [`EasyPrivateUint` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/6c20b45993ee9cbd319ab8351e2722e0c912f427/noir-projects/aztec-nr/easy-private-state/src/easy_private_state.nr#L17). +Provides `FieldNote`, a note type for storing a single `Field` value. -## Protocol Types +### Uint Note ```toml -protocol_types = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-protocol-circuits/crates/types"} +uint_note = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="uint-note" } +``` + +Provides `UintNote`, a note type for storing `u128` values. Also includes `PartialUintNote` for partial note workflows where the value is completed in public execution. + +## State Variables + +### Balance Set + +```toml +balance_set = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="balance-set" } ``` -This library contains types that are used in the Aztec protocol. Find it on [GitHub](https://github.com/AztecProtocol/aztec-packages/tree/master/noir-projects/noir-protocol-circuits/crates/types/src). +Provides `BalanceSet`, a state variable for managing private balances. Includes helper functions for adding, subtracting, and querying balances. -## Value note +## Utilities + +### Compressed String ```toml -value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/value-note" } +compressed_string = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="compressed-string" } ``` + +Provides `CompressedString` and `FieldCompressedString` utilities for working with compressed string data. diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/data_structures.md b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/data_structures.md index 4a82228a3911..f33d9e7fd890 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/data_structures.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/data_structures.md @@ -3,56 +3,36 @@ title: Data Structures description: Learn about the data structures used in Aztec portals for L1-L2 communication. --- -The `DataStructures` are structs that we are using throughout the message infrastructure and registry. +This page documents the Solidity structs used for L1-L2 message passing in the Aztec protocol. -**Links**: [Implementation (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/DataStructures.sol). +**Source**: [DataStructures.sol](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/libraries/DataStructures.sol) ## `L1Actor` -An entity on L1, specifying the address and the chainId for the entity. Used when specifying sender/recipient with an entity that is on L1. +An entity on L1, specifying the address and the chainId. Used when specifying a sender or recipient on L1. #include_code l1_actor l1-contracts/src/core/libraries/DataStructures.sol solidity -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `actor` | `address` | The L1 address of the actor | -| `chainId` | `uint256` | The chainId of the actor. Defines the blockchain that the actor lives on. | - - ## `L2Actor` -An entity on L2, specifying the address and the version for the entity. Used when specifying sender/recipient with an entity that is on L2. +An entity on L2, specifying the Aztec address and the protocol version. Used when specifying a sender or recipient on L2. #include_code l2_actor l1-contracts/src/core/libraries/DataStructures.sol solidity -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `actor` | `bytes32` | The aztec address of the actor. | -| `version` | `uint256` | The version of Aztec that the actor lives on. | - -## `L1ToL2Message` +## `L1ToL2Msg` -A message that is sent from L1 to L2. +A message sent from L1 to L2. The `secretHash` field contains the hash of a secret pre-image that must be known to consume the message on L2. Use [`computeSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/stdlib/src/hash/hash.ts) to compute it from a secret. #include_code l1_to_l2_msg l1-contracts/src/core/libraries/DataStructures.sol solidity -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `sender` | `L1Actor` | The actor on L1 that is sending the message. | -| `recipient` | `L2Actor` | The actor on L2 that is to receive the message. | -| `content` | `field (~254 bits)` | The field element containing the content to be sent to L2. | -| `secretHash` | `field (~254 bits)` | The hash of a secret pre-image that must be known to consume the message on L2. Use [`computeSecretHash` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | +## `L2ToL1Msg` -## `L2ToL1Message` - -A message that is sent from L2 to L1. +A message sent from L2 to L1. #include_code l2_to_l1_msg l1-contracts/src/core/libraries/DataStructures.sol solidity -| Name | Type | Description | -| -------------- | ------- | ----------- | -| `sender` | `L2Actor` | The actor on L2 that is sending the message. | -| `recipient` | `L1Actor` | The actor on L1 that is to receive the message. | -| `content` | `field (~254 bits)` | The field element containing the content to be consumed by the portal on L1. | - +## See also +- [Inbox](./inbox.md) - L1 contract for sending messages to L2 +- [Outbox](./outbox.md) - L1 contract for consuming messages from L2 +- [Portal messaging overview](./index.md) - How L1-L2 messaging works diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/inbox.md b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/inbox.md index b96a9c621291..17a343bcbd32 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/inbox.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/inbox.md @@ -4,9 +4,9 @@ description: Learn about the inbox mechanism in Aztec portals for receiving mess tags: [portals, contracts] --- -The `Inbox` is a contract deployed on L1 that handles message passing from L1 to the rollup (L2) +The `Inbox` is a contract deployed on L1 that handles message passing from L1 to L2. -**Links**: [Interface (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol), [Implementation (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Inbox.sol). +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/messagebridge/Inbox.sol). ## `sendL2Message()` @@ -14,30 +14,56 @@ Sends a message from L1 to L2. #include_code send_l1_to_l2_message l1-contracts/src/core/interfaces/messagebridge/IInbox.sol solidity - -| Name | Type | Description | -| -------------- | ------- | ----------- | -| Recipient | `L2Actor` | The recipient of the message. This **MUST** match the rollup version and an Aztec contract that is **attached** to the contract making this call. If the recipient is not attached to the caller, the message cannot be consumed by it. | -| Content | `field` (~254 bits) | The content of the message. This is the data that will be passed to the recipient. The content is limited to be a single field for rollup purposes. If the content is small enough it can just be passed along, otherwise it should be hashed and the hash passed along (you can use our [`Hash` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/Hash.sol) utilities with `sha256ToField` functions) | -| Secret Hash | `field` (~254 bits) | A hash of a secret that is used when consuming the message on L2. Keep this preimage a secret to make the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so make sure your app keeps track of the pre-images! Use [`computeSecretHash` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | -| ReturnValue | `bytes32` | The message hash, used as an identifier | +| Name | Type | Description | +| ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Recipient | [`L2Actor`](./data_structures.md#l2actor) | The recipient of the message. The recipient's version **MUST** match the inbox version and the actor must be an Aztec contract that is **attached** to the contract making this call. If the recipient is not attached to the caller, the message cannot be consumed by it. | +| Content | `field` (~254 bits) | The content of the message. This is the data that will be passed to the recipient. The content is limited to a single field for rollup purposes. If the content is small enough it can be passed directly, otherwise it should be hashed and the hash passed along (you can use our [`Hash`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/libraries/crypto/Hash.sol) utilities with `sha256ToField` functions). | +| Secret Hash | `field` (~254 bits) | A hash of a secret used when consuming the message on L2. Keep this preimage secret to make the consumption private. To consume the message the caller must know the pre-image (the value that was hashed). Use [`computeSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | +| ReturnValue | `(bytes32, uint256)` | The message hash (used as an identifier) and the leaf index in the tree. | #### Edge cases -- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient is larger than the field size (~254 bits). +- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient actor is larger than the field size (~254 bits). +- Will revert with `Inbox__VersionMismatch(uint256 expected, uint256 actual)` if the recipient version doesn't match the inbox version. - Will revert with `Inbox__ContentTooLarge(bytes32 content)` if the content is larger than the field size (~254 bits). - Will revert with `Inbox__SecretHashTooLarge(bytes32 secretHash)` if the secret hash is larger than the field size (~254 bits). +- Will revert with `Inbox__Ignition()` during the ignition phase (when the rollup's mana target is 0). + +## View functions + +These functions allow you to query the current state of the Inbox. + +| Function | Returns | Description | +| -------------------------- | ----------------- | ------------------------------------------------ | +| `getRoot(uint256)` | `bytes32` | Returns the root of a message tree for a given checkpoint number. | +| `getState()` | `InboxState` | Returns the current inbox state (rolling hash, total messages inserted, in-progress checkpoint). | +| `getTotalMessagesInserted()` | `uint64` | Returns the total number of messages inserted into the inbox. | +| `getInProgress()` | `uint64` | Returns the checkpoint number currently being filled. | +| `getFeeAssetPortal()` | `address` | Returns the address of the Fee Juice portal. | -## `consume()` +## Internal functions -Allows the `Rollup` to consume multiple messages in a single transaction. +:::note +The following functions are only callable by the Rollup contract and are documented here for completeness. +::: + +### `consume()` + +Consumes a message tree for a given checkpoint number. #include_code consume l1-contracts/src/core/interfaces/messagebridge/IInbox.sol solidity -| Name | Type | Description | -| -------------- | ----------- | -------------------------- | -| ReturnValue | `bytes32` | Root of the consumed tree. | +| Name | Type | Description | +| ----------- | --------- | ---------------------------------------- | +| _toConsume | `uint256` | The checkpoint number to consume. | +| ReturnValue | `bytes32` | The root of the consumed message tree. | #### Edge cases -- Will revert with `Inbox__Unauthorized()` if `msg.sender != ROLLUP` (rollup contract is sometimes referred to as state transitioner in the docs). +- Will revert with `Inbox__Unauthorized()` if `msg.sender != ROLLUP`. +- Will revert with `Inbox__MustBuildBeforeConsume()` if trying to consume a checkpoint that hasn't been built yet. + +## Related pages + +- [Outbox](./outbox.md) - L2 to L1 message passing +- [Data Structures](./data_structures.md) - Message and actor type definitions diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/index.md b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/index.md index 71d5ecb45aab..f2030ba9ae3d 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/index.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/index.md @@ -109,32 +109,14 @@ For the sake of cross-chain messages, this means inserting and nullifying L1 $\r While a message could theoretically be arbitrarily long, we want to limit the cost of the insertion on L1 as much as possible. Therefore, we allow the users to send 32 bytes of "content" between L1 and L2. If 32 suffices, no packing required. If the 32 is too "small" for the message directly, the sender should simply pass along a `sha256(content)` instead of the content directly (note that this hash should fit in a field element which is ~254 bits. More info on this below). The content can then either be emitted as an event on L2 or kept by the sender, who should then be the only entity that can "unpack" the message. In this manner, there is some way to "unpack" the content on the receiving domain. -The message that is passed along, require the `sender/recipient` pair to be communicated as well (we need to know who should receive the message and be able to check). By having the pending messages be a contract on L1, we can ensure that the `sender = msg.sender` and let only `content` and `recipient` be provided by the caller. Summing up, we can use the structs seen below, and only store the commitment (`sha256(LxToLyMsg)`) on chain or in the trees, this way, we need only update a single storage slot per message. - -```solidity -struct L1Actor { - address: actor, - uint256: chainId, -} - -struct L2Actor { - bytes32: actor, - uint256: version, -} - -struct L1ToL2Msg { - L1Actor: sender, - L2Actor: recipient, - bytes32: content, - bytes32: secretHash, -} - -struct L2ToL1Msg { - L2Actor: sender, - L1Actor: recipient, - bytes32: content, -} -``` +The message that is passed along requires the `sender/recipient` pair to be communicated as well (we need to know who should receive the message and be able to check). By having the pending messages be a contract on L1, we can ensure that the `sender = msg.sender` and let only `content` and `recipient` be provided by the caller. We only store the commitment (`sha256(LxToLyMsg)`) on chain or in the trees, so we only need to update a single storage slot per message. + +The message structures are: +- `L1Actor` and `L2Actor` - Represent entities on L1 and L2 respectively +- `L1ToL2Msg` - Message sent from L1 to L2 +- `L2ToL1Msg` - Message sent from L2 to L1 + +See the [Data Structures](./data_structures.md) page for the full Solidity definitions. :::info The `bytes32` elements for `content` and `secretHash` hold values that must fit in a field element (~ 254 bits). diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/outbox.md b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/outbox.md index 5e00124a8646..7ba777fd463f 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/outbox.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/outbox.md @@ -4,27 +4,25 @@ description: Learn about the outbox mechanism in Aztec portals for sending messa tags: [portals, contracts] --- -The `Outbox` is a contract deployed on L1 that handles message passing from the rollup and to L1. +The `Outbox` is a contract deployed on L1 that handles message passing from L2 to L1. Portal contracts call `consume()` to receive and process messages that were sent from L2 contracts. The Rollup contract inserts message roots via `insert()` when checkpoints are proven. -**Links**: [Interface (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Outbox.sol). +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/l1-contracts/src/core/messagebridge/Outbox.sol). ## `insert()` -Inserts the root of a merkle tree containing all of the L2 to L1 messages in a checkpoint specified by checkpointNumber. +Inserts the root of a merkle tree containing all of the L2 to L1 messages in a checkpoint. This function is only callable by the Rollup contract. #include_code outbox_insert l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | ------------------- | --------- | ---------------------------------------------------------------------- | -| `_checkpointNumber` | `uint256` | The checkpoint Number in which the L2 to L1 messages reside | +| `_checkpointNumber` | `uint256` | The checkpoint number in which the L2 to L1 messages reside | | `_root` | `bytes32` | The merkle root of the tree where all the L2 to L1 messages are leaves | -| `_minHeight` | `uint256` | The minimum height of the merkle tree that the root corresponds to | -#### Edge cases +### Edge cases - Will revert with `Outbox__Unauthorized()` if `msg.sender != ROLLUP_CONTRACT`. -- Will revert with `Errors.Outbox__RootAlreadySetAtCheckpoint(uint256 checkpointNumber)` if the root for the specific checkpoint has already been set. -- Will revert with `Errors.Outbox__InsertingInvalidRoot()` if the rollup is trying to insert bytes32(0) as the root. +- Will revert with `Outbox__CheckpointAlreadyProven(uint256 checkpointNumber)` if the checkpoint has already been proven. ## `consume()` @@ -34,31 +32,55 @@ Allows a recipient to consume a message from the `Outbox`. | Name | Type | Description | | ------------------- | ----------- | -------------------------------------------------------------------------------------------- | -| `_message` | `L2ToL1Msg` | The L2 to L1 message we want to consume | -| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the message we want to consume | +| `_message` | `L2ToL1Msg` | The L2 to L1 message to consume | +| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the message to consume | | `_leafIndex` | `uint256` | The index inside the merkle tree where the message is located | -| `_path` | `bytes32[]` | The sibling path used to prove inclusion of the message, the \_path length directly depends | +| `_path` | `bytes32[]` | The sibling path used to prove inclusion of the message | -#### Edge cases +### Edge cases -- Will revert with `Outbox__InvalidRecipient(address expected, address actual);` if `msg.sender != _message.recipient.actor`. +- Will revert with `Outbox__PathTooLong()` if the path length is >= 256. +- Will revert with `Outbox__LeafIndexOutOfBounds(uint256 leafIndex, uint256 pathLength)` if the leaf index exceeds the tree capacity for the given path length. +- Will revert with `Outbox__CheckpointNotProven(uint256 checkpointNumber)` if the checkpoint has not been proven yet. +- Will revert with `Outbox__VersionMismatch(uint256 expected, uint256 actual)` if the message version does not match the Outbox version. +- Will revert with `Outbox__InvalidRecipient(address expected, address actual)` if `msg.sender != _message.recipient.actor`. - Will revert with `Outbox__InvalidChainId()` if `block.chainid != _message.recipient.chainId`. -- Will revert with `Outbox__NothingToConsumeAtCheckpoint(uint256 checkpointNumber)` if the root for the checkpoint has not been set yet. -- Will revert with `Outbox__AlreadyNullified(uint256 checkpointNumber, uint256 leafIndex)` if the message at leafIndex for the checkpoint has already been consumed. -- Will revert with `Outbox__InvalidPathLength(uint256 expected, uint256 actual)` if the supplied height is less than the existing minimum height of the L2 to L1 message tree, or the supplied height is greater than the maximum (minimum height + log2(maximum messages)). -- Will revert with `MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex)` if unable to verify the message existence in the tree. It returns the message as a leaf, as well as the index of the leaf to expose more info about the error. +- Will revert with `Outbox__NothingToConsumeAtCheckpoint(uint256 checkpointNumber)` if the root for the checkpoint has not been set. +- Will revert with `Outbox__AlreadyNullified(uint256 checkpointNumber, uint256 leafIndex)` if the message has already been consumed. +- Will revert with `MerkleLib__InvalidIndexForPathLength()` if the leaf index has bits set beyond the tree height. +- Will revert with `MerkleLib__InvalidRoot(bytes32 expected, bytes32 actual, bytes32 leaf, uint256 leafIndex)` if the merkle proof verification fails. -## `hasMessageBeenConsumedAtCheckpointAndIndex()` +## `hasMessageBeenConsumedAtCheckpoint()` -Checks to see if an index of the L2 to L1 message tree for a specific checkpoint has been consumed. +Checks if an L2 to L1 message in a specific checkpoint has been consumed. #include_code outbox_has_message_been_consumed_at_checkpoint_and_index l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol solidity | Name | Type | Description | | ------------------- | --------- | ------------------------------------------------------------------------------------------------------- | -| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the index of the message we want to check | -| `_leafIndex` | `uint256` | The index of the message inside the merkle tree | +| `_checkpointNumber` | `uint256` | The checkpoint number specifying the checkpoint that contains the message to check | +| `_leafId` | `uint256` | The unique id of the message leaf | -#### Edge cases +### Edge cases - This function does not throw. Out-of-bounds access is considered valid, but will always return false. + +## `getRootData()` + +Returns the merkle root for a given checkpoint number. Returns `bytes32(0)` if the checkpoint has not been proven. + +```solidity +function getRootData(uint256 _checkpointNumber) external view returns (bytes32); +``` + +| Name | Type | Description | +| ------------------- | --------- | ------------------------------------------------ | +| `_checkpointNumber` | `uint256` | The checkpoint number to fetch the root data for | + +**Returns**: The merkle root of the L2 to L1 message tree for the checkpoint, or `bytes32(0)` if not proven. + +## Related pages + +- [Inbox](./inbox.md) - L1 to L2 message passing +- [Data Structures](./data_structures.md) - Message struct definitions +- [L1-L2 Communication (Portals)](./index.md) - Overview of cross-chain messaging diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/registry.md b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/registry.md index 297fcf233644..1ee4e8d2beeb 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/registry.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/ethereum-aztec-messaging/registry.md @@ -30,10 +30,10 @@ Retrieves the current rollup contract. ## `getRollup(uint256 _version)` -Retrieves the rollup contract for a specfic version. +Retrieves the rollup contract for a specific version. #include_code registry_get_rollup l1-contracts/src/governance/interfaces/IRegistry.sol solidity -| Name | Description | -| ----------- | ------------------ | -| ReturnValue | The current rollup | +| Name | Description | +| ----------- | ------------------------------------ | +| ReturnValue | The rollup for the specified version | diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md index 30331e77aa51..5a9fab7f8ae0 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/functions/attributes.md @@ -1,13 +1,33 @@ --- -title: Function Attributes and Macros +title: Attributes and Macros sidebar_position: 6 tags: [functions] -description: Explore function attributes in Aztec contracts that control visibility, mutability, and execution context. +description: Reference for Aztec contract attributes that control function visibility, execution context, storage, and notes. --- -On this page you will learn about function attributes and macros. - -If you are looking for a reference of function macros, go [here](../macros.md). +This page documents the attributes (macros) available in Aztec.nr for defining contract functions, storage, and notes. + +## Quick reference + +| Attribute | Applies to | Purpose | +|-----------|-----------|---------| +| `#[external("private")]` | functions | Client-side private execution with proofs | +| `#[external("public")]` | functions | Sequencer-side public execution | +| `#[external("utility")]` | functions | Unconstrained queries, not included in transactions | +| `#[internal("private")]` | functions | Private helper functions, inlined at call sites | +| `#[internal("public")]` | functions | Public helper functions, inlined at call sites | +| `#[view]` | functions | Prevents state modification | +| `#[initializer]` | functions | Contract constructor | +| `#[noinitcheck]` | functions | Callable before contract initialization | +| `#[nophasecheck]` | functions | Skips transaction phase validation | +| `#[only_self]` | functions | Only callable by the same contract | +| `#[authorize_once]` | functions | Requires authwit authorization with replay protection | +| `#[note]` | structs | Defines a private note type | +| `#[custom_note]` | structs | Defines a note with custom hash/nullifier logic | +| `#[storage]` | structs | Defines contract storage layout | +| `#[storage_no_init]` | structs | Storage with manual slot allocation | + +For macro internals, see the [macros reference](../macros.md). # External functions #[external("...")] @@ -24,84 +44,25 @@ A private function operates on private information, and is executed by the user If you are interested in what exactly the macros are doing we encourage you to run `nargo expand` on your contract. This will display your contract's code after the transformations are performed. -(If you are using VSCode you can display the expanded code by pressing `CMD + Shift + P` and typing `nargo expand` and selecting `Noir: nargo expand on current package.) - -#### The expansion broken down - -Viewing the expanded Aztec contract uncovers a lot about how Aztec contracts interact with the kernel. To aid with developing intuition, we will break down each inserted line. - -**Receiving context from the kernel.** - -Private function calls are able to interact with each other through orchestration from within the kernel circuits. The kernel circuit forwards information to each contract function (recall each contract function is a circuit). This information then becomes part of the private context. -For example, within each private function we can access some global variables. To access them we can call on the `self.context`, e.g. `self.context.chain_id()`. The value of the chain ID comes from the values passed into the circuit from the kernel. - -The kernel checks that all of the values passed to each circuit in a function call are the same. - -**Creating the function's `self.`** - -Each Aztec function has access to a `self` object. Upon creation it accepts storage and context. Context is initialized from the inputs provided by the kernel, and a hash of the function's inputs. - -We use the kernel to pass information between circuits. This means that the return values of functions must also be passed to the kernel (where they can be later passed on to another function). -We achieve this by pushing return values to the execution context, which we then pass to the kernel. - -**Hashing the function inputs.** - -Inside the kernel circuits, the inputs to functions are reduced to a single value; the inputs hash. This prevents the need for multiple different kernel circuits; each supporting differing numbers of inputs. Hashing the inputs allows to reduce all of the inputs to a single value. +(If you are using VSCode you can display the expanded code by pressing `CMD + Shift + P` and typing `nargo expand` and selecting `Noir: nargo expand on current package`.) -**Returning the context to the kernel.** +Under the hood, the macro: -The contract function must return information about the execution back to the kernel. This is done through a rigid structure we call the `PrivateCircuitPublicInputs`. - -> _Why is it called the `PrivateCircuitPublicInputs`?_ -> When verifying zk programs, return values are not computed at verification runtime, rather expected return values are provided as inputs and checked for correctness. Hence, the return values are considered public inputs. - -This structure contains a host of information about the executed program. It will contain any newly created nullifiers, any messages to be sent to l2 and most importantly it will contain the return values of the function. - -**Making the contract's storage available** - -Each `self` has a `storage` variable exposed on it. -When a `Storage` struct is declared within a contract, the `self.storage` contains real variables. - -If Storage is note defined `self.storage` contains only a placeholder value. - -Any state variables declared in the `Storage` struct can now be accessed as normal struct members. - -**Returning the function context to the kernel.** - -This function takes the application context, and converts it into the `PrivateCircuitPublicInputs` structure. This structure is then passed to the kernel circuit. +- Creates a `PrivateContext` from kernel-provided inputs (chain ID, block data, etc.) +- Initializes the `self` object with context and storage +- Hashes function inputs for the kernel (enabling variable argument counts) +- Returns execution results via `PrivateCircuitPublicInputs` (nullifiers, messages, return values) ## Utility functions #[external("utility")] -Contract functions marked with `#[external("utility")]` are used to perform state queries from an offchain client (from both private and public state!) or to modify local contract-related PXE state (e.g. when processing logs in Aztec.nr), and are never included in any transaction. No guarantees are made on the correctness of the result since the entire execution is unconstrained and heavily reliant on [oracle calls](https://noir-lang.org/docs/explainers/explainer-oracle). - -Any programming language could be used to construct these queries, since all they do is perform arbitrary computation on data that is either publicly available from any node, or locally available from the PXE. Utility functions exist as Noir contract code because they let developers utilize the rest of the contract code directly by being part of the same Noir crate, and e.g. use the same libraries, structs, etc. instead of having to rely on manual computation of storage slots, struct layout and padding, and so on. - -A reasonable mental model for them is that of a Solidity `view` function that can never be called in any transaction, and is only ever invoked via `eth_call`. Note that in these the caller assumes that the node is acting honestly by executing the true contract bytecode with correct blockchain state, the same way the Aztec version assumes the oracles are returning legitimate data. Unlike `view` functions however, `utility` functions can modify local offchain PXE state via oracle calls - this can be leveraged for example to process messages delivered offchain and then notify PXE of newly discovered notes. +Utility functions perform state queries from an offchain client and are never included in transactions. They can access both private and public state, and can modify local PXE state (e.g., processing logs). Since execution is unconstrained and relies on [oracle calls](https://noir-lang.org/docs/explainers/explainer-oracle), no guarantees are made on result correctness. -When a utility function is called, it prompts the ACIR simulator to - -1. generate the execution environment -2. execute the function within this environment - -To generate the environment, the simulator gets the block header from the [PXE database](../../../foundational-topics/pxe/index.md#database) and passes it along with the contract address to `UtilityExecutionOracle`. This creates a context that simulates the state of the blockchain at a specific block, allowing the utility function to access and interact with blockchain data as it would appear in that block, but without affecting the actual blockchain state. - -Once the execution environment is created, `runUtility` function is invoked on the simulator: - -#include_code execute_utility_function yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts typescript - -This: - -1. Prepares the ACIR for execution -2. Converts `args` into a format suitable for the ACVM (Abstract Circuit Virtual Machine), creating an initial witness (witness = set of inputs required to compute the function). `args` might be an oracle to request a user's balance -3. Executes the function in the ACVM, which involves running the ACIR with the initial witness and the context. If requesting a user's balance, this would query the balance from the PXE database -4. Extracts the return values from the `partialWitness` and decodes them based on the artifact to get the final function output. The artifact is the compiled output of the contract, and has information like the function signature, parameter types, and return types - -Beyond using them inside your other functions, they are convenient for providing an interface that reads storage, applies logic and returns values to a UI or test. Below is a snippet from exposing the `balance_of_private` function from a token implementation, which allows a user to easily read their balance, similar to the `balanceOf` function in the ERC20 standard. +A reasonable mental model is a Solidity `view` function that can only be invoked via `eth_call`, never in a transaction. Unlike Solidity `view` functions, utility functions can also modify local offchain PXE state. #include_code balance_of_private /noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust :::info -Note, that utility functions can have access to both private and (historical) public data when executed on the user's device. This is possible since these functions are not invoked as part of transactions, so we don't need to worry about preventing a contract from e.g. accidentally using stale or unverified public state. +Utility functions can access both private and historical public data since they're not part of transactions—there's no risk of using stale or unverified state. ::: ## Public functions #[external("public")] @@ -116,25 +77,14 @@ To create a public function you can annotate it with the `#[external("public")]` #include_code set_minter /noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust -Under the hood: - -- Context Creation: The macro inserts code at the beginning of the function to create a`PublicContext` object: - -```rust -let mut context = PublicContext::new(args_hasher); -``` +Under the hood, the macro: -This context provides access to public state and transaction information +- Creates a `PublicContext` object that provides access to public state and transaction information +- Initializes the storage struct if one is defined +- Wraps the function body in a scope that handles context setup and return values +- Marks the function as `pub` and `unconstrained`, meaning it doesn't generate proofs and is executed directly by the sequencer -- Storage Access: If the contract has a storage struct defined, the macro inserts code to initialize the storage: - -```rust -let storage = Storage::init(&mut context); -``` - -- Function Body Wrapping: The original function body is wrapped in a new scope that handles the context and return value -- Visibility Control: The function is marked as pub, making it accessible from outside the contract. -- Unconstrained Execution: Public functions are marked as unconstrained, meaning they don't generate proofs and are executed directly by the sequencer. +To see the exact generated code, run `nargo expand` on your contract. ## Constrained `view` Functions #[view] @@ -175,165 +125,160 @@ It is also useful in private functions when dealing with tasks of an unknown siz This macro inserts a check at the beginning of the function to ensure that the caller is the contract itself. This is done by adding the following assertion: ```rust -assert(self.msg_sender() == self.address, "Function can only be called by the same contract"); +assert(self.msg_sender().unwrap() == self.address, "Function can only be called internally"); ``` -## Implementing notes +## #[nophasecheck] -The `#[note]` attribute is used to define notes in Aztec contracts. +Private functions normally include a check to validate the current transaction phase. The `#[nophasecheck]` attribute skips this validation, allowing the function to handle phase transitions internally. -When a struct is annotated with `#[note]`, the Aztec macro applies a series of transformations and generates implementations to turn it into a note that can be used in contracts to store private data. +This is primarily used in account contract entrypoints that need to handle fee payment methods spanning multiple phases: + +```rust +#[external("private")] +#[nophasecheck] +fn entrypoint(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) { + // Handle different fee payment methods that may span phases +} +``` -1. **Note Interface Implementation**: The macro automatically implements the `NoteType`, `NoteHash` and `Packable` traits for the annotated struct. This includes the following methods: +## #[authorize_once] - - `get_id` - - `compute_note_hash` - - `compute_nullifier` - - `pack` - - `unpack` +The `#[authorize_once]` attribute enables authorization checks via the [authwit mechanism](../../../foundational-topics/advanced/authwit.md) with replay protection. Use this when a function performs actions on behalf of someone who is not the caller. -2. **Property Metadata**: A separate struct is generated to describe the note's fields, which is used for efficient retrieval of note data +```rust +#[authorize_once("from", "authwit_nonce")] +#[external("public")] +fn transfer_in_public(from: AztecAddress, to: AztecAddress, amount: u128, authwit_nonce: Field) { + // Transfer tokens from 'from' to 'to' +} +``` -3. **Export Information**: The note type and its ID are automatically exported +The macro: +- Verifies the caller is authorized to act on behalf of the `from` address +- Emits the authorization request as an offchain effect for wallet verification +- Consumes a nullifier with the provided nonce, preventing replay attacks -### Before expansion +## Internal functions #[internal("...")] -Here is how you could define a custom note: +Internal functions are callable only from within the same contract and are inlined at call sites (like Solidity's internal functions). Unlike `#[only_self]`, they don't create a separate call—the code is directly inserted where called. ```rust -#[note] -struct CustomNote { - data: Field, - owner: Address, +#[internal("private")] +fn _prepare_private_balance_increase(to: AztecAddress) -> PartialNote { + // Helper logic for private balance operations +} + +#[internal("public")] +fn _finalize_transfer(from: AztecAddress, amount: u128) { + // Helper logic for public finalization } ``` -### After expansion +Call internal functions via `self.internal`: ```rust -impl NoteType for CustomNote { - fn get_id() -> Field { - // Assigned by macros by incrementing a counter - 2 - } -} +let partial = self.internal._prepare_private_balance_increase(recipient); +``` -impl NoteHash for CustomNote { - fn compute_note_hash(self, storage_slot: Field) -> Field { - let inputs = array_concat(self.pack(), [storage_slot]); - poseidon2_hash_with_separator(inputs, DOM_SEP__NOTE_HASH) - } +Key differences from `#[only_self]`: +- **Inlined**: Code is inserted at call site, not a separate circuit/call +- **Private internal**: Can only be called from private external or internal functions +- **Public internal**: Can only be called from public external or internal functions - fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullification: Field) -> Field { - let owner_npk_m_hash = get_public_keys(self.owner).npk_m.hash(); - let secret = context.request_nsk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [ - note_hash_for_nullification, - secret - ], - DOM_SEP__NOTE_NULLIFIER as Field - ) - } +## Implementing notes - unconstrained fn compute_nullifier_unconstrained(self, storage_slot: Field, contract_address: AztecAddress, note_nonce: Field) -> Field { - // We set the note_hash_counter to 0 as the note is not transient and the concept of transient note does - // not make sense in an unconstrained context. - let retrieved_note = RetrievedNote { note: self, contract_address, nonce: note_nonce, note_hash_counter: 0 }; - let note_hash_for_nullification = compute_note_hash_for_nullification(retrieved_note, storage_slot); - let owner_npk_m_hash = get_public_keys(self.owner).npk_m.hash(); - let secret = get_nsk_app(owner_npk_m_hash); - poseidon2_hash_with_separator( - [ - note_hash_for_nullification, - secret - ], - DOM_SEP__NOTE_NULLIFIER as Field - ) - } -} +The `#[note]` attribute is used to define notes in Aztec contracts. -impl CustomNote { - pub fn new(x: [u8; 32], y: [u8; 32], owner: AztecAddress) -> Self { - CustomNote { x, y, owner } - } -} +When a struct is annotated with `#[note]`, the Aztec macro applies a series of transformations and generates implementations to turn it into a note that can be used in contracts to store private data. + +1. **NoteType trait**: Provides a unique identifier for the note type via `get_id()` -struct CustomNoteProperties { - data: aztec::note::note_getter_options::PropertySelector, - owner: aztec::note::note_getter_options::PropertySelector, +2. **NoteHash trait**: Implements note hash and nullifier computation: + - `compute_note_hash(self, owner, storage_slot, randomness)` - computes the note's hash + - `compute_nullifier(self, context, owner, note_hash_for_nullification)` - computes the nullifier using the owner's nullifying key + - `compute_nullifier_unconstrained(self, owner, note_hash_for_nullification)` - unconstrained version for use outside circuits + +3. **NoteProperties struct**: A separate struct is generated to describe the note's fields, which is used for efficient retrieval of note data + +### Example + +```rust +#[note] +struct CustomNote { + value: Field, } ``` +The `owner` is passed as a runtime parameter to the `compute_note_hash` and `compute_nullifier` functions, not stored as a field on the note. + +To see the exact generated code, run `nargo expand` on your contract. + Key things to keep in mind: -- Developers can override any of the auto-generated methods by specifying a note interface +- The note struct must implement or derive the `Packable` trait +- Developers can use `#[custom_note]` instead of `#[note]` to provide their own `NoteHash` implementation - The note's fields are automatically serialized and deserialized in the order they are defined in the struct ## Storage struct #[storage] The `#[storage]` attribute is used to define the storage structure for an Aztec contract. -When a struct is annotated with `#[storage]`, the macro does this under the hood: +When a struct is annotated with `#[storage]`, the macro: -1. **Context Injection**: injects a `Context` generic parameter into the storage struct and all its fields. This allows the storage to interact with the Aztec context, eg when using `context.msg_sender()` +1. **Context Injection**: Injects a `Context` generic parameter into the storage struct and all its fields, allowing storage to interact with the Aztec context -2. **Storage Implementation Generation**: generates an `impl` block for the storage struct with an `init` function. The developer can override this by implementing a `impl` block themselves +2. **Storage Implementation Generation**: Generates an `impl` block with an `init` function that initializes each storage variable with its assigned slot -3. **Storage Slot Assignment**: automatically assigns storage slots to each field in the struct based on their serialized length +3. **Storage Slot Assignment**: Automatically assigns storage slots to each field based on their serialized length -4. **Storage Layout Generation**: a `StorageLayout` struct and a global variable are generated to export the storage layout information for use in the contract artifact +4. **Storage Layout Generation**: Creates a `StorageLayout` struct exported via `#[abi(storage)]` for use in the contract artifact -### Before expansion +### Example ```rust #[storage] struct Storage { balance: PublicMutable, - owner: PublicMutable
, - token_map: Map, + owner: PublicMutable, + token_map: Map, } ``` -### After expansion +To see the exact generated code, run `nargo expand` on your contract. Alternatively, use `#[storage_no_init]` if you need manual control over storage slot allocation. + +Key things to keep in mind: + +- Only one storage struct can be defined per contract, and it must be named `Storage` +- `Map` types and private `Note` types always occupy a single storage slot + +## #[storage_no_init] + +The `#[storage_no_init]` attribute is an alternative to `#[storage]` that gives you manual control over storage slot allocation. Use this when you need custom slot assignments or want to maintain compatibility with existing storage layouts. + +With `#[storage_no_init]`, you must provide your own `init` function: ```rust +#[storage_no_init] struct Storage { balance: PublicMutable, - owner: PublicMutable, - token_map: Map, + owner: PublicMutable, } impl Storage { fn init(context: Context) -> Self { Storage { - balance: PublicMutable::new(context, 1), - owner: PublicMutable::new(context, 2), - token_map: Map::new(context, 3, |context, slot| Field::new(context, slot)), + balance: PublicMutable::new(context, 1), // Explicit slot assignment + owner: PublicMutable::new(context, 5), // Non-sequential slot } } } - -struct StorageLayout { - balance: dep::aztec::prelude::Storable, - owner: dep::aztec::prelude::Storable, - token_map: dep::aztec::prelude::Storable, -} - -#[abi(storage)] -global CONTRACT_NAME_STORAGE_LAYOUT = StorageLayout { - balance: dep::aztec::prelude::Storable { slot: 1 }, - owner: dep::aztec::prelude::Storable { slot: 2 }, - token_map: dep::aztec::prelude::Storable { slot: 3 }, -}; ``` -Key things to keep in mind: - -- Only one storage struct can be defined per contract -- `Map` types and private `Note` types always occupy a single storage slot +Unlike `#[storage]`, this macro does not generate: +- The `init` function (you must implement it) +- The `StorageLayout` struct for the contract artifact ## Further reading - [Macros reference](../macros.md) -- [How do macros work](./attributes.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/context.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/context.md index 9724764633e3..3d01e4ff6a09 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/context.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/functions/context.md @@ -11,7 +11,7 @@ import Image from '@theme/IdealImage'; The context is an object that is made available within every function in `Aztec.nr`. As mentioned in the [kernel circuit documentation](../../../foundational-topics/advanced/circuits/private_kernel.md). At the beginning of a function's execution, the context contains all of the kernel information that application needs to execute. During the lifecycle of a transaction, the function will update the context with each of its side effects (created notes, nullifiers etc.). At the end of a function's execution the mutated context is returned to the kernel to be checked for validity. -Behind the scenes, Aztec.nr will pass data the kernel needs to and from a circuit, this is abstracted away from the developer. In a developer's eyes; the context is a useful structure that allows access and mutate the state of the `Aztec` blockchain. +Behind the scenes, Aztec.nr will pass data the kernel needs to and from a circuit, this is abstracted away from the developer. In a developer's eyes, the context is a useful structure that allows you to access and mutate the state of the Aztec blockchain. On this page, you'll learn @@ -28,7 +28,7 @@ The `Aztec` blockchain contains two environments - public and private. - Private, for private transactions taking place on user's devices. - Public, for public transactions taking place on the network's sequencers. -As there are two distinct execution environments, they both require slightly differing execution contexts. Despite their differences; the API's for interacting with each are unified. Leading to minimal context switch when working between the two environments. +As there are two distinct execution environments, they both require slightly differing execution contexts. Despite their differences, the APIs for interacting with each are unified. Leading to minimal context switch when working between the two environments. The following section will cover both contexts. @@ -104,7 +104,7 @@ A transaction that sets this value will never be included in a block with a time ### Read Requests - +Read requests are used to prove that certain notes existed at a specific point in time. When a private function reads a note, it generates a read request that gets validated by the kernel circuit to ensure the note was valid at the time of the transaction. ### New Note Hashes diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md index 59b1e17c9c5f..dad7fa393ccd 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/functions/function_transforms.md @@ -5,236 +5,169 @@ tags: [functions] description: Understand how Aztec transforms contract functions during compilation for privacy and efficiency. --- -Below, we go more into depth of what is happening under the hood when you create a function in an Aztec contract. The [next page](./attributes.md) will give you more information about what the attributes are really doing. +This page explains what happens under the hood when you create a function in an Aztec contract. The [next page](./attributes.md) covers what the function attributes do. +## Overview + +Private functions in Aztec compile to standalone circuits that must conform to the protocol's kernel circuit interface. Public functions compile to AVM bytecode. The transformations described below bridge the gap between developer-friendly Aztec.nr syntax and these underlying requirements. + +Utility functions (marked with `#[utility]`) do not undergo these transformations—they remain as regular Noir functions. ## Function transformation -When you define a function in an Aztec contract, it undergoes several transformations when it is compiled. These transformations prepare the function for execution. These transformations include: +When you define a private or public function in an Aztec contract, it undergoes several transformations during compilation: - [Creating a context for the function](#context-creation) - [Handling function inputs](#private-and-public-input-injection) - [Processing return values](#return-value-handling) -- [Generating function signatures](#function-signature-generation) -- [Generating contract artifacts](#contract-artifacts) - -Let's explore each of these transformations in detail. ## Context creation -Every function in an Aztec contract operates within a specific context which provides some extra information and functionality. This is either a `PrivateContext` or `PublicContext` object, depending on whether it is a private or public function. For private functions, it creates a hash of all input parameters to ensure privacy. +Every function in an Aztec contract operates within a specific context that provides execution information and functionality. This is either a `PrivateContext` or `PublicContext` object, depending on whether it is a private or public function. ### Private functions -For private functions, the context creation involves hashing all input parameters: +For private functions, context creation involves serializing and hashing all input parameters: ```rust -let mut args_hasher = ArgsHasher::new(); -// Hash each parameter -args_hasher.add(param1); -args_hasher.add(param2); -// add all parameters +// Parameters are serialized into an array +let serialized_args: [Field; N] = /* serialized parameters */; + +// Hash the arguments using poseidon2 +let args_hash = aztec::hash::hash_args(serialized_args); -let mut context = PrivateContext::new(inputs, args_hasher.hash()); +// Create the context with the inputs and args hash +let mut context = PrivateContext::new(inputs, args_hash); ``` -This hashing process is important because it is used to verify the function's execution without exposing the input data. +This hashing is important because the kernel circuit uses it to verify the function received the correct parameters without exposing the input data. ### Public functions -For public functions, context creation is simpler: +For public functions, context creation uses a lazy evaluation pattern: ```rust -let mut context = PublicContext::new(inputs); +let mut context = PublicContext::new(|| { + // compute args hash when needed + hash_args(serialized_args) +}); ``` -These `inputs` are explained in the [private and public input injection](#private-and-public-input-injection) further down on this page. +### Using the context -### Using the context in functions +The context object provides methods for interacting with the blockchain. Storage access and contract calls are handled through a `ContractSelf` wrapper that the macros generate automatically. -Once created, the context object provides various useful methods. Here are some common use cases: +## Private and public input injection + +An additional parameter is automatically added to every private function. -#### Accessing storage +The injected input is always the first parameter of the transformed function and is of type `PrivateContextInputs` for private functions. -The context allows you to interact with contract storage. eg if you have a function that calls storage like this: +Original function definition: ```rust -let sender_balance = storage.balances.at(owner); +fn my_function(param1: Type1, param2: Type2) { ... } ``` -This calls the context to read from the appropriate storage slot. - -#### Interacting with other contracts - -The context provides methods to call other contracts: +Transformed function with injected input: ```rust -let token_contract = TokenContract::at(token); +fn my_function(inputs: PrivateContextInputs, param1: Type1, param2: Type2) { ... } ``` -Under the hood, this creates a new instance of the contract interface with the specified address. +The `PrivateContextInputs` struct contains: -## Private and public input injection - -An additional parameter is automatically added to every function. - -The injected input is always the first parameter of the transformed function and is of type `PrivateContextInputs` for private functions or `PublicContextInputs` for public functions. - -Original function definition - ```rust - fn my_function(param1: Type1, param2: Type2) { ... } - ``` +- `call_context` - information about how the function was called (msg_sender, contract_address, function_selector, is_static_call) +- `anchor_block_header` - the historical block header used during private execution +- `tx_context` - transaction-level data (chain_id, version, gas_settings) +- `start_side_effect_counter` - the side effect counter at function entry -Transformed function with injected input - ```rust - fn my_function(inputs: PrivateContextInputs, param1: Type1, param2: Type2) { ... } - ``` +These inputs are made available through the `PrivateContext` object within your function. -The `inputs` parameter includes: - -- msg sender, ie the address of the account calling the function -- contract address -- chain ID -- block context, eg the block number & timestamp -- function selector of the function being called - -This makes these inputs available to be consumed within private annotated functions. +Public functions run in the AVM and access their context data through AVM opcodes rather than injected inputs. ## Return value handling -Return values in Aztec contracts are processed differently from traditional smart contracts when using private functions. +Return values in Aztec contracts are processed differently from traditional smart contracts. ### Private functions -- The original return value is assigned to a special variable: - ```rust - let macro__returned__values = original_return_expression; - ``` +For private functions, the return value is serialized, hashed, and stored in the context: -- A new `ArgsHasher` is created for the return values: - ```rust - let mut returns_hasher = ArgsHasher::new(); - ``` +```rust +// The original return value is captured +let macro__returned__values = original_return_expression; -- The hash of the return value is set in the context: - ```rust - context.set_return_hash(returns_hasher); - ``` +// The return value is serialized and hashed +let serialized_return: [Field; N] = /* serialized return value */; +self.context.set_return_hash(serialized_return); +``` -- The function's return type is changed to `PrivateCircuitPublicInputs`, which is returned by calling `context.finish()` at the end of the function. +The function's return type is changed to `PrivateCircuitPublicInputs`, which is returned by calling `context.finish()` at the end of the function. -This process allows the return values to be included in the function's computation result while maintaining privacy. +This process allows the return values to be included in the function's computation result while maintaining privacy. The actual return values are stored in the execution cache and can be retrieved by the caller using the hash. ### Public functions -In public functions, the return value is directly used, and the function's return type remains as specified by the developer. +In public functions, the return value is handled directly by the AVM and the function's return type remains as specified by the developer. ## Function signature generation -Unique function signatures are generated for each contract function. - -The function signature is computed like this: +Each contract function has a unique 4-byte function selector. The selector is computed by hashing the function's signature string using Poseidon2: ```rust -fn compute_fn_signature_hash(fn_name: &str, parameters: &[Type]) -> u32 { - let signature = format!( - "{}({})", - fn_name, - parameters.iter().map(signature_of_type).collect::>().join(",") - ); - let mut keccak = Keccak::v256(); - let mut result = [0u8; 32]; - keccak.update(signature.as_bytes()); - keccak.finalize(&mut result); - // Take the first 4 bytes of the hash and convert them to an integer - // If you change the following value you have to change NUM_BYTES_PER_NOTE_TYPE_ID in l1_note_payload.ts as well - let num_bytes_per_note_type_id = 4; - u32::from_be_bytes(result[0..num_bytes_per_note_type_id].try_into().unwrap()) +impl FunctionSelector { + pub fn from_signature(signature: str) -> Self { + let bytes = signature.as_bytes(); + let hash = poseidon2_hash_bytes(bytes); + // hash is truncated to fit within 32 bits (4 bytes) + FunctionSelector::from_field(hash) + } } ``` -- A string representation of the function is created, including the function name and parameter types -- This signature string is then hashed using Keccak-256 -- The first 4 bytes of the resulting hash are converted to a u32 integer - -### Integration into contract interface - -The computed function signatures are integrated into the contract interface like this: - -- During contract compilation, placeholder values (0) are initially used for function selectors +The signature string follows the format `function_name(param_types)`. For example, `transfer(Field,Field)`. -- After type checking, the `update_fn_signatures_in_contract_interface()` function is called to replace these placeholders with the actual computed signatures - -- For each function in the contract interface: - - The function's parameters are extracted - - The signature hash is computed using `compute_fn_signature_hash` - - The placeholder in the contract interface is replaced with the computed hash - -This process ensures that each function in the contract has a unique, deterministic signature based on its name and parameter types. They are inspired by Solidity's function selector mechanism. +This approach is inspired by Solidity's function selector mechanism, but uses Poseidon2 instead of Keccak-256 for compatibility with Aztec's circuit-friendly hash functions. ## Contract artifacts -Contract artifacts in Aztec are automatically generated structures that describe the contract's interface. They provide information about the contract's functions, their parameters, and return types. - -### Contract artifact generation process - -For each function in the contract, an artifact is generated like this: +Contract artifacts are automatically generated structures that describe the contract's interface. They preserve the original function signatures (parameters and return types) before macro transformations are applied. -- A struct is created to represent the function's parameters: +For each function in the contract, an ABI export is generated with: - ```rust - struct {function_name}_parameters { - // Function parameters are listed here - } - ``` +1. A parameters struct containing all function parameters +2. An ABI struct marked with `#[abi(functions)]` containing the parameters and return type - This struct is only created if the function has parameters. - -- An ABI struct is generated for the function: +For example, given a function: ```rust - let export_struct_source = format!( - " - #[abi(functions)] - struct {}_abi {{ - {}{} - }}", - func.name(), - parameters, - return_type - ); +fn increment(owner: AztecAddress) -> Field { ... } ``` -- These structs are added to the contract's types. - -### Content of artifacts - -The artifacts contain: - -- Function name -- Parameters (if any), including their names and types -- Return type (if the function has returns) - -For example, for a function `transfer(recipient: Address, amount: Field) -> bool`, the artifact would look like: +The following structs are generated: ```rust -struct transfer_parameters { - recipient: Address, - amount: Field, +pub struct increment_parameters { + pub owner: AztecAddress } #[abi(functions)] -struct transfer_abi { - parameters: transfer_parameters, - return_type: bool, +pub struct increment_abi { + parameters: increment_parameters, + return_type: Field } ``` -Contract artifacts are important because: +The `#[abi(functions)]` attribute marks the struct for inclusion in the contract ABI's `outputs.functions` array. This is important because macro processing changes the actual return type of private functions to `PrivateCircuitPublicInputs`, but the toolchain needs access to the original signatures. + +Contract artifacts enable: -- They provide a machine-readable description of the contract -- They can be used to generate bindings for interacting with the contract (read [here](../../../aztec-nr/how_to_compile_contract.md) to learn how to create TypeScript bindings) -- They help decode function return values in the simulator +- Machine-readable contract interface descriptions +- TypeScript binding generation (see [how to compile contracts](../../how_to_compile_contract.md)) +- Function return value decoding in the simulator ## Further reading + - [Function attributes and macros](./attributes.md) +- [Aztec.nr macro source code](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/macros) - for those who want to see the actual transformation implementation diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md index 6a493272ef14..7a0a27d59326 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md @@ -2,58 +2,57 @@ title: How to Define Functions sidebar_position: 1 tags: [functions, smart-contracts] -description: Define different types of functions in your Aztec smart contracts for various execution environments. +description: Define different types of functions in your Aztec contracts for private, public, and utility execution. --- +## Overview + This guide shows you how to define different types of functions in your Aztec contracts, each serving specific purposes and execution environments. +## Quick reference + +| Annotation | Execution | State access | +| ------------------------ | ----------------- | ------------------------------------------------------------ | +| `#[external("private")]` | User device | Private state (and selected public values via storage types) | +| `#[external("public")]` | Sequencer | Public state | +| `#[external("utility")]` | Offchain client | Public + private (unconstrained) | +| `#[internal("private")]` | N/A | Inlined private helper (non-entrypoint) | +| `#[internal("public")]` | N/A | Inlined public helper (non-entrypoint) | +| `#[view]` | Private or public | Read-only (no state mutation) | +| `#[only_self]` | Private or public | Callable only by the same contract | +| `#[initializer]` | Private or public | One-time initialization | + ## Prerequisites -- An Aztec contract project set up with `aztec-nr` dependency -- Basic understanding of Noir programming language -- Familiarity with Aztec's execution model (private vs public) +- An Aztec contract project set up with the `aztec-nr` dependency +- Basic understanding of [Noir programming language](https://noir-lang.org/docs) +- Familiarity with Aztec Protocol's [call types](../../../foundational-topics/call_types.md) (private vs public) ## Define private functions -Create functions that execute privately on user devices using the `#[external("private")]` annotation. For example: +Use `#[external("private")]` to create functions that execute privately on user devices. For example: -```rust -#[external("private")] -fn execute_private_action(param1: AztecAddress, param2: u128) { - // logic -} -``` +#include_code increment /docs/examples/contracts/counter_contract/src/main.nr rust -Private functions maintain privacy of user inputs and execution logic. Private functions only have access to private state. +Private functions run in a private context, can access private state, and can read certain public values through storage types like [`DelayedPublicMutable`](../how_to_define_storage.md#delayedpublicmutable). ## Define public functions -Create functions that execute on the sequencer using the `#[external("public")]` annotation: +Use `#[external("public")]` to create functions that execute on the sequencer: -```rust -#[external("public")] -fn create_item(recipient: AztecAddress, item_id: Field) { - // logic -} -``` +#include_code mint_public /docs/examples/contracts/bob_token_contract/src/main.nr rust -Public functions can access public state, similar to EVM contracts. Public functions do not have direct access to private state. +Public functions operate on public state, similar to EVM contracts. They can write to private storage, but any data written from a public function is publicly visible. ## Define utility functions -Create offchain query functions using the `#[external("utility")]` annotation. +Create offchain query functions using the `#[external("utility")]` annotation with `unconstrained`. -Utility functions are standalone unconstrained functions that cannot be called from private or public functions: they are meant to be called by _applications_ to perform auxiliary tasks: query contract state (e.g. a token balance), process messages received offchain, etc. Example: +Utility functions are standalone unconstrained functions that cannot be called from private or public functions. They are meant to be called by _applications_ to perform auxiliary tasks like querying contract state or processing offchain messages. Example: -```rust -#[external("utility")] -unconstrained fn get_private_items( - owner: AztecAddress, - page_index: u32, -) -> ([Field; MAX_NOTES_PER_PAGE], bool) { - // logic -} -``` +#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust + +Use `aztec.js` `simulate` to execute utility functions and read their return values. For details, see [Call Types](../../../foundational-topics/call_types.md#simulate). ## Define view functions @@ -68,32 +67,21 @@ fn get_config_value() -> Field { ``` View functions cannot modify contract state. They're akin to Ethereum's `view` functions. +`#[view]` only applies to `#[external("private")]` and `#[external("public")]` functions. ## Define only-self functions Create contract-only functions using the `#[only_self]` annotation: -```rust -#[external("public")] -#[only_self] -fn update_counter_public(item: Field) { - // logic -} -``` +#include_code _assert_is_owner /docs/examples/contracts/bob_token_contract/src/main.nr rust -Internal functions are only callable within the same contract. +Only-self functions are only callable by the same contract, which is useful when a private function enqueues a public call that should only be callable internally. ## Define initializer functions Create constructor-like functions using the `#[initializer]` annotation: -```rust -#[external("private")] -#[initializer] -fn constructor() { - // logic -} -``` +#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust ### Use multiple initializers @@ -103,21 +91,34 @@ Define multiple initialization options: 2. Choose which one to call during deployment 3. Any initializer marks the contract as initialized -## Create library methods +## Define internal functions -Define reusable contract logic as regular functions (no special annotation needed): +Create helper functions using `#[internal("private")]` or `#[internal("public")]`. Internal functions are inlined at call sites and do not create separate entrypoints: ```rust -#[contract_library_method] -fn process_value( - context: &mut PrivateContext, - storage: Storage<&mut PrivateContext>, - account: AztecAddress, - value: u128, - max_items: u32, -) -> u128 { - // logic +#[internal("private")] +fn _prepare_transfer(to: AztecAddress, amount: u128) -> Field { + // helper logic for private functions +} + +#[internal("public")] +fn _update_balance(owner: AztecAddress, amount: u128) { + // helper logic for public functions } ``` -Library methods are inlined when called and reduce code duplication. +Call internal functions via `self.internal`: + +```rust +let result = self.internal._prepare_transfer(recipient, amount); +``` + +Key constraints: + +- Private internal functions can only be called from private external or internal functions +- Public internal functions can only be called from public external or internal functions + +## Next steps + +- [Attributes and Macros](./attributes.md) +- [Call Types](../../../foundational-topics/call_types.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/public_private_calls.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/public_private_calls.md deleted file mode 100644 index e86298adaaf6..000000000000 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/public_private_calls.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Private <> Public Communication -sidebar_position: 7 -tags: [functions] -description: Learn how to make calls between public and private functions in Aztec contracts. ---- - - -import Image from "@theme/IdealImage"; - -import Disclaimer from "@site/src/components/Disclaimers/_wip_disclaimer.mdx"; - - - -Aztec operates on a model of private and public functions that are able to work together. Private functions work by providing evidence of correct execution generated locally through kernel proofs. Public functions, on the other hand, are able to utilize the latest state to manage updates and perform alterations. - -On this page, you’ll learn: - -- How private and public functions work -- The role of public functions in managing state alterations and updates -- Communication and interactions between private and public functions -- How the sequencer manages the order of operations of private functions - -### Objectives - -The goal for L2 communication is to setup the most simple mechanism that will support - -- _private_ and _public_ functions -- _private_ functions that can call _private_ or _public_ functions -- _public_ functions that can call _private_ or _public_ functions - -Before diving into the communication abstracts for Aztec, we need to understand some of our limitations. One being that public functions (as known from Ethereum) must operate on the current state to provide meaningful utility, e.g., at the tip. -This works fine when there is only one builder (sequencer) executing it first, and then others verifying as the builder always knows the tip. On the left in the diagram below, we see a block where the transactions are applied one after another each building on the state before it. For example, if Tx 1 update storage `a = 5`, then in Tx 2 reading `a` will return `5`. - -This works perfectly well when everything is public and a single builder is aware of all changes. However, in a private setting, we require the user to present evidence of correct execution as part of their transaction in the form of a kernel proof (generated locally on user device ahead of time). This way, the builder doesn't need to have knowledge of everything happening in the transaction, only the results. If we were to build this proof on the latest state, we would encounter problems. How can two different users build proofs at the same time, given that they will be executed one after the other by the sequencer? The simple answer is that they cannot, as race conditions would arise where one of the proofs would be invalidated by the other due to a change in the state root (which would nullify Merkle paths). - -To avoid this issue, we permit the use of historical data as long as the data has not been nullified previously. Note, that because this must include nullifiers that were inserted after the proof generation, but before execution we need to nullify (and insert the data again) to prove that it was not nullified. Without emitting the nullifier we would need our proof to point to the current head of the nullifier tree to have the same effect, e.g., back to the race conditions we were trying to avoid. - -In this model, instead of informing the builder of our intentions, we construct the proof $\pi$ and then provide them with the transaction results (new note hashes and nullifiers, contract deployments and cross-chain messages) in addition to $\pi$. The builder will then be responsible for inserting these new note hashes and nullifiers into the state. They will be aware of the intermediates and can discard transactions that try to produce existing nullifiers (double spend), as doing so would invalidate the rollup proof. - -On the left-hand side of the diagram below, we see the fully public world where storage is shared, while on the right-hand side, we see the private world where all reads are historical. - - - -Given that Aztec will comprise both private and public functions, it is imperative that we determine the optimal ordering for these functions. From a logical standpoint, it is reasonable to execute the private functions first as they are executed on a state $S_i$, where $i \le n$, with $S_n$ representing the current state where the public functions always operate on the current state $S_n$. Prioritizing the private functions would also afford us the added convenience of enabling them to invoke the public functions, which is particularly advantageous when implementing a peer-to-pool architecture such as that employed by Uniswap. - -Transactions that involve both private and public functions will follow a specific order of execution, wherein the private functions will be executed first, followed by the public functions, and then moving on to the next transaction. - -It is important to note that the execution of private functions is prioritized before executing any public functions. This means that private functions cannot "wait" on the results of any of their calls to public functions. Stated differently, any calls made across domains are unilateral in nature, akin to shouting into the void with the hope that something will occur at a later time. The figure below illustrates the order of function calls on the left-hand side, while the right-hand side shows how the functions will be executed. Notably, the second private function call is independent of the output of the public function and merely occurs after its execution. - - - -Multiple of these transactions are then ordered into a L2 block by the sequencer, who will also be executing the public functions (as they require the current head). Example seen below. - - - -:::info -Be mindful that if part of a transaction is reverting, say the public part of a call, it will revert the entire transaction. Similarly to Ethereum, it might be possible for the block builder to create a block such that your valid transaction reverts because of altered state, e.g., trade incurring too much slippage or the like. -::: - -To summarize: - -- _Private_ function calls are fully "prepared" and proven by the user, which provides the kernel proof along with new note hashes and nullifiers to the sequencer. -- _Public_ functions altering public state (updatable storage) must be executed at the current "head" of the chain, which only the sequencer can ensure, so these must be executed separately to the _private_ functions. -- _Private_ and _public_ functions within an Aztec transaction are therefore ordered such that first _private_ functions are executed, and then _public_. - -A more comprehensive overview of the interplay between private and public functions and their ability to manipulate data is presented below. It is worth noting that all data reads performed by private functions are historical in nature, and that private functions are not capable of modifying public storage. Conversely, public functions have the capacity to manipulate private storage (e.g., inserting new note hashes, potentially as part of transferring funds from the public domain to the private domain). - - - -:::info -You can think of private and public functions as being executed by two actors that can only communicate to each other by mailbox. -::: - -So, with private functions being able to call public functions (unilaterally) we had a way to go from private to public, what about the other way? Recall that public functions CANNOT call private functions directly. Instead, you can use the append-only merkle tree to save messages from a public function call, that can later be executed by a private function. Note, only a transaction coming after the one including the message from a public function can consume it. In practice this means that unless you are the sequencer it will not be within the same rollup. - -Given that private functions have the capability of calling public functions unilaterally, it is feasible to transition from a private to public function within the same transaction. However, the converse is not possible. To achieve this, the append-only merkle tree can be employed to save messages from a public function call, which can then be executed by a private function at a later point in time. It is crucial to reiterate that this can only occur at a later stage and cannot take place within the same rollup because the proof cannot be generated by the user. - -:::info -Theoretically the builder has all the state trees after the public function has inserted a message in the public tree, and is able to create a proof consuming those messages in the same block. But it requires pending UTXO's on a block-level. -::: - -From the above, we should have a decent idea about what private and public functions can do inside the L2, and how they might interact. - -## A note on L2 access control - -Many applications rely on some form of access control to function well. USDC have a blacklist, where only parties not on the list should be able to transfer. And other systems such as Aave have limits such that only the pool contract is able to mint debt tokens and transfers held funds. - -Access control like this cannot easily be enforced in the private domain, as reading is also nullifying (to ensure data is up to date). However, as it is possible to read historical public state, one can combine private and public functions to get the desired effect. - -This concept is known as delayed public mutable state, and relies on using delays when changing public data so that it can also be read in private with currentness guarantees. Since values cannot be immediately modified but instead require delays to elapse, it is possible to privately prove that an application is using the current value _as long as the transaction gets included before some time in the future_, which would be the earliest the value could possibly change. - -If the public state is only changed infrequently, and it is acceptable to have delays when doing so, then delayed public mutable state is a good solution to this problem. diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md b/docs/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md index 9cd2cd395b81..0e2bca1725b9 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/functions/visibility.md @@ -7,20 +7,49 @@ description: Understand function visibility modifiers in Aztec and how they affe In Aztec there are multiple different types of visibility that can be applied to functions. Namely we have `data visibility` and `function visibility`. This page explains these types of visibility. -### Data Visibility +## Data visibility -Data visibility is used to describe whether the data (or state) used in a function is generally accessible (public) or on a need to know basis (private). +Data visibility describes whether the data (or state) used in a function is generally accessible (public) or on a need-to-know basis (private). -### Function visibility +## Function visibility -This is the kind of visibility you are more used to seeing in Solidity and more traditional programming languages. It is used to describe whether a function is callable from other contracts, or only from within the same contract. +Function visibility describes whether a function is callable from other contracts, or only from within the same contract. This is similar to the visibility modifiers you may be familiar with from Solidity. -By default, all functions are callable from other contracts, similarly to the Solidity `public` visibility. To make them only callable from the contract itself, you can mark them as `internal`. Contrary to solidity, we don't have the `external` nor `private` keywords. `external` since it is limited usage when we don't support inheritance, and `private` since we don't support inheritance and it would also be confusing with multiple types of `private`. +### The `#[external(...)]` attribute -A good place to use `internal` is when you want a private function to be able to alter public state. As mentioned above, private functions cannot do this directly. They are able to call public functions and by making these internal we can ensure that this state manipulating function is only callable from our private function. +In Aztec.nr, the `#[external(...)]` attribute marks a function as externally callable - meaning it can be invoked via a transaction or by other contracts. The attribute takes a parameter specifying the execution context: + +- `#[external("private")]` - The function executes in a private context with access to private state +- `#[external("public")]` - The function executes in a public context with access to public state + +### The `#[only_self]` attribute + +By default, all external functions are callable from other contracts, similar to Solidity's `public` visibility. To restrict a function so it can only be called by the same contract, use the `#[only_self]` attribute: + +```rust +#[external("public")] +#[only_self] +fn _increase_public_balance(to: AztecAddress, amount: u128) { + // This function can only be called by this contract + let new_balance = self.storage.public_balances.at(to).read().add(amount); + self.storage.public_balances.at(to).write(new_balance); +} +``` + +A common use case for `#[only_self]` is when a private function needs to modify public state. Since private functions cannot directly modify public state, they enqueue calls to public functions. By marking the public function with `#[only_self]`, you ensure that only your contract can call it - preventing external parties from manipulating the public state directly. :::danger -Note that non-internal functions could be used directly as an entry-point, which currently means that the `msg_sender` would be `0`, so for now, using address `0` as a burn address is not recommended. You can learn more about this in the [Accounts concept page](../../../foundational-topics/accounts/keys.md). +Note that functions without `#[only_self]` can be used directly as an entry-point, which currently means that the `msg_sender` would be `0`. For this reason, using address `0` as a burn address is not recommended. You can learn more about this in the [Accounts concept page](../../../foundational-topics/accounts/keys.md). ::: +### The `#[internal]` attribute + +The `#[internal]` attribute is different from `#[only_self]`. While `#[only_self]` restricts _who_ can call a function (only the same contract, but still via an external call), `#[internal]` functions are **inlined** into the calling function. This is similar to how Solidity's `internal` functions use EVM's `JUMP` instruction rather than `CALL`. + +Internal functions: + +- Cannot be called externally (no transaction can invoke them directly) +- Are inlined at compile time into the functions that call them +- Have access to the calling function's context + To understand how visibility works under the hood, check out the [Inner Workings page](./attributes.md). diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/globals.md b/docs/docs-developers/docs/aztec-nr/framework-description/globals.md index 4c8633e2a2d3..e73396ed206c 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/globals.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/globals.md @@ -1,24 +1,26 @@ --- title: Global Variables -description: Documentation of Aztec's Global Variables in the Public and Private Contexts +description: Access chain ID, block number, timestamps, and gas information in your Aztec contracts sidebar_position: 9 --- # Global Variables -For developers coming from solidity, this concept will be similar to how the global `block` variable exposes a series of block values. The idea is the same in Aztec. Developers can access a namespace of values made available in each function. +Similar to Solidity's global `block` variable, Aztec exposes contextual values within each function via the `context` object. -`Aztec` has two execution environments, Private and Public. Each execution environment contains a different global variables object. +Aztec has two execution environments—Private and Public—each with different available globals. ## Private Global Variables +Private functions access transaction context via `TxContext`: + #include_code tx-context /noir-projects/noir-protocol-circuits/crates/types/src/abis/transaction/tx_context.nr rust -The private global variables are equal to the transaction context and contain: +The following fields are accessible via `context` methods: ### Chain Id -The chain id differs depending on which Aztec instance you are on ( NOT the Ethereum hardfork that the rollup is settling to ). On original deployment of the network, this value will be 1. +The unique identifier for the Aztec network instance (not the Ethereum chain the rollup settles to). ```rust context.chain_id(); @@ -26,7 +28,7 @@ context.chain_id(); ### Version -The version number indicates which Aztec hardfork you are on. The Genesis block of the network will have the version number 1. +The Aztec protocol version number. The genesis block has version 1. ```rust context.version(); @@ -34,17 +36,27 @@ context.version(); ### Gas Settings -The gas limits set by the user for the transaction, the max fee per gas, and the inclusion fee. +The gas limits, max fees per gas, and inclusion fee set by the user for the transaction. + +```rust +context.gas_settings(); +``` ## Public Global Variables +Public functions access block-level context via `GlobalVariables`: + #include_code global-variables /noir-projects/noir-protocol-circuits/crates/types/src/abis/global_variables.nr rust -The public global variables contain the values present in the `private global variables` described above, with the addition of: +:::note +Not all fields in `GlobalVariables` are exposed via context methods. The `coinbase`, `fee_recipient`, and `slot_number` fields are used internally by the protocol. +::: + +Public functions have access to `chain_id()` and `version()` (same syntax as private), plus the following block-level values: ### Timestamp -The timestamp is the unix timestamp in which the block has been executed. The value is provided by the block's proposer (therefore can have variance). This value will always increase. +The unix timestamp when the block is executed. Provided by the block proposer, so it may have slight variance. Always increases monotonically. ```rust context.timestamp(); @@ -52,14 +64,26 @@ context.timestamp(); ### Block Number -The block number is a sequential identifier that labels each individual block of the network. This value will be the block number of the block the accessing transaction is included in. -The block number of the genesis block will be 1, with the number increasing by 1 for every block after. +The sequential block identifier. Genesis block is 1, incrementing by 1 for each subsequent block. ```rust context.block_number(); ``` -:::info _Why do the available global variables differ per execution environment?_ -The global variables are constrained by the proving environment. In the case of public functions, they are executed on a sequencer that will know the timestamp and number of the next block ( as they are the block producer ). -In the case of private functions, we cannot be sure which block our transaction will be included in, hence we can not guarantee values for the timestamp or block number. +### Gas Fees + +The current L2 and DA gas prices for the block. You can access gas-related information via: + +```rust +context.l2_gas_left(); // Remaining L2 gas +context.da_gas_left(); // Remaining DA gas +context.base_fee_per_l2_gas(); // L2 gas price +context.base_fee_per_da_gas(); // DA gas price +context.transaction_fee(); // Final tx fee (only available in teardown phase) +``` + +:::info Why do available globals differ between environments? +Private functions execute on the user's device before the transaction is submitted, so they cannot know which block will include the transaction. Therefore, `timestamp` and `block_number` are unavailable in private context. + +Public functions execute on a sequencer who knows the current block's timestamp and number, making these values accessible. ::: diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_call_contracts.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_call_contracts.md index c75c6e8a8993..18cf76fba8c2 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_call_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_call_contracts.md @@ -5,70 +5,64 @@ tags: [functions, contracts, composability] description: Call functions in other contracts from your Aztec smart contracts to enable composability. --- -This guide shows you how to call functions in other contracts from your Aztec smart contracts, enabling contract composability and interaction. - -## Prerequisites - -- An Aztec contract project with dependencies properly configured -- Access to the target contract's source code or ABI -- Understanding of Aztec contract compilation and deployment +This guide shows you how to call functions in other contracts from your Aztec smart contracts. ## Add the target contract as a dependency Add the contract you want to call to your `Nargo.toml` dependencies: ```toml -other_contract = { git="https://github.com/your-repo/", tag="v1.0.0", directory="path/to/contract" } +[dependencies] +token = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-contracts/contracts/app/token_contract" } ``` -## Import the contract interface - -Import the contract at the top of your contract file: +Then import the contract interface at the top of your contract file: ```rust -use other_contract::OtherContract; +use token::Token; ``` ## Call contract functions -Use this pattern to call functions in other contracts: +Use `self.call()` to call functions on other contracts: + +```rust +self.call(Token::at(token_address).transfer(recipient, amount)); +``` + +The pattern is: -1. Specify the contract address: `Contract::at(contract_address)` -2. Call the function: `.function_name(param1, param2)` -3. Execute the call: `.call(&mut context)` +1. Form the call: `Contract::at(address).function_name(args)` +2. Execute it: `self.call(...)` or `self.view(...)` for read-only calls -### Make private function calls +### Private-to-private calls -Call private functions directly using `.call()`: +#include_code private_call /noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr rust + +### Public-to-public calls + +From a public function, call other public functions directly: ```rust -OtherContract::at(contract_address).private_function(param1, param2).call(&mut context); +self.call(Token::at(token_address).transfer_in_public(recipient, amount)); ``` -### Make public-to-public calls - -Call public functions from other public functions using `.call()`: +Capture return values by assigning the result: ```rust -let result = OtherContract::at(contract_address) - .public_function(param1, param2, param3) - .call(&mut context); +let balance = self.view(Token::at(token_address).balance_of_public(account)); ``` -### Make private-to-public calls +Use `self.view()` for read-only calls that cannot modify state. -Enqueue public functions to be executed after private execution completes: +### Private-to-public calls + +From a private function, enqueue public function calls for later execution: ```rust -OtherContract::at(contract_address) - .public_function(param1, param2) - .enqueue(&mut context); +self.enqueue(Token::at(token_address).mint_to_public(recipient, amount)); ``` :::info -Public functions always execute after private execution completes. Learn more in the [concepts overview](../../foundational-topics/index.md). +Public functions execute after all private execution completes. Return values are not available in the private context. Learn more about [call types](../../foundational-topics/call_types.md). ::: - -### Use other call types - -Explore additional call types for specialized use cases in the [call types reference](../../foundational-topics/call_types.md). diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_communicate_cross_chain.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_communicate_cross_chain.md index 44f88c4c961c..9916eb56d1ab 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_communicate_cross_chain.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_communicate_cross_chain.md @@ -5,188 +5,90 @@ sidebar_position: 12 description: Send messages and data between L1 and L2 contracts using portal contracts and cross-chain messaging. --- -This guide shows you how to implement cross-chain communication between Ethereum (L1) and Aztec (L2) contracts using portal contracts. +This guide covers cross-chain communication between Ethereum (L1) and Aztec (L2) using portal contracts. + +Aztec uses an Inbox/Outbox pattern for cross-chain messaging. Messages sent from L1 are inserted into the `Inbox` contract and later consumed on L2. Messages sent from L2 are inserted into the `Outbox` contract and later consumed on L1. Portal contracts are L1 contracts that facilitate this communication for your application. ## Prerequisites -- An Aztec contract project set up with `aztec-nr` dependency -- Understanding of Aztec L1/L2 architecture +- An Aztec contract project with `aztec-nr` dependency - Access to Ethereum development environment for L1 contracts - Deployed portal contract on L1 (see [token bridge tutorial](../../tutorials/js_tutorials/token_bridge.md)) -## Send messages from L1 to L2 +## L1 to L2 messaging -### Send a message from your L1 portal contract +### Send a message from L1 -Use the `Inbox` contract to send messages from L1 to L2. Call `sendL2Message` with these parameters: +Use the `Inbox` contract's `sendL2Message` function: | Parameter | Type | Description | | ------------- | --------- | ------------------------------------------------------- | -| `actor` | `L2Actor` | Your L2 contract address and rollup version | -| `contentHash` | `bytes32` | Hash of your message content (use `Hash.sha256ToField`) | -| `secretHash` | `bytes32` | Hash of a secret for message consumption | +| `_recipient` | `L2Actor` | L2 contract address and rollup version | +| `_content` | `bytes32` | Hash of message content (use `Hash.sha256ToField`) | +| `_secretHash` | `bytes32` | Hash of secret for message consumption | -In your Solidity contract: +#include_code deposit_public l1-contracts/test/portals/TokenPortal.sol solidity -```solidity -import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; -import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; -import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; +:::note Message availability +L1 to L2 messages are not available immediately. The proposer batches messages from the Inbox and includes them in the next L2 block. You must wait for this before consuming the message on L2. +::: -// ... initialize inbox, get rollupVersion from rollup contract ... +### Consume the message on L2 -DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2ContractAddress, rollupVersion); +Call `consume_l1_to_l2_message` on the context. The `content` must match the hash sent from L1, and the `secret` must be the pre-image of the `secretHash`. Consuming a message emits a nullifier to prevent double-spending. -// Hash your message content with a unique function signature -bytes32 contentHash = Hash.sha256ToField( - abi.encodeWithSignature("your_action_name(uint256,address)", param1, param2) -); +The content hash must be computed identically on both L1 and L2. Create a shared library for your content hash functions—see [`token_portal_content_hash_lib`](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/token_portal_content_hash_lib) for an example. -// Send the message -(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, secretHash); -``` +#include_code claim_public noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr rust -### Consume the message in your L2 contract - -To consume a message coming from L1, use the `consume_l1_to_l2_message` function within the context: - -- The `content_hash` must match the hash that was sent from L1 -- The `secret` is the pre-image of the `secretHash` sent from L1 -- The `sender` is the L1 portal contract address -- The `message_leaf_index` helps the RPC find the correct message -- If the content or secret doesn't match, the transaction will revert -- "Consuming" a message pushes a nullifier to prevent double-spending - -```rust -#[external("public")] -fn consume_message_from_l1( - secret: Field, - message_leaf_index: Field, - // your function parameters -) { - // Recreate the same content hash as on L1 - let content_hash = /* compute your content hash */; - - // Consume the L1 message - context.consume_l1_to_l2_message( - content_hash, - secret, - portal_address, // Your L1 portal contract address - message_leaf_index - ); - - // Execute your contract logic here -} -``` +This function works in both public and private contexts. -## Send messages from L2 to L1 +## L2 to L1 messaging -### Send a message from your L2 contract +### Send a message from L2 -Use `message_portal` in your `context` to send messages from L2 to L1: +Call `message_portal` on the context to send messages to your L1 portal: -```rust -#[external("public")] -fn send_message_to_l1( - // your function parameters -) { - // Note: This can be called from both public and private functions - // Create your message content (must fit in a single Field) - let content = /* compute your content hash */; +#include_code exit_to_l1_public noir-projects/noir-contracts/contracts/app/token_bridge_contract/src/main.nr rust - // Send message to L1 portal - context.message_portal(portal_address, content); -} -``` +This function works in both public and private contexts. -### Consume the message in your L1 portal - -Use the `Outbox` to consume L2 messages on L1: - -```solidity -import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; -import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; -import {Hash} from "@aztec/core/libraries/crypto/Hash.sol"; - -function consumeMessageFromL2( - // your parameters - uint256 _l2BlockNumber, - uint256 _leafIndex, - bytes32[] calldata _path -) external { - // Recreate the message structure - DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ - sender: DataStructures.L2Actor(l2ContractAddress, rollupVersion), - recipient: DataStructures.L1Actor(address(this), block.chainid), - content: Hash.sha256ToField( - abi.encodeWithSignature( - "your_action_name(address,uint256,address)", - param1, param2, param3 - ) - ) - }); - - // Consume the message - outbox.consume(message, _l2BlockNumber, _leafIndex, _path); - - // Execute your L1 logic here -} -``` +### Consume the message on L1 + +Use the `Outbox` contract to consume L2 messages. + +:::note Message availability +L2 to L1 messages are only available after the epoch proof is submitted to L1. Since multiple L2 blocks fit within an epoch, there may be a delay—especially if the message was sent near the start of an epoch. +::: + +#include_code token_portal_withdraw l1-contracts/test/portals/TokenPortal.sol solidity -:::info +:::info Getting the membership witness -You can get the witness for the l2 to l1 message as follows: +Compute the witness for the L2 to L1 message in TypeScript: ```ts import { computeL2ToL1MembershipWitness } from "@aztec/stdlib/messaging"; import { computeL2ToL1MessageHash } from "@aztec/stdlib/hash"; -// Compute the message hash const l2ToL1Message = computeL2ToL1MessageHash({ - l2Sender: l2ContractAddress, + l2Sender: l2BridgeAddress, l1Recipient: EthAddress.fromString(portalAddress), - content: messageContent, + content: withdrawContentHash, rollupVersion: new Fr(version), chainId: new Fr(chainId), }); const witness = await computeL2ToL1MembershipWitness( - node, - exitReceipt.blockNumber!, + aztecNode, + txReceipt.blockNumber!, l2ToL1Message ); -``` -::: - -## Best practices - -### Structure messages properly - -Use function signatures to prevent message misinterpretation: - -```solidity -// ❌ Ambiguous format -bytes memory message = abi.encode(_value, _contract, _recipient); - -// ✅ Clear function signature -bytes memory message = abi.encodeWithSignature( - "execute_action(uint256,address,address)", - _value, _contract, _recipient -); +// Use witness.leafIndex and witness.siblingPath for the L1 consume call ``` -### Use designated callers - -Control message execution order with designated callers: - -```solidity -bytes memory message = abi.encodeWithSignature( - "execute_action(uint256,address,address)", - _value, _recipient, - _withCaller ? msg.sender : address(0) -); -``` +::: ## Example implementations @@ -195,4 +97,4 @@ bytes memory message = abi.encodeWithSignature( ## Next steps -Follow the [cross-chain messaging tutorial](../../tutorials/js_tutorials/token_bridge.md) for a complete implementation example. +Follow the [token bridge tutorial](../../tutorials/js_tutorials/token_bridge.md) for a complete implementation example. diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_define_storage.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_define_storage.md index 8cb265e48116..b077b3cf9eaf 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_define_storage.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_define_storage.md @@ -7,631 +7,543 @@ description: Define and manage storage state in your Aztec smart contracts using This guide shows you how to declare storage and use various storage types provided by Aztec.nr for managing contract state. +## Choosing the right storage type + +| Need | Use | +|------|-----| +| Public value anyone can read/write | `PublicMutable` | +| Public value set once (contract name, decimals) | `PublicImmutable` | +| Public key-value mapping | `Map>` | +| Private collection per user (token balances) | `Owned>` | +| Single private value per user | `Owned>` | +| Immutable private value per user | `Owned>` | +| Contract-wide private singleton (admin key) | `SinglePrivateMutable` | +| Public value readable in private execution | `DelayedPublicMutable` | + ## Prerequisites - An Aztec contract project set up with `aztec-nr` dependency - Understanding of Aztec's private and public state model - Familiarity with Noir struct syntax -- Basic knowledge of maps and data structures For storage concepts, see [storage overview](../../foundational-topics/state_management.md). ## Define your storage struct -### Create a storage struct with #[storage] - -Declare storage using a struct annotated with `#[storage]`. For example: +Declare storage using a struct annotated with `#[storage]`: ```rust #[storage] struct Storage { - // The admin of the contract admin: PublicMutable, + balances: Owned, Context>, } ``` -### Context parameter - -The `Context` parameter provides execution mode information. - -### Access storage in functions +The `Context` parameter determines which methods are available on state variables based on execution context. -Use the `storage` keyword to access your storage variables in contract functions. +Access storage in functions using `self.storage`: -## Use maps for key-value storage - -Maps store key-value pairs where keys are `Field` elements and values can be any type. - -You can import `Map` as: - -```noir -use dep::aztec::state_vars::Map; +```rust +#[external("public")] +fn get_admin() -> AztecAddress { + self.storage.admin.read() +} ``` -### Understand map structure +## Private storage types -- Keys: Always `Field` or serializable types -- Values: Any type, including other maps -- Multiple maps: Supported in the same contract +Aztec.nr provides private state variables that operate on notes. Private state variables that are "owned" (tied to a specific owner address) must be wrapped in an `Owned` state variable. -### Declare private maps +For creating custom note types to use with these storage variables, see [how to implement custom notes](./how_to_implement_custom_notes.md). -Specify the note type for private storage maps: +### Owned state variables + +Private state variables like `PrivateMutable`, `PrivateImmutable`, and `PrivateSet` implement the `OwnedStateVariable` trait. You must wrap them in `Owned`: ```rust -private_items: Map, Context>, -``` +use aztec::state_vars::{Owned, PrivateMutable, PrivateImmutable, PrivateSet}; -### Declare public maps +#[storage] +struct Storage { + private_value: Owned, Context>, + private_config: Owned, Context>, + private_notes: Owned, Context>, +} +``` -Use `PublicState` for public storage maps: +Access the underlying state variable for a specific owner using `.at(owner)`: ```rust -authorized_users: Map, Context>, +let owner = self.msg_sender().unwrap(); +self.storage.private_value.at(owner).initialize(note); ``` -### Access map values +### PrivateMutable -Use the `.at()` method to access values by key: +`PrivateMutable` holds a single private value per owner that can be updated. When the value changes, the current note is nullified and a new note is inserted. ```rust -assert(storage.authorized_users.at(context.msg_sender()).read(), "caller is not authorized"); +private_value: Owned, Context>, ``` -:::tip - -This is equivalent to Solidity's `authorized_users[msg.sender]` pattern. +#### `initialize` -::: +Creates the first note for this state variable. Can only be called once per owner. Returns a `NoteMessage` that requires you to specify how to deliver the note. -## Use private storage types +```rust +#[external("private")] +fn initialize_value(value: Field) { + let owner = self.msg_sender().unwrap(); + let note = FieldNote { value }; -Aztec.nr provides three private state variable types: + self.storage.private_value.at(owner).initialize(note).deliver( + MessageDelivery.CONSTRAINED_ONCHAIN, + ); +} +``` -- `PrivateMutable`: Single mutable private value -- `PrivateImmutable`: Single immutable private value -- `PrivateSet`: Collection of private notes +:::note MessageDelivery options -All private storage operates on note types rather than arbitrary data types. Learn how to implement custom notes and use them with Maps [here](./how_to_implement_custom_notes.md) +Private state operations return a `NoteMessage` that must be delivered. Choose based on your needs: -### PrivateMutable +- **`CONSTRAINED_ONCHAIN`** - Cryptographic guarantees that recipients can decrypt. Use when contracts need to verify message contents. +- **`UNCONSTRAINED_ONCHAIN`** - Faster proving, stored onchain. Use when recipients can verify validity through other means. +- **`UNCONSTRAINED_OFFCHAIN`** - Lowest cost, requires custom delivery infrastructure. Use for high-volume applications. -PrivateMutable is a private state variable that is unique in a way. When a PrivateMutable is initialized, a note is created to represent its value. Updating the value means to destroy the current note, and to create a new one with the updated value. +::: -Like for public state, we define the struct to have context and a storage slot. You can view the implementation [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr). +#### `replace` -An example of `PrivateMutable` usage in contracts is keeping track of important values. The `PrivateMutable` is added to the `Storage` struct as follows: +Updates the value by nullifying the current note and inserting a new one. Takes a function that transforms the old note into a new note: ```rust -// #[storage] -// ...etc -my_value: PrivateMutable, -``` - -#### `initialize` - -As mentioned, the PrivateMutable should be initialized to create the first note and value. When this function is called, a nullifier of the storage slot is created, preventing this PrivateMutable from being initialized again. +#[external("private")] +fn update_value(new_value: Field) { + let owner = self.msg_sender().unwrap(); + self.storage.private_value.at(owner).replace(|_old_note| FieldNote { value: new_value }).deliver( + MessageDelivery.CONSTRAINED_ONCHAIN, + ); +} -Unlike public states, which have a default initial value of `0` (or many zeros, in the case of a struct, array or map), a private state (of type `PrivateMutable`, `PrivateImmutable` or `PrivateSet`) does not have a default initial value. The `initialize` method (or `insert`, in the case of a `PrivateSet`) must be called. +#[external("private")] +fn increment_value() { + let owner = self.msg_sender().unwrap(); + self.storage.private_value.at(owner).replace(|old_note| { + FieldNote { value: old_note.value + 1 } + }).deliver(MessageDelivery.CONSTRAINED_ONCHAIN); +} +``` -#### `is_initialized` +#### `initialize_or_replace` -An unconstrained method to check whether the PrivateMutable has been initialized or not. It takes an optional owner and returns a boolean. You can view the implementation [here (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr). +Handles both initialization and replacement in one call. The function receives `Option::none()` if uninitialized, or `Option::some(note)` if a note exists: ```rust -let is_initialized = my_value.is_initialized(); +#[external("private")] +fn set_value(new_value: Field) { + let owner = self.msg_sender().unwrap(); + self.storage.private_value.at(owner).initialize_or_replace(|maybe_note| { + // maybe_note is None if uninitialized, Some(note) if exists + FieldNote { value: new_value } + }).deliver(MessageDelivery.CONSTRAINED_ONCHAIN); +} ``` -#### `replace` - -To update the value of a `PrivateMutable`, we can use the `replace` method. The method takes a function (or closure) that transforms the current note into a new one. - -When called, the method will: - -- Nullify the old note -- Apply the transform function to produce a new note -- Insert the new note into the data tree +#### `get_note` -An example of this is seen in an example card game, where an update function is passed in to transform the current note into a new one (in this example, updating a `CardNote` data): +Reads the current note. The read nullifies the note and creates a new one with the same value to ensure you're reading the latest value. ```rust -let new_note = MyNote::new(new_value, owner); -storage.my_value.replace(&mut new_note).deliver(encode_and_encrypt_note(&mut context, owner)); +#[external("private")] +fn read_value() { + let owner = self.msg_sender().unwrap(); + let note_message = self.storage.private_value.at(owner).get_note(); + // Access the note content via note_message.get_new_note() + note_message.deliver(MessageDelivery.CONSTRAINED_ONCHAIN); +} ``` :::info -Calling `deliver(encode_and_encrypt_note())` on the `replace` method will encrypt the new note and post it to the data availability layer so that the note information is retrievable by the recipient. +Reading a `PrivateMutable` nullifies and recreates the note. This makes reads indistinguishable from writes and ensures the sequencer cannot learn the note's value. ::: -If two people are trying to modify the PrivateMutable at the same time, only one will succeed as we don't allow duplicate nullifiers! Developers should put in place appropriate access controls to avoid race conditions (unless a race is intended!). - -#### `get_note` +#### `is_initialized` -This function allows us to get the note of a PrivateMutable, essentially reading the value. +An unconstrained method to check whether the `PrivateMutable` has been initialized: ```rust -let note = my_value.get_note() +#[external("utility")] +unconstrained fn is_initialized(owner: AztecAddress) -> bool { + self.storage.private_value.at(owner).is_initialized() +} ``` -:::info - -To ensure that a user's private execution always uses the latest value of a PrivateMutable, the `get_note` function will nullify the note that it is reading. This means that if two people are trying to use this function with the same note, only one will succeed (no duplicate nullifiers allowed). - -This also makes read operations indistinguishable from write operations and allows the sequencer to verifying correct execution without learning anything about the value of the note. - -::: - #### `view_note` -Functionally similar to [`get_note`](#get_note), but executed in unconstrained functions and can be used by the wallet to fetch notes for use by front-ends etc. +An unconstrained method to read the note without nullifying it. Use in utility functions only: + +```rust +#[external("utility")] +unconstrained fn view_value(owner: AztecAddress) -> FieldNote { + self.storage.private_value.at(owner).view_note() +} +``` ### PrivateImmutable -`PrivateImmutable` represents a unique private state variable that, as the name suggests, is immutable. Once initialized, its value cannot be altered. You can view the implementation [here (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr). +`PrivateImmutable` holds a single private value per owner that cannot be changed after initialization. -#### `initialize` +```rust +private_config: Owned, Context>, +``` -When this function is invoked, it creates a nullifier for the storage slot, ensuring that the PrivateImmutable cannot be initialized again. +#### `initialize` -Set the value of an PrivateImmutable by calling the `initialize` method: +Sets the permanent value. Can only be called once per owner: ```rust #[external("private")] -fn initialize_private_immutable(my_value: u8) { - let new_note = MyNote::new(my_value, context.msg_sender().unwrap()); +fn set_config(value: Field) { + let owner = self.msg_sender().unwrap(); + let note = ConfigNote { value }; - storage.my_private_immutable.initialize(new_note).deliver(encode_and_encrypt_note( - &mut context, - context.msg_sender(), - )); + self.storage.private_config.at(owner).initialize(note).deliver( + MessageDelivery.CONSTRAINED_ONCHAIN, + ); } ``` -:::info - -Calling `deliver(encode_and_encrypt_note())` on `initialize` will encrypt the new note and post it to the data availability layer so that the note information is retrievable by the recipient. - -::: - -Once initialized, an PrivateImmutable's value remains unchangeable. This method can only be called once. - -#### `is_initialized` - -An unconstrained method to check if the PrivateImmutable has been initialized. Takes an optional owner and returns a boolean. You can find the implementation [here (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr). - #### `get_note` -Similar to the `PrivateMutable`, we can use the `get_note` method to read the value of an PrivateImmutable. - -Use this method to retrieve the value of an initialized PrivateImmutable. +Returns the note directly (not a `NoteMessage`) since immutable notes don't need to be recreated. Multiple callers can read concurrently: ```rust #[external("private")] -fn get_immutable_note() -> MyNote { - storage.my_private_immutable.get_note() +fn read_config() -> ConfigNote { + let owner = self.msg_sender().unwrap(); + self.storage.private_config.at(owner).get_note() } ``` -Unlike a `PrivateMutable`, the `get_note` function for an PrivateImmutable doesn't nullify the current note in the background. This means that multiple accounts can concurrently call this function to read the value. - -This function will throw if the `PrivateImmutable` hasn't been initialized. - -#### `view_note` - -Functionally similar to `get_note`, but executed unconstrained and can be used by the wallet to fetch notes for use by front-ends etc. - ### PrivateSet -`PrivateSet` is used for managing a collection of notes. All notes in a `PrivateSet` are of the same `NoteType`. But whether these notes all belong to one entity, or are accessible and editable by different entities, is up to the developer. - -For example, adding a mapping of private items to storage, indexed by `AztecAddress`: +`PrivateSet` manages a collection of notes for each owner. All notes share the same storage slot but can have different values. ```rust -private_items: Map, Context>, +balances: Owned, Context>, ``` #### `insert` -Allows us to modify the storage by inserting a note into the `PrivateSet`. - -A hash of the note will be generated, and inserted into the note hash tree, allowing us to later use in contract interactions. Recall that the content of the note should be shared with the owner to allow them to use it, as mentioned this can be done via an encrypted log or offchain via web2, or completely offline. +Adds a new note to the set: ```rust -storage.set.at(aztec_address).insert(new_note).deliver(encode_and_encrypt_note(&mut context, aztec_address)); +let note = UintNote { value: amount }; +self.storage.balances.at(owner).insert(note).deliver( + MessageDelivery.CONSTRAINED_ONCHAIN, +); ``` -:::info - -Calling `deliver(encode_and_encrypt_note())` on `insert` will encrypt the new note and post it to the data availability layer so that the note information is retrievable by the recipient. - -::: - #### `pop_notes` -This function pops (gets, removes and returns) the notes the account has access to based on the provided filter. - -The kernel circuits are constrained to a maximum number of notes this function can return at a time. Check [here (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr) and look for `MAX_NOTE_HASH_READ_REQUESTS_PER_CALL` for the up-to-date number. +Retrieves and nullifies notes matching the filter options: -Because of this limit, we should always consider using the second argument `NoteGetterOptions` to limit the number of notes we need to read and constrain in our programs. This is quite important as every extra call increases the time used to prove the program and we don't want to spend more time than necessary. +```rust +let options = NoteGetterOptions::new(); +let notes = self.storage.balances.at(owner).pop_notes(options); +``` -An example of such options is using the filter functions from the value note library (like `filter_notes_min_sum`) to get "enough" notes to cover a given value. Essentially, this function will return just enough notes to cover the amount specified such that we don't need to read all our notes. For users with a lot of notes, this becomes increasingly important. +Use filters to limit the notes retrieved: ```rust use value_note::filter::filter_notes_min_sum; -// etc... -let options = NoteGetterOptions::with_filter(filter_notes_min_sum, subtrahend as Field); -let notes = self.set.pop_notes(options); +let options = NoteGetterOptions::with_filter(filter_notes_min_sum, amount as Field); +let notes = self.storage.balances.at(owner).pop_notes(options); ``` #### `get_notes` -This function has the same behavior as `pop_notes` above but it does not delete the notes. - -#### `remove` - -Will remove a note from the `PrivateSet` if it previously has been read from storage, e.g. you have fetched it through a `get_notes` call. This is useful when you want to remove a note that you have previously read from storage and do not have to read it again. +Similar to `pop_notes` but does not nullify the notes. Use when you need to read without consuming: -Note that if you obtained the note you are about to remove via `get_notes` it's much better to use `pop_notes` as `pop_notes` results in significantly fewer constraints since it doesn't need to check that the note has been previously read, as it reads and deletes at once. +```rust +let options = NoteGetterOptions::new(); +let notes = self.storage.balances.at(owner).get_notes(options); +``` -#### `view_notes` +#### `remove` -Functionally similar to [`get_notes`](#get_notes), but executed unconstrained and can be used by the wallet to fetch notes for use by front-ends etc. +Removes a previously retrieved note: ```rust -let mut options = NoteViewerOptions::new(); -let notes = set.view_notes(options.set_offset(offset)); +self.storage.balances.at(owner).remove(retrieved_note); ``` -There's also a limit on the maximum number of notes that can be returned in one go. To find the current limit, refer to [this file (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/constants.nr) and look for `MAX_NOTES_PER_PAGE`. - -The key distinction is that this method is unconstrained. It does not perform a check to verify if the notes actually exist, which is something the [`get_notes`](#get_notes) method does under the hood. Therefore, it should only be used in an unconstrained contract function. +:::tip -This function requires a `NoteViewerOptions`. The `NoteViewerOptions` is essentially similar to the [`NoteGetterOptions`](#notegetteroptions), except that it doesn't take a custom filter. +Prefer `pop_notes` over `get_notes` followed by `remove` as it results in fewer constraints. -## Use public storage types +::: -Aztec.nr provides two public state variable types that work similarly to Ethereum's storage model: +#### `view_notes` -- `PublicMutable`: Mutable public value that can be updated -- `PublicImmutable`: Immutable public value that can only be set once +An unconstrained method to view notes without constraints: -Both types are generic over any serializable type `T`, allowing you to store simple values like integers and booleans, as well as complex structs. Public storage is transparent - all values are visible to anyone observing the blockchain. +```rust +#[external("utility")] +unconstrained fn view_balance(owner: AztecAddress) -> BoundedVec { + let options = NoteViewerOptions::new(); + self.storage.balances.at(owner).view_notes(options) +} +``` -### PublicMutable +### SinglePrivateMutable and SinglePrivateImmutable -Store mutable public state using `PublicMutable` for values that need to be updated throughout the contract's lifecycle. +For contract-wide private values (not per-owner), use `SinglePrivateMutable` or `SinglePrivateImmutable`. These store exactly one value for the entire contract - a global singleton - rather than separate values per owner. -:::info -An example using a larger struct can be found in the [lending example](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/lending_contract)'s use of an [`Asset`](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/noir-contracts/contracts/app/lending_contract/src/asset.nr). -::: +| Type | Use Case | Access Pattern | +|------|----------|----------------| +| `Owned>` | Per-owner private state (like balances) | `.at(owner).get_note()` | +| `SinglePrivateMutable` | Contract-wide singleton (like admin) | `.get_note()` directly | -For example, to add `config_value` public state variable into our storage struct, we can define it as: +Since there's only one value at the storage slot, there's no need to specify an owner to look it up: ```rust -config_value: PublicMutable, +#[storage] +struct Storage { + admin: SinglePrivateMutable, + config: SinglePrivateImmutable, +} + +// Access directly without .at(owner) +let note_message = self.storage.admin.get_note(); +let config = self.storage.config.get_note(); ``` -To add a group of `authorized_users` that are able to perform actions in our contract, and we want them in public storage: +When initializing, you still pass an owner address - but this specifies who can decrypt the note, not the storage location: ```rust -authorized_users: Map, Context>, +// owner_address determines who can see the note, not where it's stored +self.storage.admin.initialize(note, owner_address).deliver(MessageDelivery.CONSTRAINED_ONCHAIN); ``` -#### `read` - -On the `PublicMutable` structs we have a `read` method to read the value at the location in storage. For our `config_value` example from earlier, this could be used as follows to check that the stored value matches the `msg_sender()`: +## Public storage types -```rust -let admin = storage.admin.read(); -assert(admin == context.msg_sender().unwrap(), "caller is not admin"); -``` +Public storage works similarly to Ethereum's storage model - values are stored directly and are visible to everyone. -#### `write` +### PublicMutable -We have a `write` method on the `PublicMutable` struct that takes the value to write as an input and saves this in storage. It uses the serialization method to serialize the value which inserts (possibly multiple) values into storage: +Stores mutable public state that can be read and written in public functions: ```rust -storage.admin.write(new_admin); +admin: PublicMutable, +total_supply: PublicMutable, ``` -### PublicImmutable +:::note -`PublicImmutable` is a type that is initialized from public once, typically during a contract deployment, but which can later be read from public, private and utility execution contexts. This state variable is useful for stuff that you would usually have in `immutable` values in Solidity, e.g. this can be the name of a contract or its version number. +Unlike private state which must be explicitly initialized, uninitialized `PublicMutable` returns the default value (zero for numbers, empty for addresses). This matches Ethereum's behavior. -Just like the `PublicMutable` it is generic over the variable type `T`. The type must implement the `Serialize` and `Deserialize` traits. +::: + +#### `read` ```rust -my_public_immutable: PublicImmutable, +let admin = self.storage.admin.read(); ``` -You can find the details of `PublicImmutable` in the implementation [here (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr). - -#### `new` - -Is done exactly like the `PublicMutable` struct, but with the `PublicImmutable` struct. +#### `write` ```rust -my_public_immutable: PublicImmutable, +self.storage.admin.write(new_admin); ``` -#### `initialize` +### PublicImmutable -This function sets the immutable value. It can only be called once. +Stores public state that is set once and can be read from public, private, and utility contexts: ```rust -storage.my_public_immutable.initialize(my_value); +name: PublicImmutable, +decimals: PublicImmutable, ``` -:::warning -A `PublicImmutable`'s storage **must** only be set once via `initialize`. Attempting to override this by manually accessing the underlying storage slots breaks all properties of the data structure, rendering it useless. -::: +#### `initialize` + +Can only be called once, typically in the constructor: ```rust #[external("public")] -fn initialize_public_immutable(my_value: u8) { - let mut new_struct = MyStruct { account: context.msg_sender().unwrap(), value: my_value }; - storage.my_public_immutable.initialize(new_struct); +#[initializer] +fn constructor(name: str<31>, decimals: u8) { + self.storage.name.initialize(FieldCompressedString::from_string(name)); + self.storage.decimals.initialize(decimals); } ``` #### `read` -Returns the stored immutable value. This function is available in public, private and utility contexts. +Can be called from any context (public, private, or utility): ```rust -#[external("utility")] -unconstrained fn get_public_immutable() -> MyStruct { - storage.my_public_immutable.read() +// In public +#[external("public")] +fn get_name() -> FieldCompressedString { + self.storage.name.read() } -``` - -## Use custom structs in public storage - -Both `PublicMutable` and `PublicImmutable` are generic over any serializable type, which means you can store custom structs in public storage. This is useful for storing configuration data, game state, or any other structured data that needs to be publicly visible. -### Define a custom struct for storage - -To use a custom struct in public storage, it must implement the `Packable` trait. You can automatically derive this along with other useful traits: - -```rust -use dep::aztec::protocol_types::{ - address::AztecAddress, - traits::{Deserialize, Packable, Serialize} -}; +// In private (reads from historical state) +#[external("private")] +fn get_name_private() -> FieldCompressedString { + self.storage.name.read() +} -// Required derives for public storage: -// - Packable: Required for all public storage -// - Serialize: Required for returning from functions -// - Deserialize: Required for receiving as parameters -#[derive(Deserialize, Packable, Serialize)] -pub struct Asset { - pub interest_accumulator: u128, - pub last_updated_ts: u64, - pub loan_to_value: u128, - pub oracle: AztecAddress, +// In utility +#[external("utility")] +unconstrained fn get_name_unconstrained() -> FieldCompressedString { + self.storage.name.read() } ``` -Common optional derives include: - -- `Eq`: For equality comparisons between structs +### Maps -### Store custom structs - -Once defined, use your custom struct in storage declarations: +Maps store key-value pairs where keys implement the `ToField` trait and values are public state variables: ```rust #[storage] struct Storage { - // Single custom struct - config: PublicMutable, - - // Map of custom structs - assets: Map, Context>, - - // Immutable custom struct (like contract config) - initial_config: PublicImmutable, + minters: Map, Context>, + public_balances: Map, Context>, } ``` -### Read and write custom structs - -Work with custom structs using the same `read()` and `write()` methods as built-in types: +Use the `.at()` method to access the state variable for a given key: ```rust -#[public] -fn update_asset(asset_id: Field, new_accumulator: u128) { - // Read the current struct - let mut asset = storage.assets.at(asset_id).read(); - - // Modify fields - asset.interest_accumulator = new_accumulator; - asset.last_updated_ts = context.timestamp(); - - // Write back the updated struct - storage.assets.at(asset_id).write(asset); +#[external("public")] +fn is_minter(minter: AztecAddress) -> bool { + self.storage.minters.at(minter).read() } -#[public] -fn get_asset(asset_id: Field) -> Asset { - storage.assets.at(asset_id).read() +#[external("public")] +fn set_minter(minter: AztecAddress, approve: bool) { + self.storage.minters.at(minter).write(approve); } ``` -You can also create and store new struct instances: +This is equivalent to Solidity's `minters[minter]` pattern. + +Maps can contain other maps for multi-dimensional lookups: ```rust -#[public] -fn initialize_asset( - interest_accumulator: u128, - loan_to_value: u128, - oracle: AztecAddress -) { - let last_updated_ts = context.timestamp() as u64; +// Map game_id -> player_address -> score +games: Map, Context>, Context>, - storage.assets.at(0).write( - Asset { - interest_accumulator, - last_updated_ts, - loan_to_value, - oracle, - } - ); -} +// Access: self.storage.games.at(game_id).at(player).read() ``` -### Use custom structs in nested maps +:::note + +Maps can only be used with public state variables (`PublicMutable`, `PublicImmutable`, `DelayedPublicMutable`) or other `Map`s. For private state, use the `Owned` wrapper described above. -Custom structs work seamlessly with nested map structures: +::: + +### Custom structs in public storage + +Custom structs can be stored in `PublicMutable` and `PublicImmutable` if they implement the required traits: ```rust +use aztec::protocol_types::traits::{Deserialize, Packable, Serialize}; + #[derive(Deserialize, Eq, Packable, Serialize)] -pub struct Game { - pub started: bool, - pub finished: bool, - pub current_round: u32, +pub struct Config { + pub admin: AztecAddress, + pub fee: u128, + pub enabled: bool, } #[storage] struct Storage { - // Map game_id -> player_address -> Game struct - games: Map, Context>, Context>, + config: PublicMutable, } -#[public] -fn start_game(game_id: Field, player: AztecAddress) { - let game = Game { - started: true, - finished: false, - current_round: 0, - }; - storage.games.at(game_id).at(player).write(game); +#[external("public")] +fn update_config(new_config: Config) { + self.storage.config.write(new_config); } -``` - -## Delayed Public Mutable - -This storage type is used if you want to use public values in private execution. - -A typical use case is some kind of system configuration, such as a protocol fee or access control permissions. These values are public (known by everyone) and mutable. Reading them in private however is tricky: private execution is always asynchronous and performed over _historical_ state, and hence one cannot easily prove that a given public value is current. - -:::note Alternative approaches - -A naive way to solve this is to enqueue a public call that will assert the current public value, but this leaks _which_ public value is being read, severely reducing privacy. Even if the value itself is already public, the fact that we're using it because we're interacting with some related contract is not. For example, we may leak that we're interacting with a certain DeFi protocol by reading its fee. -An alternative approach is to create notes in public that are then nullified in private, but this introduces contention: only a single user may use the note and therefore read the state, since nullifying it will prevent all others from doing the same. In some schemes there's only one account that will read the state anyway, but this is not the general case. - -::: - -Delayed Public Mutable state works around this by introducing **delays**: - -- Instead, a value change is be scheduled ahead of time, and some minimum amount of time must pass between the scheduling and the new value taking effect. -- This means that we can privately prove that a historical public value cannot possibly change before some point in the future (due to the minimum delay), and therefore that our transaction will be valid **as long as it gets included before this future time**. -- In other words, we're saying "this value is public but can't change until \_\_\_". - -This results in the following key properties of `DelayedPublicMutable` state: - -- public values can only be changed after a certain delay has passed, never immediately -- the scheduling of value changes is itself public, including both the new value and the time at which the change will take effect -- transactions that read `DelayedPublicMutable` state become invalid after some time if not included in a block - -:::warning Privacy Consideration - -While `DelayedPublicMutable` state variables are much less leaky than the assertion in public approach, they do reveal some information to external observers by setting the `include_by_timestamp` property of the transaction request. The impact of this can be mitigated with proper selection of the delay value and schedule times. - -::: - -### Choosing Delays - -The `include_by_timestamp` transaction property will be set to a value close to the current timestamp plus the duration of the delay in seconds. The exact value depends on the anchor block over which the private proof is constructed. For example, if current timestamp is `X` and a `DelayedPublicMutable` state variable has a delay of 3000 seconds, then transactions that read this value privately will set `include_by_timestamp` to a value close to 'X + 3000' (clients building proofs on older state will select a lower `include_by_timestamp`). - -These delays can be changed during the contract lifetime as the application's needs evolve. - -:::tip Delay duration - -Applications using similar delays will therefore be part of the same privacy set. It is recommended to look for industry standards for these delays. For example: - -- 12 hours for time-sensitive operations, such as emergency mechanisms -- 5 days for middle-of-the-road operations -- 2 weeks for operations that require lengthy public scrutiny. - -Smaller delays are fine too. As a rule of thumb, the smaller the delay, the smaller the privacy set, so your mileage may vary. - -Additionally, you may choose to coordinate and constrain your transactions to set `include_by_timestamp` to a value lower than would be strictly needed by the applications you interact with (if any!) using some common delay, and by doing so prevent privacy leakage. - -Note that wallets can also warn users that a value change will soon take place and that sending a transaction at that time might result in reduced privacy, allowing them to choose to wait until after the epoch. - -::: - -:::info - -Even though only transactions that interact with `DelayedPublicMutable` state _need_ to set the `include_by_timestamp` property, there is no reason why transactions that do not wouldn't also set this value. +#[external("public")] +fn get_config() -> Config { + self.storage.config.read() +} +``` -If indeed most applications converge on a small set of delays, then wallets could opt to select any of those to populate the `include_by_timestamp` field, as if they were interacting with a `DelayedPublicMutable` state variable with that delay. +## DelayedPublicMutable -This prevents the network-wide privacy set from being split between transactions that read `DelayedPublicMutable` state and those that don't, which is beneficial to everyone. +Use `DelayedPublicMutable` when you need to read public values in private execution. Changes to the value are delayed, allowing private proofs to remain valid for a known time window. -::: +### Declare with initial delay -### DelayedPublicMutable - -Unlike other state variables, `DelayedPublicMutable` receives not only a type parameter for the underlying datatype, but also a `DELAY` type parameter with the value change delay as a number of seconds. +The delay is specified as a const generic parameter (in seconds): ```rust -my_delayed_value: DelayedPublicMutable, -``` +// 5 days = 432000 seconds +global CONFIG_DELAY: u64 = 432000; -:::note -`DelayedPublicMutable` requires that the underlying type `T` implements both the `ToField` and `FromField` traits, meaning it must fit in a single `Field` value. There are plans to extend support by requiring instead an implementation of the `Serialize` and `Deserialize` traits, therefore allowing for multi-field variables, such as complex structs. -::: - -Since `DelayedPublicMutable` lives in public storage, by default its contents are zeroed-out. Intialization is performed by calling `schedule_value_change`, resulting in initialization itself being delayed. +#[storage] +struct Storage { + fee: DelayedPublicMutable, +} +``` ### `schedule_value_change` -This is the means by which a `DelayedPublicMutable` variable mutates its contents. It schedules a value change for the variable at a future timestamp after the `DELAY` has elapsed from the current timestamp, at which point the scheduled value becomes the current value automatically and without any further action, both in public and in private. If a pending value change was scheduled but not yet effective (because insufficient time had elapsed), then the previous schedule value change is replaced with the new one and eliminated. There can only be one pending value change at a time. - -This function can only be called in public, typically after some access control check: +Schedules a value change that takes effect after the delay: ```rust #[external("public")] -fn set_my_value(new_value: MyType) { - assert_eq(storage.admin.read(), context.msg_sender().unwrap(), "caller is not admin"); - storage.my_delayed_value.schedule_value_change(new_value); +fn set_fee(new_fee: u128) { + assert(self.storage.admin.read() == self.msg_sender().unwrap(), "not admin"); + self.storage.fee.schedule_value_change(new_fee); } ``` -If one wishes to schedule a value change from private, simply enqueue a public call to a public `internal` contract function. Recall that **all scheduled value changes, including the new value and scheduled timestamp are public**. - -:::warning - -A `DelayedPublicMutable`'s storage **must** only be mutated via `schedule_value_change`. Attempting to override this by manually accessing the underlying storage slots breaks all properties of the data structure, rendering it useless. - -::: - ### `get_current_value` -Returns the current value in a public, private or utility execution context. Once a value change is scheduled via `schedule_value_change` and the delay time passes, this automatically returns the new value. +Returns the current value. In private, this sets the transaction's `include_by_timestamp` to ensure the proof remains valid: ```rust -storage.my_delayed_value.get_current_value() -``` - -Also, calling in private will set the `include_by_timestamp` property of the transaction request, introducing a new validity condition to the entire transaction: it cannot be included in any block with a timestamp larger than `include_by_timestamp`. +// In public +#[external("public")] +fn get_fee_public() -> u128 { + self.storage.fee.get_current_value() +} -```rust -let current_value = storage.my_delayed_value.get_current_value(); +// In private +#[external("private")] +fn get_fee_private() -> u128 { + self.storage.fee.get_current_value() +} ``` ### `get_scheduled_value` -Returns the last scheduled value change, along with the timestamp at which the scheduled value becomes the current value. This may either be a pending change, if the timestamp is in the future, or the last executed scheduled change if the timestamp is in the past (in which case there are no pending changes). +Returns the scheduled value and when it takes effect: ```rust -storage.my_delayed_value.get_scheduled_value() +let (scheduled_value, effective_timestamp) = self.storage.fee.get_scheduled_value(); ``` -It is not possible to call this function in private: doing so would not be very useful at it cannot be asserted that a scheduled value change will not be immediately replaced if `shcedule_value_change` where to be called. +:::warning Privacy Consideration + +Reading `DelayedPublicMutable` in private sets the `include_by_timestamp` property, which may reveal timing information. Choose delays that align with common values to maximize privacy sets. + +::: + +### Recommended delays + +- **12 hours** - Time-sensitive operations like emergency mechanisms +- **5 days** - Standard operations +- **2 weeks** - Operations requiring lengthy public scrutiny diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_emit_event.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_emit_event.md index a63457dc8bf0..3ebd2dd4d471 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_emit_event.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_emit_event.md @@ -5,75 +5,97 @@ sidebar_position: 7 description: Learn how to emit events from your Aztec smart contracts for offchain applications to consume. --- -This guide shows you how to emit events and logs from your Aztec contracts to communicate with offchain applications. +Events allow contracts to communicate with offchain applications. Private events are encrypted and delivered to specific recipients, while public events are visible to everyone. ## Prerequisites - An Aztec contract project set up with `aztec-nr` dependency - Understanding of private vs public functions in Aztec -- Basic knowledge of event handling in blockchain applications -## Emit private events +## Define an event -### Emit encrypted events +Declare events using the `#[event]` attribute: -Use encrypted events to send private data to specific recipients: +```rust +#[event] +struct Transfer { + from: AztecAddress, + to: AztecAddress, + amount: u128, +} +``` + +## Emit private events + +In private functions, emit events using `self.emit()` and deliver them to recipients: ```rust -// Import from aztec.nr -use aztec::event::event_emission::emit_event_in_private; - -emit_event_in_private( - MyEvent { param1, param2, param3 }, - &mut context, - recipient, - MessageDelivery.UNCONSTRAINED_ONCHAIN, -); +use aztec::messages::message_delivery::MessageDelivery; + +#[external("private")] +fn transfer(to: AztecAddress, amount: u128) { + let from = self.msg_sender().unwrap(); + + // ... transfer logic ... + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.UNCONSTRAINED_ONCHAIN, + ); +} ``` -:::note -Developer can choose whether to emit encrypted events or not. Emitting the events means that they will be posted to Ethereum, in blobs, and will inherit the availability guarantees of Ethereum. Developers may choose not to emit events and to share information with recipients offchain, or through alternative mechanisms that are to be developed (e.g. alternative, cheaper data availability solutions). +:::warning +You **must** call `deliver_to()` on the returned `EventMessage`. If you don't, the event information is lost forever. The compiler will warn you about unused `EventMessage` values. ::: -The `MessageDelivery` enum provides three modes: +### Deliver to multiple recipients -- `MessageDelivery.CONSTRAINED_ONCHAIN` (value: 1): Constrained encryption, guarantees correct recipient -- `MessageDelivery.UNCONSTRAINED_ONCHAIN` (value: 2): Faster but trusts sender, may lose events if tagged incorrectly -- `MessageDelivery.UNCONSTRAINED_OFFCHAIN` (value: 3): Lowest cost, requires custom offchain infrastructure +You can deliver the same event to multiple recipients with different delivery modes: -### Event processing +```rust +let message = self.emit(Transfer { from, to, amount }); +message.deliver_to(from, MessageDelivery.UNCONSTRAINED_OFFCHAIN); +message.deliver_to(to, MessageDelivery.CONSTRAINED_ONCHAIN); +``` -Events are automatically discovered and decrypted by the wallet when contract functions are invoked. +The `MessageDelivery` options are: + +- **`CONSTRAINED_ONCHAIN`** - Constrained encryption with onchain delivery. Slowest proving but provides cryptographic guarantees that recipients can decrypt messages. +- **`UNCONSTRAINED_ONCHAIN`** - Unconstrained encryption with onchain delivery. Faster proving, but trusts the sender to encrypt correctly. +- **`UNCONSTRAINED_OFFCHAIN`** - Unconstrained encryption with offchain delivery. Lowest cost, but requires custom infrastructure to deliver messages to recipients. + +:::note +Emitting private events is optional. Onchain delivery publishes encrypted data to Ethereum blobs, inheriting Ethereum's data availability guarantees. You can choose to share information offchain instead. +::: ## Emit public events -Emit structured public events using the `emit` function: +In public functions, emit events using `self.emit()`: ```rust -// Import from aztec.nr -use aztec::event::event_emission::emit_event_in_public; +#[external("public")] +fn update_value(value: Field) { + // ... update logic ... -emit_event_in_public( - MyPublicEvent { field1: values[0], field2: values[1] }, - &mut context, -); + self.emit(ValueUpdated { value }); +} ``` -## Emit public logs +Public events are emitted as plaintext logs, similar to Solidity events. -### Emit unstructured data +## Emit unstructured public logs -Emit unstructured public logs using `emit_public_log`: +For unstructured data, use `emit_public_log` directly on the context: ```rust -context.emit_public_log(my_value); -context.emit_public_log([1, 2, 3]); -context.emit_public_log("My message"); +self.context.emit_public_log("My message"); +self.context.emit_public_log([1, 2, 3]); ``` -### Query public events +## Query public logs -Query public events from offchain applications: +Query public logs from offchain applications using the Aztec node: ```typescript const fromBlock = await node.getBlockNumber(); @@ -84,10 +106,15 @@ const logFilter = { const publicLogs = (await node.getPublicLogs(logFilter)).logs; ``` -## Consider costs +## Cost considerations + +Event data published onchain is stored in Ethereum blobs, which incurs costs. Consider: + +- Use `UNCONSTRAINED_OFFCHAIN` delivery for lower costs when you have custom delivery infrastructure +- Only emit events when necessary for your application's functionality -Event data is published to Ethereum as blobs, which incurs costs. Consider: +## Next steps -- Encrypted events are optional - use alternative communication methods if needed -- Future alternatives for data availability may become available -- Balance event utility with cost implications +- Learn about [storage](./how_to_define_storage.md) to persist data in your contracts +- Explore [calling other contracts](./how_to_call_contracts.md) for cross-contract interactions +- Understand [cross-chain communication](./how_to_communicate_cross_chain.md) between Ethereum and Aztec diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_implement_custom_notes.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_implement_custom_notes.md index f4e94b55b918..4b7113842e99 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_implement_custom_notes.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_implement_custom_notes.md @@ -6,464 +6,192 @@ tags: [smart contracts, notes, privacy] keywords: [implementing note, note, custom note] --- -This guide shows you how to create custom note types for storing specialized private data in your Aztec contracts. Notes are the fundamental data structure in Aztec when working with private state. +This guide shows you how to create custom note types for storing specialized private data in your Aztec contracts. ## Prerequisites -- Basic understanding of [Aztec private state](../../foundational-topics/state_management.md) -- Familiarity with [notes and UTXOs](../../foundational-topics/state_management.md) +- Basic understanding of [Aztec private state and notes](../../foundational-topics/state_management.md) - Aztec development environment set up -## Why create custom notes? +## When to create custom notes You may want to create your own note type if you need to: -- Use a specific type of private data or struct not already implemented in Aztec.nr -- Experiment with custom note hashing and nullifier schemes -- Store multiple pieces of related data together (e.g., a card in a game with multiple attributes) -- Optimize storage by combining data that's used together +- Store specific data types not provided by built-in note libraries +- Combine multiple fields into a single note (e.g., game cards with multiple attributes) +- Implement custom nullifier schemes for advanced use cases :::info Built-in Note Types Aztec.nr provides pre-built note types for common use cases: -**ValueNote** - For numeric values like token balances: +**UintNote** - For numeric values like token balances (supports partial notes): ```toml # In Nargo.toml -value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/smart-contracts/value-note" } +uint_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/uint-note" } ``` -```rust -use value_note::value_note::ValueNote; -let note = ValueNote::new(100, owner); +**FieldNote** - For storing single Field values: + +```toml +# In Nargo.toml +field_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/field-note" } ``` **AddressNote** - For storing Aztec addresses: ```toml # In Nargo.toml -address_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/smart-contracts/address-note" } +address_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/address-note" } ``` -```rust -use address_note::address_note::AddressNote; -let note = AddressNote::new(stored_address, owner); -``` - -If these don't meet your needs, continue reading to create your own custom note type. ::: -## Standard note implementation - -### Creating a custom note struct +## Creating a custom note Define your custom note with the `#[note]` macro: -```rust -use aztec::{ - macros::notes::note, - oracle::random::random, - protocol_types::{address::AztecAddress, traits::Packable}, -}; +#include_code nft_note_struct /docs/examples/tutorials/token_bridge_contract/contracts/aztec/nft/src/nft.nr rust -// The #[note] macro marks this struct as a note type -// Required traits: -// - Eq: Allows equality comparisons between notes -// - Packable: Enables efficient packing/unpacking of the note's data -#[derive(Eq, Packable)] -#[note] -pub struct CustomNote { - // Application-specific data - value: Field, - data: u32, - // Required fields for all notes - owner: AztecAddress, // Used for access control and nullifier generation - randomness: Field, // Prevents brute-force attacks on note contents -} -``` +The `#[note]` macro generates the following for your struct: -The `#[note]` macro automatically implements other required traits for your note type (ex. the `NoteHash` trait). +- `NoteType` trait - Provides a unique type ID for the note +- `NoteHash` trait - Handles note hash and nullifier computation +- `NoteProperties` - Enables field selection when querying notes -### Required fields +### Required traits -Every custom note needs these essential fields: +Your note struct must derive: -1. **Application data**: Your specific fields (e.g., `value`, `amount`, `token_id`) -2. **Owner**: Used for nullifier generation and access control (must be `AztecAddress` type) -3. **Randomness**: Prevents brute-force attacks on note contents (must be `Field` type) +- `Packable` - Required by the `#[note]` macro for serialization +- `Eq` - Required by storage types like `PrivateSet` for note comparisons -The order of fields doesn't matter, but convention is to put application data first, then owner, then randomness: +The `#[note]` macro handles the `NoteType`, `NoteHash`, and `NoteProperties` traits automatically. -```rust -#[derive(Eq, Packable)] -#[note] -pub struct MyNote { - // Application-specific data - data: Field, - amount: u128, - - // Required fields - owner: AztecAddress, - randomness: Field, -} -``` +### How note hashing works -### Why randomness matters +When a note is inserted, the `#[note]` macro generates code that computes the note hash by combining: -Without randomness, note contents can be guessed through brute force. For example, if you know someone's Aztec address, you could try hashing it with many potential values to find which note hash in the tree belongs to them. +1. **Your packed note data** - The fields you define in your struct +2. **Owner address** - Provided by the storage variable +3. **Storage slot** - Determined by the storage layout +4. **Randomness** - Generated automatically to prevent brute-force attacks -### Why owner is important +This happens automatically - you don't need to include owner or randomness fields in your struct. -The `owner` field provides two critical functions: +## Using notes in storage -1. **Access control**: Ensures only the owner can spend the note -2. **Privacy from sender**: Prevents the sender from tracking when a note is spent +Notes are stored using `Owned>` which manages note ownership: -Without using the owner's nullifier key, a sender could derive the nullifier offchain and monitor when it appears in the nullifier tree, breaking privacy. +```rust +use aztec::{ + macros::storage::storage, + state_vars::{Owned, PrivateSet}, +}; -### Implementing note methods +#[storage] +struct Storage { + // Collection of notes, indexed by owner + nfts: Owned, Context>, +} +``` -A note is just a Struct, so you can add whatever methods you need. For example, you can add a constructor and helper methods: +### Inserting notes -```rust -impl CustomNote { - pub fn new(value: Field, data: u32, owner: AztecAddress) -> Self { - // Safety: We use randomness to preserve privacy. The sender already knows - // the full note pre-image, so we trust them to cooperate in random generation - let randomness = unsafe { random() }; +#include_code mint /docs/examples/tutorials/token_bridge_contract/contracts/aztec/nft/src/main.nr rust - CustomNote { value, data, owner, randomness } - } +### Reading and removing notes - pub fn get_value(self) -> Field { - self.value - } +Use `pop_notes` to read and nullify notes atomically. This is the recommended pattern for most use cases: - pub fn get_data(self) -> u32 { - self.data - } -} -``` +#include_code burn /docs/examples/tutorials/token_bridge_contract/contracts/aztec/nft/src/main.nr rust + +:::warning +There's also a `get_notes` function that reads without nullifying, but use it with caution - the returned notes may have already been spent in another transaction. +::: + +## Custom note hashing + +Most notes should use the standard `#[note]` macro. Use `#[custom_note]` only when you need: -## Custom note with custom hashing +- Custom nullifier schemes (e.g., notes spendable by anyone with a secret, not tied to an owner) +- Partial notes that can be completed in public execution +- Non-standard hash computation for specific security requirements -For complete control over note hashing and nullifier generation, use the `#[custom_note]` macro: +With `#[custom_note]`, you must implement the `NoteHash` trait yourself: ```rust -use dep::aztec::{ +use aztec::{ context::PrivateContext, macros::notes::custom_note, note::note_interface::NoteHash, protocol_types::{ + address::AztecAddress, constants::{DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_NULLIFIER}, hash::poseidon2_hash_with_separator, traits::Packable, }, }; -// TransparentNote for public-to-private transitions -// No owner field needed - security comes from secret knowledge #[derive(Eq, Packable)] #[custom_note] -pub struct TransparentNote { - amount: u128, - secret_hash: Field, // Hash of a secret that must be known to spend +pub struct CustomHashNote { + pub data: Field, } -impl NoteHash for TransparentNote { - fn compute_note_hash(self, storage_slot: Field) -> Field { - let inputs = self.pack().concat([storage_slot]); - poseidon2_hash_with_separator(inputs, DOM_SEP__NOTE_HASH) +impl NoteHash for CustomHashNote { + fn compute_note_hash( + self, + owner: AztecAddress, + storage_slot: Field, + randomness: Field, + ) -> Field { + // Custom hash computation + poseidon2_hash_with_separator( + [self.data, owner.to_field(), storage_slot, randomness], + DOM_SEP__NOTE_HASH, + ) } - // Custom nullifier that doesn't use owner's key - // Security is enforced by requiring the secret preimage fn compute_nullifier( self, - _context: &mut PrivateContext, + context: &mut PrivateContext, + owner: AztecAddress, note_hash_for_nullification: Field, ) -> Field { + // Standard nullifier using owner's nullifier secret key + let owner_npk_m = aztec::keys::getters::get_public_keys(owner).npk_m; + let secret = context.request_nsk_app(owner_npk_m.hash()); poseidon2_hash_with_separator( - [note_hash_for_nullification], - DOM_SEP__NOTE_NULLIFIER as Field, + [note_hash_for_nullification, secret], + DOM_SEP__NOTE_NULLIFIER, ) } unconstrained fn compute_nullifier_unconstrained( self, - note_hash_for_nullification: Field + owner: AztecAddress, + note_hash_for_nullification: Field, ) -> Field { - self.compute_nullifier(zeroed(), note_hash_for_nullification) - } -} -``` - -This pattern is useful for "shielding" tokens - creating notes in public that can be redeemed in private by anyone who knows the secret. - -## Basic usage in storage - -Before diving into Maps, let's understand basic custom note usage. - -### Declare storage - -```rust -use dep::aztec::state_vars::{PrivateSet, PrivateImmutable}; - -#[storage] -struct Storage { - // Collection of notes for a single owner - balances: PrivateSet, - - // Single immutable configuration - config: PrivateImmutable, -} -``` - -### Insert notes - -```rust -use dep::aztec::messages::message_delivery::MessageDelivery; - -#[external("private")] -fn create_note(value: Field, data: u32) { - let owner = context.msg_sender().unwrap(); - let note = CustomNote::new(value, data, owner); - - storage.balances - .insert(note) - .emit(&mut context, owner, MessageDelivery.CONSTRAINED_ONCHAIN); -} -``` - -### Read notes - -```rust -use dep::aztec::note::note_getter_options::NoteGetterOptions; - -#[external("private")] -fn get_notes() -> BoundedVec { - storage.balances.get_notes(NoteGetterOptions::new()) -} - -#[external("private")] -fn find_note_by_value(target_value: Field) -> CustomNote { - let options = NoteGetterOptions::new() - .select(CustomNote::properties().value, target_value, Option::none()) - .set_limit(1); - - let notes = storage.balances.get_notes(options); - assert(notes.len() == 1, "Note not found"); - notes.get(0) -} -``` - -### Transfer notes - -```rust -#[external("private")] -fn transfer_note(to: AztecAddress, value: Field) { - // Find and remove from sender - let note = find_note_by_value(value); - storage.balances.remove(note); - - // Create new note for recipient - let new_note = CustomNote::new(note.value, note.data, to); - storage.balances.insert(new_note) - .emit(&mut context, to, MessageDelivery.CONSTRAINED_ONCHAIN); -} -``` - -## Using custom notes with Maps - -Maps are essential for organizing custom notes by key in private storage. They allow you to efficiently store and retrieve notes based on addresses, IDs, or other identifiers. - -### Common Map patterns - -```rust -use dep::aztec::{ - macros::notes::note, - oracle::random::random, - protocol_types::{address::AztecAddress, traits::Packable}, - state_vars::{Map, PrivateMutable, PrivateSet}, -}; - -#[derive(Eq, Packable)] -#[note] -pub struct CardNote { - points: u32, - strength: u32, - owner: AztecAddress, - randomness: Field, -} - -impl CardNote { - pub fn new(points: u32, strength: u32, owner: AztecAddress) -> Self { - let randomness = unsafe { random() }; - CardNote { points, strength, owner, randomness } - } -} - -#[storage] -struct Storage { - // Map from player address to their collection of cards - card_collections: Map, Context>, - - // Map from player address to their active card - active_cards: Map, Context>, - - // Nested maps: game_id -> player -> cards - game_cards: Map, Context>, Context>, -} -``` - -Common patterns: - -- `Map>` - Multiple notes per user (like token balances, card collections) -- `Map>` - Single note per user (like user profile, active state) -- `Map>>` - Nested organization (game sessions, channels) - -### Inserting into mapped PrivateSets - -To add notes to a mapped PrivateSet: - -```rust -use dep::aztec::messages::message_delivery::MessageDelivery; - -#[external("private")] -fn add_card_to_collection(player: AztecAddress, points: u32, strength: u32) { - let card = CardNote::new(points, strength, player); - - // Insert into the player's collection - storage.card_collections - .at(player) - .insert(card) - .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); -} -``` - -### Using mapped PrivateMutable - -For PrivateMutable in a Map, handle both initialization and updates: - -```rust -use dep::aztec::messages::message_delivery::MessageDelivery; - -#[external("private")] -fn set_active_card(player: AztecAddress, points: u32, strength: u32) { - // Check if already initialized - let is_initialized = storage.active_cards.at(player).is_initialized(); - - if is_initialized { - // Replace existing card - storage.active_cards - .at(player) - .replace(|_old_card| CardNote::new(points, strength, player)) - .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); - } else { - // Initialize for first time - let card = CardNote::new(points, strength, player); - storage.active_cards - .at(player) - .initialize(card) - .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); - } -} -``` - -### Reading from mapped PrivateSets - -```rust -use dep::aztec::note::note_getter_options::NoteGetterOptions; - -#[external("private")] -fn get_player_cards(player: AztecAddress) -> BoundedVec { - // Get all cards for this player - storage.card_collections - .at(player) - .get_notes(NoteGetterOptions::new()) -} - -#[external("private")] -fn get_total_points(player: AztecAddress) -> u32 { - let options = NoteGetterOptions::new(); - let notes = storage.card_collections.at(player).get_notes(options); - - let mut total = 0; - for i in 0..notes.len() { - let card = notes.get(i); - total += card.points; + let owner_npk_m = aztec::keys::getters::get_public_keys(owner).npk_m; + let secret = aztec::keys::getters::get_nsk_app(owner_npk_m.hash()); + poseidon2_hash_with_separator( + [note_hash_for_nullification, secret], + DOM_SEP__NOTE_NULLIFIER, + ) } - total -} -``` - -### Reading from mapped PrivateMutable - -```rust -#[external("private")] -fn get_active_card(player: AztecAddress) -> CardNote { - storage.active_cards.at(player).get_note() } ``` -### Filtering notes in Maps +## Viewing notes (unconstrained) -Filter notes by their fields when reading from maps: +For read-only queries without constraints: -```rust -use dep::aztec::{note::note_getter_options::NoteGetterOptions, utils::comparison::Comparator}; - -#[external("private")] -fn find_strong_cards(player: AztecAddress, min_strength: u32) -> BoundedVec { - let options = NoteGetterOptions::new() - .select(CardNote::properties().strength, Comparator.GTE, min_strength) - .set_limit(10); - - storage.card_collections.at(player).get_notes(options) -} -``` - -### Working with nested Maps - -Navigate nested map structures to organize data hierarchically: - -```rust -use dep::aztec::messages::message_delivery::MessageDelivery; - -#[external("private")] -fn add_card_to_game( - game_id: Field, - player: AztecAddress, - points: u32, - strength: u32 -) { - let card = CardNote::new(points, strength, player); - - // Navigate nested maps: game_cards[game_id][player] - storage.game_cards - .at(game_id) - .at(player) - .insert(card) - .emit(&mut context, player, MessageDelivery.CONSTRAINED_ONCHAIN); -} - -#[external("private")] -fn get_game_cards( - game_id: Field, - player: AztecAddress -) -> BoundedVec { - storage.game_cards - .at(game_id) - .at(player) - .get_notes(NoteGetterOptions::new()) -} -``` +#include_code view_notes /noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr rust ## Further reading -- [What the `#[note]` macro does](../../aztec-nr/framework-description/functions/attributes.md#implementing-notes) -- [Note lifecycle and nullifiers](../../foundational-topics/advanced/storage/indexed_merkle_tree.mdx) -- [Advanced note patterns](./advanced/how_to_retrieve_filter_notes.md) -- [Note portals for L1 communication](./how_to_communicate_cross_chain.md) -- [Macros reference](../../aztec-nr/framework-description/macros.md) -- [Keys, including npk_m_hash](../../foundational-topics/accounts/keys.md) +- [What the `#[note]` macro does](./functions/attributes.md#implementing-notes) +- [Note getter options](./advanced/how_to_retrieve_filter_notes.md) +- [Storage types](../../foundational-topics/state_management.md) +- [Macros reference](./macros.md) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_use_authwit.md b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_use_authwit.md index 9adace2f3f47..355d061bf9d0 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/how_to_use_authwit.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/how_to_use_authwit.md @@ -11,85 +11,62 @@ Authentication witnesses (authwit) allow other contracts to execute actions on b - An Aztec contract project set up with `aztec-nr` dependency - Understanding of private and public functions in Aztec -- Access to the `authwit` library in your contract For conceptual background, see [Authentication Witnesses](../../foundational-topics/advanced/authwit.md). -## Set up the authwit library +## Import the authwit library -Add the `authwit` library to your `Nargo.toml` file: +The `aztec` library includes authwit functionality. Import the necessary components: -```toml -[dependencies] -aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/smart-contracts/aztec" } +```rust +use aztec::{ + authwit::auth::{compute_authwit_message_hash_from_call, set_authorized}, + macros::functions::authorize_once, +}; ``` -Import the authwit library in your contract: +## Using the `authorize_once` macro -```rust -use aztec::authwit::auth::compute_authwit_nullifier; -``` +The `#[authorize_once]` macro validates that a caller has authorization from the `from` address. It handles authwit verification and nullifier emission automatically. -## Implement authwit in private functions +### Private function example -### Validate authentication in a private function +#include_code transfer_in_private noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust -Check if the current call is authenticated using the `authorize_once` macro: +### Public function example -```rust -#[authorize_once("from", "authwit_nonce")] -#[external("private")] -fn execute_private_action( - from: AztecAddress, - to: AztecAddress, - value: u128, - authwit_nonce: Field, -) { - storage.values.at(from).sub(from, value).deliver(encode_and_encrypt_note(&mut context, from)); - storage.values.at(to).add(to, value).deliver(encode_and_encrypt_note(&mut context, to)); -} -``` +#include_code transfer_in_public noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +The macro parameters specify: + +- `"from"` - the parameter name containing the address that must have authorized the call +- `"authwit_nonce"` - the parameter name containing the nonce for replay protection + +## Setting authorization from contracts -This allows anyone with a valid authwit (created by `from`) to execute an action on its behalf. +When a contract needs to authorize another contract to act on its behalf, use `set_authorized` to update the auth registry. This is common in bridge contracts where contract A authorizes contract B to perform actions. -## Set approval state from contracts +#include_code authwit_uniswap_set noir-projects/noir-contracts/contracts/app/uniswap_contract/src/main.nr rust -Enable contracts to approve actions on their behalf by updating the public auth registry: +Key steps: 1. Compute the message hash using `compute_authwit_message_hash_from_call` -2. Set the authorization using `set_authorized` +2. Call `set_authorized` to store the approval in the registry +3. Execute the authorized action -This pattern is commonly used in bridge contracts (like the [uniswap example contract](https://github.com/AztecProtocol/aztec-packages/tree/next/noir-projects/noir-contracts/contracts/app/uniswap_contract)) where one contract needs to authorize another to perform actions on its behalf: +When authorization and consumption happen in the same transaction, state changes are squashed, saving gas. -```rust -#[external("public")] -#[only_self] -fn _approve_and_execute_action( - target_contract: AztecAddress, - bridge_contract: AztecAddress, - value: u128, -) { - // Since we will authorize and instantly execute the action, all in public, we can use the same nonce - // every interaction. In practice, the authwit should be squashed, so this is also cheap! - let authwit_nonce = 0xdeadbeef; - - let selector = FunctionSelector::from_signature("execute_action((Field),u128,Field)"); - let message_hash = compute_authwit_message_hash_from_call( - bridge_contract, - target_contract, - context.chain_id(), - context.version(), - selector, - [context.this_address().to_field(), value as Field, authwit_nonce], - ); - - // We need to make a call to update it. - set_authorized(&mut context, message_hash, true); - - let this_address = storage.my_address.read(); - // Execute the action! - OtherContract::at(bridge_contract) - .execute_external_action(this_address, value, this_address, authwit_nonce) - .call(&mut context) -} -``` +## Canceling authwits + +Users can revoke an authwit before it's used by emitting its nullifier: + +#include_code cancel_authwit noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust + +:::note +The cancel transaction must be finalized before any transaction attempts to use the authwit. If both are pending simultaneously, the outcome depends on which the sequencer includes first. +::: + +## Next steps + +- [Using authwits in aztec.js](../../aztec-js/how_to_use_authwit.md) - Create and manage authwits from your client application +- [Authentication Witnesses concepts](../../foundational-topics/advanced/authwit.md) - Deeper explanation of the authwit mechanism diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/macros.md b/docs/docs-developers/docs/aztec-nr/framework-description/macros.md index ca87fdc6aca2..b19d93d3f79f 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/macros.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/macros.md @@ -1,25 +1,50 @@ --- -title: Aztec macros +title: Aztec Macros description: Learn about macros available in Aztec.nr for code generation and abstraction. sidebar_position: 8 tags: [contracts, functions] --- -## All Aztec macros +Aztec.nr provides macros (attributes) that transform your code during compilation to handle the complexities of private execution, proof generation, and state management. -In addition to the function macros in Noir, Aztec also has its own macros for specific functions. An Aztec contract function can be annotated with more than 1 macro. -It is also worth mentioning Noir's `unconstrained` function type [here (Noir docs page)](https://noir-lang.org/docs/noir/concepts/unconstrained/). +## Quick reference -- `#[aztec]` - Defines a contract, placed above `contract ContractName{}` -- `#[external("...")]` - Whether the function is to be callable from outside the contract. There are 3 types of external functions: `#[external("public")]`, `#[external("private")]` or `#[external("utility")]` - The type of external defines whether the function is to be executed from a public, private or utility context (see Further Reading) -- `#[initializer]` - If one or more functions are marked as an initializer, then one of them must be called before any non-initializer functions -- `#[noinitcheck]` - The function is able to be called before an initializer (if one exists) -- `#[view]` - Makes calls to the function static -- `#[only_self]` - Available only for `external` functions - any external caller except the current contract is rejected. -- `#[internal]` - Function can only be called from within the contract and the call itself is inlined (e.g. akin to EVM's JUMP and not EVM's CALL) -- `#[note]` - Creates a custom note -- `#[storage]` - Defines contract storage +### Contract + +| Attribute | Purpose | +|-----------|---------| +| `#[aztec]` | Marks a module as an Aztec contract | + +### Functions + +| Attribute | Purpose | +|-----------|---------| +| `#[external("private")]` | Client-side private execution with proofs | +| `#[external("public")]` | Sequencer-side public execution | +| `#[external("utility")]` | Unconstrained queries, not included in transactions | +| `#[internal("private")]` | Private helper, inlined at call sites | +| `#[internal("public")]` | Public helper, inlined at call sites | +| `#[view]` | Prevents state modification | +| `#[initializer]` | Contract constructor | +| `#[noinitcheck]` | Callable before contract initialization | +| `#[nophasecheck]` | Skips transaction phase validation | +| `#[only_self]` | Only callable by the same contract | +| `#[authorize_once]` | Requires authwit authorization with replay protection | + +Functions can have multiple attributes (e.g., `#[external("public")]` with `#[view]` and `#[only_self]`). + +### Structs + +| Attribute | Purpose | +|-----------|---------| +| `#[note]` | Defines a private note type | +| `#[custom_note]` | Note with custom hash/nullifier logic | +| `#[storage]` | Defines contract storage layout | +| `#[storage_no_init]` | Storage with manual slot allocation | + +For detailed explanations and examples, see the [Attributes and Macros reference](./functions/attributes.md). ## Further reading -[How do Aztec macros work?](../../aztec-nr/framework-description/functions/function_transforms.md) +- [Attributes and Macros reference](./functions/attributes.md) - detailed documentation for each macro +- [Inner workings of functions](./functions/function_transforms.md) - how macros transform your code diff --git a/docs/docs-developers/docs/aztec-nr/how_to_compile_contract.md b/docs/docs-developers/docs/aztec-nr/how_to_compile_contract.md index 44d49be93aef..a7d63c90f4a5 100644 --- a/docs/docs-developers/docs/aztec-nr/how_to_compile_contract.md +++ b/docs/docs-developers/docs/aztec-nr/how_to_compile_contract.md @@ -32,14 +32,22 @@ The compiler automatically generates type-safe interfaces for contract interacti Use generated interfaces instead of manual function calls: ```rust -contract FPC { +contract MyContract { use dep::token::Token; #[external("private")] - fn fee_entrypoint_private(amount: Field, asset: AztecAddress, secret_hash: Field, nonce: Field) { - assert(asset == storage.other_asset.read()); - Token::at(asset).transfer_to_public(context.msg_sender(), context.this_address(), amount, nonce).call(&mut context); - FPC::at(context.this_address()).pay_fee_with_shielded_rebate(amount, asset, secret_hash).enqueue(&mut context); + fn transfer_tokens(token_address: AztecAddress, recipient: AztecAddress, amount: u128) { + // Use the generated Token interface to call another contract + self.call(Token::at(token_address).transfer(recipient, amount)); + } + + #[external("private")] + fn transfer_then_mint(token_address: AztecAddress, recipient: AztecAddress, amount: u128) { + // Private call executed immediately + self.call(Token::at(token_address).transfer(recipient, amount)); + + // Public call enqueued for later execution + self.enqueue(Token::at(token_address).mint_to_public(recipient, amount)); } } ``` diff --git a/docs/docs-developers/docs/aztec-nr/how_to_test_contracts.md b/docs/docs-developers/docs/aztec-nr/how_to_test_contracts.md index 519371edfb2a..9c047f7af815 100644 --- a/docs/docs-developers/docs/aztec-nr/how_to_test_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/how_to_test_contracts.md @@ -233,8 +233,8 @@ Authwits require **contract accounts**, not light accounts. ```rust use aztec::test::helpers::authwit::{ - add_private_authwit_from_call_interface, - add_public_authwit_from_call_interface, + add_private_authwit_from_call, + add_public_authwit_from_call, }; ``` @@ -252,7 +252,7 @@ unconstrained fn test_private_authwit() { let burn_call = Token::at(token_address).burn_private(owner, amount, nonce); // Grant authorization from owner to spender - add_private_authwit_from_call_interface(owner, spender, burn_call); + add_private_authwit_from_call(env, owner, spender, burn_call); // Spender can now execute the authorized action env.call_private(spender, burn_call); @@ -261,7 +261,7 @@ unconstrained fn test_private_authwit() { ### Public authwits -````rust +```rust #[test] unconstrained fn test_public_authwit() { let (env, token_address, owner, spender) = setup(true); @@ -270,11 +270,12 @@ unconstrained fn test_public_authwit() { let transfer_call = Token::at(token_address).transfer_public(owner, recipient, 100, nonce); // Grant public authorization - add_public_authwit_from_call_interface(owner, spender, transfer_call); + add_public_authwit_from_call(env, owner, spender, transfer_call); // Execute with authorization env.call_public(spender, transfer_call); } +``` ## Time traveling diff --git a/docs/docs-developers/docs/aztec-nr/index.md b/docs/docs-developers/docs/aztec-nr/index.md index 4a49d03fb5b1..de1d7711eb7c 100644 --- a/docs/docs-developers/docs/aztec-nr/index.md +++ b/docs/docs-developers/docs/aztec-nr/index.md @@ -15,7 +15,7 @@ help you write Noir programs to deploy on the Aztec network. ### Prerequisites - Install [Aztec Local Network and Tooling](../../getting_started_on_local_network.md) -- Install the [Noir LSP](../aztec-nr/installation.md) for your editor. +- Install the [Noir VSCode Extension](./installation.md) for syntax highlighting and error detection. ### Flow @@ -25,7 +25,7 @@ help you write Noir programs to deploy on the Aztec network. ```toml # Nargo.toml [dependencies] -aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/smart-contracts/aztec" } +aztec = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="aztec" } ``` Update your `main.nr` contract file to use the Aztec.nr macros for writing contracts. diff --git a/docs/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md b/docs/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md index 731b5b90d6b7..a7eea2e93cfe 100644 --- a/docs/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md +++ b/docs/docs-developers/docs/foundational-topics/advanced/circuits/public_execution.md @@ -73,5 +73,5 @@ These snapshots are validated in the rollup circuits to ensure continuity across ## Related Pages - [Private Kernel](./private_kernel.md) – How private functions are processed -- [Private - Public Communication](../../../aztec-nr/framework-description/functions/public_private_calls.md) – How private and public functions interact +- [Call Types](../../call_types.md) – How private and public functions interact - [State Management](../../state_management.md) – How public and private state works diff --git a/docs/docs-developers/docs/foundational-topics/transactions.md b/docs/docs-developers/docs/foundational-topics/transactions.md index ca8407f449bc..615b7f8fd024 100644 --- a/docs/docs-developers/docs/foundational-topics/transactions.md +++ b/docs/docs-developers/docs/foundational-topics/transactions.md @@ -36,7 +36,7 @@ The following diagram provides a more detailed overview of the transaction execu -See the page on [contract communication](../aztec-nr/framework-description/functions/public_private_calls.md) for more context on transaction execution. +See the page on [call types](./call_types.md) for more context on transaction execution. ### Transaction Requests diff --git a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr index 40aa5bf1665c..4ea4c0e6f3fe 100644 --- a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr @@ -2,8 +2,10 @@ use dep::aztec::macros::aztec; #[aztec] pub contract Claim { + // docs:start:history_import + use dep::aztec::history::note_inclusion::ProveNoteInclusion; + // docs:end:history_import use dep::aztec::{ - history::note_inclusion::ProveNoteInclusion, macros::{functions::{external, initializer}, storage::storage}, note::{ note_interface::NoteHash, retrieved_note::RetrievedNote, @@ -50,8 +52,10 @@ pub contract Claim { // slot of the note as there is no risk of claiming with a note that is not a donation note. // 3) Prove that the note hash exists in the note hash tree + // docs:start:prove_note_inclusion let header = self.context.get_anchor_block_header(); header.prove_note_inclusion(proof_retrieved_note); + // docs:end:prove_note_inclusion // 4) Compute and emit a nullifier which is unique to the note and this contract to ensure the reward can be // claimed only once with the given note. diff --git a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr index ea7ad6c9b30f..4c8aaf597116 100644 --- a/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr @@ -249,10 +249,12 @@ pub contract NFT { ) { let nfts = self.storage.private_nfts; + // docs:start:pop_notes let notes = nfts.at(from).pop_notes(NoteGetterOptions::new() .select(NFTNote::properties().token_id, Comparator.EQ, token_id) .set_limit(1)); assert(notes.len() == 1, "NFT not found when transferring"); + // docs:end:pop_notes let new_note = NFTNote { token_id }; @@ -292,6 +294,7 @@ pub contract NFT { /// Returns an array of token IDs owned by `owner` in private and a flag indicating whether a page limit was /// reached. Starts getting the notes from page with index `page_index`. Zero values in the array are placeholder /// values for non-existing notes. + // docs:start:view_notes #[external("utility")] unconstrained fn get_private_nfts( owner: AztecAddress, @@ -311,4 +314,5 @@ pub contract NFT { let page_limit_reached = notes.len() == options.limit; (owned_nft_ids, page_limit_reached) } + // docs:end:view_notes } diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr index 1f9d3f886970..36431a5ea7d6 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr @@ -161,6 +161,7 @@ pub contract Token { self.storage.total_supply.write(supply); } + // docs:start:transfer_in_public #[authorize_once("from", "authwit_nonce")] #[external("public")] fn transfer_in_public( @@ -174,6 +175,7 @@ pub contract Token { let to_balance = self.storage.public_balances.at(to).read().add(amount); self.storage.public_balances.at(to).write(to_balance); } + // docs:end:transfer_in_public #[authorize_once("from", "authwit_nonce")] #[external("public")] @@ -290,13 +292,16 @@ pub contract Token { * Cancel a private authentication witness. * @param inner_hash The inner hash of the authwit to cancel. */ + // docs:start:cancel_authwit #[external("private")] fn cancel_authwit(inner_hash: Field) { let on_behalf_of = self.msg_sender().unwrap(); let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); self.context.push_nullifier(nullifier); } + // docs:end:cancel_authwit + // docs:start:transfer_in_private #[authorize_once("from", "authwit_nonce")] #[external("private")] fn transfer_in_private( @@ -310,6 +315,7 @@ pub contract Token { // docs:end:increase_private_balance self.storage.balances.at(to).add(amount).deliver(MessageDelivery.CONSTRAINED_ONCHAIN); } + // docs:end:transfer_in_private #[authorize_once("from", "authwit_nonce")] #[external("private")] @@ -344,6 +350,7 @@ pub contract Token { /// us to have it inlined in the `transfer_to_private` function which results in one fewer kernel iteration. Note /// that in this case we don't pass `completer` as an argument to this function because in all the callsites we /// want to use the message sender as the completer anyway. + // docs:start:prepare_private_balance_increase #[internal("private")] fn _prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { let partial_note = UintNote::partial( @@ -356,6 +363,7 @@ pub contract Token { partial_note } + // docs:end:prepare_private_balance_increase /// Finalizes a transfer of token `amount` from public balance of `msg_sender` to a private balance of `to`. /// The transfer must be prepared by calling `prepare_private_balance_increase` from `msg_sender` account and @@ -411,6 +419,7 @@ pub contract Token { // In all the flows in this contract, `from` (the account from which we're subtracting the `amount`) and // `completer` (the entity that can complete the partial note) are the same so we represent them with a single // argument. + // docs:start:finalize_transfer_to_private #[internal("public")] fn _finalize_transfer_to_private( from_and_completer: AztecAddress, @@ -426,6 +435,7 @@ pub contract Token { // We finalize the transfer by completing the partial note. partial_note.complete(self.context, from_and_completer, amount); } + // docs:end:finalize_transfer_to_private /// Mints token `amount` to a private balance of `to`. Message sender has to have minter permissions (checked /// in the enqueued call). diff --git a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr index c5cff21ddd17..42bf20ea231f 100644 --- a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr +++ b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/filter.nr @@ -1,11 +1,14 @@ +// docs:start:custom_filter_imports use aztec::{ note::retrieved_note::RetrievedNote, protocol_types::{ constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, utils::field::full_field_less_than, }, }; +// docs:end:custom_filter_imports use field_note::field_note::FieldNote; +// docs:start:custom_filter pub fn filter_notes_min_sum( notes: [Option>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL], min_sum: Field, @@ -23,3 +26,4 @@ pub fn filter_notes_min_sum( selected } +// docs:end:custom_filter