Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial portal docs + minor cleanups #1469

Merged
merged 8 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 0 additions & 134 deletions docs/docs/concepts/foundation/communication/cross_chain_calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,140 +173,6 @@ With many L2's reading from the same L1, we can also more easily setup generic b



## Standards

### Structure of messages
The application developer should consider creating messages that follow a structure like function calls, e.g., using a function signature and arguments. This will make it easier for the developer to ensure that they are not making messages that could be misinterpreted by the recipient. Example could be using `amount, token_address, recipient_address` as the message for a withdraw function and `amount, token_address, on_behalf_of_address` for a deposit function. Any deposit could then also be mapped to a withdraw or vice versa. This is not a requirement, but just good practice.

```solidity
// Do this!
bytes memory message abi.encodeWithSignature(
"withdraw(uint256,address,address)",
_amount,
_token,
_to
);

// Don't to this!
bytes memory message = abi.encode(
_amount,
_token,
_to
);
```

### Error Handling

Handling error when moving cross chain can quickly get tricky. Since the L1 and L2 calls are practically async and independent of each other, the L2 part of a withdraw might execute just fine, with the L1 part failing. If not handling this well, the funds might be lost forever! The contract builder should therefore consider in what ways his application can fail cross chain, and handle those cases explicitly.

First, entries in the outboxes SHOULD only be consumed if the execution is successful. For an L2 -> L1 call, the L1 execution can revert the transaction completely if anything fails. As the tx can be atomic, the failure also reverts the consumption of the entry.

If it is possible to enter a state where the second part of the execution fails forever, the application builder should consider including additional failure mechanisms (for token withdraws this could be depositing them again etc).

For L1 -> L2 calls, a badly priced L1 deposit, could lead to funds being locked in the bridge, but the message never leaving the L1 inbox because it is never profitable to include in a rollup. The inbox must support cancelling after some specific deadline is reached, but it is the job of the application builder to setup their contract such that the user can perform these cancellations.

Generally it is good practice to keep cross-chain calls simple to avoid too many edge cases and state reversions.

:::info
Error handling for cross chain messages is handled by the application contract and not the protocol. The protocol only delivers the messages, it does not ensure that they are executed successfully.
:::


### Designated caller
A designated caller is the ability to specify who should be able to call a function that consumes a message. This is useful for ordering of batched messages, as we will see in a second.

When doing multiple cross-chain calls as one action it is important to consider the order of the calls. Say for example, that you want to do a uniswap trade on L1 because you are a whale and slippage on L2 is too damn high.

You would practically, withdraw funds from the rollup, swap them on L1, and then deposit the swapped funds back into the rollup. This is a fairly simple process, but it requires that the calls are done in the correct order. For one, if the swap is called before the funds are withdrawn, the swap will fail. And if the deposit is called before the swap, the funds might get lost!

As the message boxes only will allow the recipient portal to consume the message, we can use this to our advantage to ensure that the calls are done in the correct order. Say that we include a designated "caller" in the messages, and that the portal contract checks that the caller matches the designated caller or designated is address(0) (anyone can call). When the message are to be consumed on L1, it can compute the message as seen below:

```solidity
bytes memory message = abi.encodeWithSignature(
"withdraw(uint256,address,address)",
_amount,
_to,
_withCaller ? msg.sender : address(0)
);
```

This way, the message can be consumed by the portal contract, but only if the caller is the designated caller. By being a bit clever when specifying the designated caller, we can ensure that the calls are done in the correct order. For the uniswap example, say that we have a portal contracts implementing the designated caller:

```solidity
contract TokenPortal {
function deposit(
uint256 _amount,
bytes32 _to,
bytes32 _caller,
) external returns (bytes32) {
bytes memory message = abi.encodeWithSignature(
"deposit(uint256,bytes32,bytes32)",
_amount,
_to,
_caller
);
ASSET.safeTransferFrom(msg.sender, address(this), _amount);
return INBOX.sendL2Message(message);
}

function withdraw(uint256 _amount, address _to, bool _withCaller) external {
// Including selector as message separator
bytes memory message = abi.encodeWithSignature(
"withdraw(uint256,address,address)",
_amount,
_to,
_withCaller ? msg.sender : address(0)
);
OUTBOX.consume(message);
ASSET.safeTransfer(_to, _amount);
}
}

contract UniswapPortal {
function swapAndDeposit(
address _inputTokenPortal,
uint256 _inAmount,
uint24 _fee,
address _outputTokenPortal,
bytes32 _aztecRecipient,
bool _withCaller
) public (bytes32) {
// Withdraw funds from the rollup, using designated caller
TokenPortal(_inputTokenPortal).withdraw(_inAmount, address(this), true);

// Consume the message to swap on uniswap
OUTBOX.consume(
abi.encodeWithSignature(
"swap(address,uint256,uint24,address,bytes32,address)",
_inputTokenPortal,
_inAmount,
_fee,
_outputTokenPortal,
_aztecRecipient,
_withCaller ? msg.sender : address(0)
)
);

// Perform swap on uniswap
uint256 amountOut = ...;

// Approve token to _outputTokenPortal

// Deposit token into rollup again
return TokenPortal(_outputTokenPortal).deposit(
amountOut, _aztecRecipient, bytes32(0)
);
}
}
```

