Skip to content

Commit

Permalink
feat(docs): getting started, portals page, some other nits (#8515)
Browse files Browse the repository at this point in the history
Please read [contributing guidelines](CONTRIBUTING.md) and remove this
line.

closes AztecProtocol/dev-rel#387
closes AztecProtocol/dev-rel#394 
closes AztecProtocol/dev-rel#379 
closes AztecProtocol/dev-rel#395
closes AztecProtocol/dev-rel#392

tried to update algolia stuff (without paying) but im not sure it worsk
:(

---------

Co-authored-by: josh crites <jc@joshcrites.com>
Co-authored-by: josh crites <critesjosh@gmail.com>
  • Loading branch information
3 people committed Sep 20, 2024
1 parent 8dfdebc commit 9632e0d
Show file tree
Hide file tree
Showing 38 changed files with 553 additions and 182 deletions.
4 changes: 3 additions & 1 deletion docs/docs/aztec/concepts/accounts/authwit.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Authentication Witness
title: Authentication Witness (Authwit)
tags: [accounts, authwit]
importance: 1
keywords: [authwit, authentication witness, accounts]
---

Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (eg protocols or other users) to execute an action on their behalf.
Expand Down
156 changes: 156 additions & 0 deletions docs/docs/aztec/concepts/communication/cross_chain_calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
id: portals
title: L1 <--> L2 communication (Portals)
description: "This page is a conceptual introduction to Portals, how Aztec communicates with L1 (Ethereum)"
keywords: [portals]
tags: [portals, protocol, ethereum]
importance: 1
---

# L1-L2 Communication (Portals)

import Image from "@theme/IdealImage";

In Aztec, what we call _portals_ are the key element in facilitating communication between L1 and L2. While typical L2 solutions rely on synchronous communication with L1, Aztec's privacy-first nature means this is not possible. You can learn more about why in the previous section.

Traditional L1 \<-\> L2 communication might involve direct calls between L2 nd L1 contracts. However, in Aztec, due to the privacy components and the way transactions are processed (kernel proofs built on historical data), direct calls between L1 and L2 would not be possible if we want to maintain privacy.

Portals are the solution to this problem, acting as bridges for communication between the two layers. These portals can transmit messages from public functions in L1 to private functions in L2 and vice versa, thus enabling messaging while maintaining privacy.

This page covers:

- How portals enable privacy communication between L1 and L2
- How messages are sent, received, and processed
- Message Boxes and how they work
- How and why linking of contracts between L1 and L2 occurs

## Objective

The goal is to set up a minimal-complexity mechanism, that will allow a base-layer (L1) and the Aztec Network (L2) to communicate arbitrary messages such that:

- L2 functions can `call` L1 functions.
- L1 functions can `call` L2 functions.
- The rollup-block size have a limited impact by the messages and their size.

## High Level Overview

This document will contain communication abstractions that we use to support interaction between _private_ functions, _public_ functions and Layer 1 portal contracts.

Fundamental restrictions for Aztec:

- L1 and L2 have very different execution environments, stuff that is cheap on L1 is most often expensive on L2 and vice versa. As an example, `keccak256` is cheap on L1, but very expensive on L2.
- _Private_ function calls are fully "prepared" and proven by the user, which provides the kernel proof along with commitments 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 Aztec are therefore ordered such that first _private_ functions are executed, and then _public_. For a more detailed description of why, see above.
- Messages are consumables, and can only be consumed by the recipient. See [Message Boxes](#message-boxes) for more information.

With the aforementioned restrictions taken into account, cross-chain messages can be operated in a similar manner to when _public_ functions must transmit information to _private_ functions. In such a scenario, a "message" is created and conveyed to the recipient for future use. It is worth noting that any call made between different domains (_private, public, cross-chain_) is unilateral in nature. In other words, the caller is unaware of the outcome of the initiated call until told when some later rollup is executed (if at all). This can be regarded as message passing, providing us with a consistent mental model across all domains, which is convenient.

As an illustration, suppose a private function adds a cross-chain call. In such a case, the private function would not have knowledge of the result of the cross-chain call within the same rollup (since it has yet to be executed).

Similarly to the ordering of private and public functions, we can also reap the benefits of intentionally ordering messages between L1 and L2. When a message is sent from L1 to L2, it has been "emitted" by an action in the past (an L1 interaction), allowing us to add it to the list of consumables at the "beginning" of the block execution. This practical approach means that a message could be consumed in the same block it is included. In a sophisticated setup, rollup $n$ could send an L2 to L1 message that is then consumed on L1, and the response is added already in $n+1$. However, messages going from L2 to L1 will be added as they are emitted.

:::info
Because everything is unilateral and async, the application developer have to explicitly handle failure cases such that user can gracefully recover. Example where recovering is of utmost importance is token bridges, where it is very inconvenient if the locking of funds on one domain occur, but never the minting or unlocking on the other.
:::

## Components

### Portal

A "portal" refers to the part of an application residing on L1, which is associated with a particular L2 address (the confidential part of the application). It could be a contract or even an EOA on L1.

### Message Boxes

In a logical sense, a Message Box functions as a one-way message passing mechanism with two ends, one residing on each side of the divide, i.e., one component on L1 and another on L2. Essentially, these boxes are utilized to transmit messages between L1 and L2 via the rollup contract. The boxes can be envisaged as multi-sets that enable the same message to be inserted numerous times, a feature that is necessary to accommodate scenarios where, for instance, "deposit 10 eth to A" is required multiple times. The diagram below provides a detailed illustration of how one can perceive a message box in a logical context.

<Image img={require("/img/com-abs-5.png")} />

- Here, a `sender` will insert a message into the `pending` set, the specific constraints of the actions depend on the implementation domain, but for now, say that anyone can insert into the pending set.
- At some point, a rollup will be executed, in this step messages are "moved" from pending on Domain A, to ready on Domain B. Note that consuming the message is "pulling & deleting" (or nullifying). The action is atomic, so a message that is consumed from the pending set MUST be added to the ready set, or the state transition should fail. A further constraint on moving messages along the way, is that only messages where the `sender` and `recipient` pair exists in a leaf in the contracts tree are allowed!
- When the message has been added to the ready set, the `recipient` can consume the message as part of a function call.

A difference when compared to other cross-chain setups, is that Aztec is "pulling" messages, and that the message doesn't need to be calldata for a function call. For other rollups, execution is happening FROM the "message bridge", which then calls the L1 contract. For Aztec, you call the L1 contract, and it should then consume messages from the message box.

Why? _Privacy_! When pushing, we would be needing full `calldata`. Which for functions with private inputs is not really something we want as that calldata for L1 -> L2 transactions are committed to on L1, e.g., publicly sharing the inputs to a private function.

By instead pulling, we can have the "message" be something that is derived from the arguments instead. This way, a private function to perform second half of a deposit, leaks the "value" deposited and "who" made the deposit (as this is done on L1), but the new owner can be hidden on L2.

To support messages in both directions we require two of these message boxes (one in each direction). However, due to the limitations of each domain, the message box for sending messages into the rollup and sending messages out are not fully symmetrical. In reality, the setup looks closer to the following:

<Image img={require("/img/com-abs-6.png")} />

:::info
The L2 -> L1 pending messages set only exist logically, as it is practically unnecessary. For anything to happen to the L2 state (e.g., update the pending messages), the state will be updated on L1, meaning that we could just as well insert the messages directly into the ready set.
:::

### Rollup Contract

The rollup contract has a few very important responsibilities. The contract must keep track of the _L2 rollup state root_, perform _state transitions_ and ensure that the data is available for anyone else to synchronize to the current state.

To ensure that _state transitions_ are performed correctly, the contract will derive public inputs for the **rollup circuit** based on the input data, and then use a _verifier_ contract to validate that inputs correctly transition the current state to the next. All data needed for the public inputs to the circuit must be from the rollup block, ensuring that the block is available. For a valid proof, the _rollup state root_ is updated and it will emit an _event_ to make it easy for anyone to find the data.

As part of _state transitions_ where cross-chain messages are included, the contract must "move" messages along the way, e.g., from "pending" to "ready".

### Kernel Circuit

For L2 to L1 messages, the public inputs of a user-proof will contain a dynamic array of messages to be added, of size at most `MAX_MESSAGESTACK_DEPTH`, limited to ensure it is not impossible to include the transaction. The circuit must ensure, that all messages have a `sender/recipient` pair, and that those pairs exist in the contracts tree and that the `sender` is the L2 contract that actually emitted the message.
For consuming L1 to L2 messages the circuit must create proper nullifiers.

### Rollup Circuit

The rollup circuit must ensure that, provided two states $S$ and $S'$ and the rollup block $B$, applying $B$ to $S$ using the transition function must give us $S'$, e.g., $T(S, B) \mapsto S'$. If this is not the case, the constraints are not satisfied.

For the sake of cross-chain messages, this means inserting and nullifying L1 $\rightarrow$ L2 in the trees, and publish L2 $\rightarrow$ L1 messages on chain. These messages should only be inserted if the `sender` and `recipient` match an entry in the contracts leaf (as checked by the kernel).

### Messages

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 struct's 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,
}
```

:::info
The `bytes32` elements for `content` and `secretHash` hold values that must fit in a field element (~ 254 bits).
:::

:::info
The nullifier computation should include the index of the message in the message tree to ensure that it is possible to send duplicate messages (e.g., 2 x deposit of 500 dai to the same account).

To make it possible to hide when a specific message is consumed, the `L1ToL2Msg` is extended with a `secretHash` field, where the `secretPreimage` is used as part of the nullifier computation. This way, it is not possible for someone just seeing the `L1ToL2Msg` on L1 to know when it is consumed on L2.
:::

## Combined Architecture

The following diagram shows the overall architecture, combining the earlier sections.

<Image img={require("/img/com-abs-7.png")} />

## Learn more

Check out the [protocol specs](../../../protocol-specs/l1-smart-contracts#portals) for more information about cross-chain communication and contracts on L1.
12 changes: 12 additions & 0 deletions docs/docs/aztec/concepts/communication/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Communication
sidebar_position: 5
---

## Cross-chain communication

See [L1 \<--\> L2 communication (Portals)](./cross_chain_calls.md) for information about how Aztec communications with L1 (Ethereum) through Portals.

## Private / Public execution

For in-depth information about how private and public functions can call each other, read the [Smart Contracts section](../../smart_contracts/functions/public_private_calls.md).
2 changes: 2 additions & 0 deletions docs/docs/aztec/concepts/pxe/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Private Execution Environment (PXE)
sidebar_position: 6
tags: [PXE]
keywords: [pxe, private execution environment]
importance: 1
---

The Private Execution Environment (or PXE, pronounced 'pixie') is a client-side library for the execution of private operations. It is a TypeScript library and can be run within Node, such as when you run the sandbox. In the future it could be run inside wallet software or a browser.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/aztec/concepts/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Transactions on Aztec start with a call from Aztec.js, which creates a request c

See [this diagram](https://raw.githubusercontent.com/AztecProtocol/aztec-packages/2fa143e4d88b3089ebbe2a9e53645edf66157dc8/docs/static/img/sandbox_sending_a_tx.svg) for a more detailed overview of the transaction execution process. It highlights 3 different types of transaction execution: contract deployments, private transactions and public transactions.

See the page on [contract communication](../smart_contracts/communication/index.md) for more context on transaction execution.
See the page on [contract communication](../smart_contracts/functions/public_private_calls.md) for more context on transaction execution.

### Enabling Transaction Semantics: The Aztec Kernel

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/aztec/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ Explore the Concepts for a deeper understanding into the components that make up
### Start coding

<div>
<Card shadow='tl' link='/guides/developer_guides/getting_started/quickstart'>
<Card shadow='tl' link='/guides/developer_guides/getting_started'>
<CardHeader>
<h3>Developer quickstart</h3>
<h3>Developer Getting Started Guide</h3>
</CardHeader>
<CardBody>
Follow the getting started guide to start developing with the Aztec Sandbox
Expand Down
12 changes: 0 additions & 12 deletions docs/docs/aztec/smart_contracts/communication/index.md

This file was deleted.

6 changes: 3 additions & 3 deletions docs/docs/aztec/smart_contracts/functions/inner_workings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Inner Workings of Functions and Macros
sidebar_position: 3
sidebar_position: 4
tags: [functions]
---

Expand Down Expand Up @@ -79,14 +79,14 @@ This function takes the application context, and converts it into the `PrivateCi

Unconstrained functions are an underlying part of Noir. In short, they are functions which are not directly constrained and therefore should be seen as un-trusted. That they are un-trusted means that the developer must make sure to constrain their return values when used. Note: Calling an unconstrained function from a private function means that you are injecting unconstrained values.

Defining a function as `unconstrained` tells Aztec to simulate it completely client-side in the [ACIR simulator](../../../aztec/concepts/pxe/index.md) without generating proofs. They are useful for extracting information from a user through an [oracle](../oracles/index.md).
Defining a function as `unconstrained` tells Aztec to simulate it completely client-side in the [ACIR simulator](../../concepts/pxe/index.md) without generating proofs. They are useful for extracting information from a user through an [oracle](../oracles/index.md).

When an unconstrained 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 blockheader from the [PXE database](../../../aztec/concepts/pxe/index.md#database) and passes it along with the contract address to `ViewDataOracle`. This creates a context that simulates the state of the blockchain at a specific block, allowing the unconstrained function to access and interact with blockchain data as it would appear in that block, but without affecting the actual blockchain state.
To generate the environment, the simulator gets the blockheader from the [PXE database](../../concepts/pxe/index.md#database) and passes it along with the contract address to `ViewDataOracle`. This creates a context that simulates the state of the blockchain at a specific block, allowing the unconstrained 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, `execute_unconstrained_function` is invoked:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
---
title: Private <> Public Communication
sidebar_position: 3
tags: [functions]
---


import Image from "@theme/IdealImage";

import Disclaimer from "@site/src/components/Disclaimers/\_wip_disclaimer.mdx";
Expand Down
Loading

0 comments on commit 9632e0d

Please sign in to comment.