We could then have withdraw transactions (on L2) where we are specifying the `UniswapPortal` as the caller. Because the order of the calls are specified in the contract, and that it reverts if any of them fail, we can be sure that it will execute the withdraw first, then the swap and then the deposit. Since only the `UniswapPortal` is able to execute the withdraw, we can be sure that the ordering is ensured. However, note that this means that if it for some reason is impossible to execute the batch (say prices moved greatly), the user will be stuck with the funds on L1 unless the `UniswapPortal` implements proper error handling!

:::caution
Designated callers are enforced at the contract level for contracts that are not the rollup itself, and should not be trusted to implement the standard correctly. The user should always be aware that it is possible for the developer to implement something that looks like designated caller without providing the abilities to the user.
:::


## Open Questions
- Can we handle L2 access control without public function calls?
- Essentially, can we have "private shared state" that is updated very sparingly but where we accept the race-conditions as they are desired in specific instances.
Expand Down
127 changes: 127 additions & 0 deletions docs/docs/dev_docs/contracts/portals/data_structures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: Data Structures
---

The `DataStructures` are structs that we are using throughout the message infrastructure and registry.

**Links**: [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/DataStructures.sol).

## `Entry`

An entry for the messageboxes multi-sets.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An entry for the messageboxes multi-sets.
An entry for the message box (the entry acts like a multi-set, enabling multiple entries for the same message)

Copy link
Contributor Author

@LHerskind LHerskind Aug 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entry does not act like a multiset, it is an entry in a multiset.


```solidity title="DataStructures.sol"
struct Entry {
uint64 fee;
uint32 count;
uint32 version;
uint32 deadline;
}
```

| Name | Type | Description |
| -------------- | ------- | ----------- |
| `fee` | `uint64` | The fee provided to sequencer for including in the inbox. 0 if Outbox (as not applicable). |
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
| `count` | `uint32` | The occurrence of the entry in the dataset |
| `version` | `uint32` | The version of the entry |
| `deadline` | `uint32` | The consumption deadline of the message. |


## `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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 for the entity. Used when specifying sender/recipient with an entity that is on L1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure what exactly we want here, often it is just denoted chainid in solidity docs etc, but I prefer chainId.


```solidity title="DataStructures.sol"
struct L1Actor {
address actor;
uint256 chainId;
}
```

| Name | Type | Description |
| -------------- | ------- | ----------- |
| `actor` | `address` | The ethereum address of the actor |
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
| `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 chainid for the entity. Used when specifying sender/recipient with an entity that is on L2.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An entity on L2, specifying the address and the chainid for the entity. Used when specifying sender/recipient with an entity that is on L2.
An entity on L2, specifying the address and the chainId for the entity. Used when specifying sender/recipient with an entity that is on L2.

Copy link
Contributor Author

@LHerskind LHerskind Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, chainid should be replaced by version here actually.


```solidity title="DataStructures.sol"
struct L2Actor {
bytes32 actor;
uint256 version;
}
```

| Name | Type | Description |
| -------------- | ------- | ----------- |
| `actor` | `bytes32` | The aztec address of the actor. |
| `version` | `uint256` | The version of the actor. Defines the rollup that the actor lives on. |
LHerskind marked this conversation as resolved.
Show resolved Hide resolved

## `L1ToL2Message`

A message that is sent from L1 to L2.

```solidity title="DataStructures.sol"
struct L1ToL2Msg {
L1Actor sender;
L2Actor recipient;
bytes32 content;
bytes32 secretHash;
uint32 deadline;
uint64 fee;
}
```

| 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. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `recipient` | `L2Actor` | The actor on L2 that is to receive the message. |
| `recipient` | `L2Actor` | The actor on L2 that should 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. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you explained in your other PR, but should we explain how to do a sha256 hash to a field ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secretHash is not a sha256 to field. But will add the note on how to compute it.

Use the [`computeMessageSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret.

| `deadline` | `uint32` | The message consumption-deadline time in seconds. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `deadline` | `uint32` | The message consumption-deadline time in seconds. |
| `deadline` | `uint32` | The deadline for consuming a message in seconds. |

| `fee` | `uint64` | The fee that the sequencer will be paid for inclusion of the message. |
LHerskind marked this conversation as resolved.
Show resolved Hide resolved

## `L2ToL1Message`

A message that is sent from L2 to L1.

```solidity title="DataStructures.sol"
struct L2ToL1Msg {
DataStructures.L2Actor sender;
DataStructures.L1Actor recipient;
bytes32 content;
}
```

| 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. |

## `RegistrySnapshot`

A snapshot of the registry values.

```solidity title="DataStructures.sol"
struct RegistrySnapshot {
address rollup;
address inbox;
address outbox;
uint256 blockNumber;
}
```

| Name | Type | Description |
| -------------- | ------- | ----------- |
| `rollup` | `address` | The address of the rollup contract for the snapshot. |
| `inbox` | `address` | The address of the inbox contract for the snapshot. |
| `outbox` | `address` | The address of the outbox contract for the snapshot. |
| `blockNumber` | `uint256` | The blocknumber at which the snapshot was created. |
LHerskind marked this conversation as resolved.
Show resolved Hide resolved




Loading