From ea8dca0c76c5b114b2b2c382e0b517a852198299 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Tue, 8 Aug 2023 18:02:30 +0000 Subject: [PATCH 1/8] feat: initial portal docs + minor cleanups --- .../communication/cross_chain_calls.md | 134 ------- docs/docs/dev_docs/contracts/portals/inbox.md | 138 ++++++++ docs/docs/dev_docs/contracts/portals/main.md | 331 ++++++++++++++++++ .../docs/dev_docs/contracts/portals/outbox.md | 90 +++++ .../dev_docs/contracts/portals/registry.md | 116 ++++++ docs/sidebars.js | 13 + .../core/interfaces/messagebridge/IOutbox.sol | 4 +- .../interfaces/messagebridge/IRegistry.sol | 2 + .../src/core/messagebridge/Registry.sol | 3 +- .../noir-libs/noir-aztec/src/context.nr | 4 +- 10 files changed, 696 insertions(+), 139 deletions(-) create mode 100644 docs/docs/dev_docs/contracts/portals/inbox.md create mode 100644 docs/docs/dev_docs/contracts/portals/main.md create mode 100644 docs/docs/dev_docs/contracts/portals/outbox.md create mode 100644 docs/docs/dev_docs/contracts/portals/registry.md diff --git a/docs/docs/concepts/foundation/communication/cross_chain_calls.md b/docs/docs/concepts/foundation/communication/cross_chain_calls.md index 27d4f945744..12b28f209c4 100644 --- a/docs/docs/concepts/foundation/communication/cross_chain_calls.md +++ b/docs/docs/concepts/foundation/communication/cross_chain_calls.md @@ -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. diff --git a/docs/docs/dev_docs/contracts/portals/inbox.md b/docs/docs/dev_docs/contracts/portals/inbox.md new file mode 100644 index 00000000000..32ea16213d3 --- /dev/null +++ b/docs/docs/dev_docs/contracts/portals/inbox.md @@ -0,0 +1,138 @@ +--- +title: Inbox +--- + +The `Inbox` is a contract deployed on L1 that handle message passing from L1 into the rollup (L2) + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Inbox.sol). + +## `sendL2Message()` + +Sends a message from L1 to L2. + +```solidity +function sendL2Message( + DataStructures.L2Actor memory _recipient, + uint32 _deadline, + bytes32 _content, + bytes32 _secretHash +) external payable returns (bytes32); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| Recipient | `L2Actor` | Who is to receive 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. | +| Deadline | `uint256` | The deadline for the message to be consumed. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | +| 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`](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 to keep the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so keep track of the pre-images! 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. | +| Fee (msg.value) | `uint256` | The fee to the sequencer for including the message. This is the amount of ETH that the sequencer will receive for including the message. Note that only values that can fit in `uint64` will be accepted | +| ReturnValue | `bytes32` | The message hash, used as an identifier | + +#### Edge cases + +- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient is too large to be included in a field (~254 bits). +- Will revert with `Inbox__DeadlineBeforeNow()` if the deadline is before the current block. +- Will revert with `Inbox__ContentTooLarge(bytes32 content)` if the content is too large to be included in a field (~254 bits). +- Will revert with `Inbox__SecretHashTooLarge(bytes32 secretHash)` if the secret hash is too large to be included in a field (~254 bits). +- Will revert with `Inbox__FeeTooHigh()` if the fee is larger than `type(uint64).max`. +- Will revert `Inbox__IncompatibleEntryArguments(bytes32 entryKey, uint64 storedFee, uint64 feePassed, uint32 storedVersion, uint32 versionPassed, uint32 storedDeadline, uint32 deadlinePassed)` if insertion is not possible due to invalid entry arguments. + +## `cancelL2Message()` +Cancels a message that have not yet been consumed. + +```solidity +function cancelL2Message( + DataStructures.L1ToL2Msg memory _message, + address _feeCollector +) external returns (bytes32 entryKey); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_message` | `L1ToL2Msg` | The message to cancel | +| `_feeCollector`| `address` | The address to refund the fee to | +| ReturnValue | `bytes32` | The hash of the message | + +#### Edge cases + +- Will revert with `Inbox__Unauthorized()` if `msg.sender != _message.sender.actor`. +- Will revert with `Inbox__NotPastDeadline()` if `block.timestamp <= _message.deadline`. +- Will revert with `Inbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. + +## `batchConsume()` + +Allows the `Rollup` to consume multiple messages from in a single transaction. + +```solidity +function batchConsume(bytes32[] memory _entryKeys, address _feeCollector) external; +``` +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKeys` | `bytes32[]` | The entry keys (message hashs) to consume | +| ReturnValue | `Entry` | The entry for the given key | + +#### Edge cases + +- Will revert with `Registry__RollupNotRegistered(address rollup)` if `msg.sender` is not registered as a rollup on the [`Registry`](./registry.md). +- Will revert with `Inbox__InvalidVersion(uint256 entry, uint256 rollup)` if the rollup version does not match the version of the message. +- Will revert with `Inbox__PastDeadline()` if the message deadline has passed. +- Will revert with `Inbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. + +## `withdrawFees()` + +Will claim the fees that have accrued for the `msg.sender` from consuming messages. + +Let the sequencer withdraw fees from the inbox. + +```solidity +function withdrawFees() external; +``` + +#### Edge cases + +- Will revert with `Inbox__FailedToWithdrawFees()` if the transfer call fails. + +## `get()` +Retries the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. + +```solidity +function get(bytes32 _entryKey) + external view returns (DataStructures.Entry memory); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKey` | `bytes32` | The entry key (message hash) | +| ReturnValue | `Entry` | The entry for the given key | + +#### Edge cases +- Will revert with `Inbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. + + +## `contains()` +Returns whether the key is found in the inbox. + +```solidity +function contains( + bytes32 _entryKey +) external view returns (bool); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKey` | `bytes32` | The entry key (message hash)| +| ReturnValue | `bool` | True if contained, false otherwise| + +## `computeEntryKey()` +Computes the hash of a message. + +```solidity +function computeEntryKey( + DataStructures.L1ToL2Msg memory _message +) external pure returns (bytes32 entryKey); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_message` | `L1ToL2Msg` | The message to compute hash for | +| ReturnValue | `bytes32` | The hash of the message | \ No newline at end of file diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md new file mode 100644 index 00000000000..b433e00526c --- /dev/null +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -0,0 +1,331 @@ +--- +title: Portals +--- + +## What is a portal + +A portal is the point of contact between L1 and a specific contract on Aztec. For applications such as token bridges, this is the point where the tokens are are held on L1 while used in L2. + +As outlined in the [foundational concepts](../../../concepts/foundation/communication/cross_chain_calls.md), an Aztec L2 contract is linked to *ONE* L1 contract at time of deployment (specified by the developer). This L1 contract is the only that can send messages to that specific L2 contract, and the only party that can receive messages sent from the L2 contract to L1. Note, that a portal don't actually need to be a contract, it could be any address on L1. We say that an Aztec contract is attached to a portal. + +## Passing data to the rollup + +Whether it is tokens or other information that is to be passed to the rollup, the portal should use the [`Inbox`](./inbox.md) to do it. + +The `Inbox` can be seen as a mailbox to the rollup, portals put messages into the box, and the sequencers then decide which of these message they want to include in their blocks (each message has a fee attached to it, so there is a fee market here). + +When sending messages, we need to specify quite a bit of information beyond just the content that we are sharing. Namely we need to specify: + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| Recipient | `L2Actor` | Who is to receive 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. | +| Deadline | `uint256` | The deadline for the message to be consumed. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | +| 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`](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 to keep the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so keep track of the pre-images! 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. | +| Fee | `uint64` | The fee to the sequencer for including the message. This is the amount of ETH that the sequencer will receive for including the message. Note that it is not a full `uint256` but only `uint64`| + +:::warning +Should we discard the use of the L2Actor struct and use the individual fields instead? Might make integrations a tiny bit more pleasent to work with. +::: + +With all that information at hand, we can call the `sendL2Message` function on the Inbox. The function will return a `field` (inside `bytes32`) that is the hash of the message, which is the hash of the message which can be used as an identifier to spot when your message is included in a rollup block. + +```solidity title="IInbox.sol" + function sendL2Message( + DataStructures.L2Actor memory _recipient, + uint32 _deadline, + bytes32 _content, + bytes32 _secretHash + ) external payable returns (bytes32); +``` + +Time passes by, a sequencer will see your tx and the juicy fee that you provide and include it in a rollup block. At this point in time, it is removed from L1, and now available to be consumed on L2. + +To consume the message, we can use the `consume_l1_to_l2_message` function on the `context`. +The `msg_key` is the hash of the message that we got from the `sendL2Message` call, the `content` is the content of the message, and the `secret` is the secret that is hashed to compute the `secretHash`. + +```rust title="context.nr" + fn consume_l1_to_l2_message( + &mut self, + inputs: abi::PrivateContextInputs, + msg_key: Field, + content: Field, + secret: Field + ) +``` + +Computing the `content` might be a little unhandy in the contracts, as we don't have a lot of byte utilities yet. A good example would be to look at the [Non-native token example](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/hash.nr). + +:::info +The `inputs` that is passed into `consume_l1_to_l2_message` are only temporary, and will be removed in the future when we improve syntax. +::: + +An example of where this could be used would be a token bridge, where the Aztec contract has a `mint` function that consumes a message from L1 and mints tokens to the user. Note that we are using a private function, as we don't want to expose the user that is receiving the tokens to the public. + +```rust title="NonNativeTokenContract.nr" +// mints token upon consuming valid messages from portal +fn mint( + ... + amount: Field, + owner: Field, + msg_key: Field, + secret: Field, +) -> distinct pub abi::PrivateCircuitPublicInputs { + ... + let content_hash = get_mint_content_hash(amount, owner, canceller); + + // Consume the message from L1 + context.consume_l1_to_l2_message(inputs, msg_key, content_hash, secret); + + // Mint the tokens to the owner + let balance = storage.balances.at(owner); + send_note(&mut context, balance, amount, owner); + ... +} +``` + +After the transaction is mined the message is consumed (and cannot be consumed again), the tokens are minted and the user can now use them on L2. + +## Passing data data to L1 + +To pass data to L1, we use the `Outbox`. The `Outbox` is the mailbox for L2 to L1 messages, and is the location on L1 where all the messages that have been sent from L2 will live, and can be consumed from. + +Similarly to messages going into the L2 from L1, a message can only be consumed by the recipient, however note that it is up to the portal contract to ensure that the sender is as expected! + +Recall that we mentioned that the Aztec contract is specifying what portal it is attached to at deployment. This value is stored in the contract tree which lives in the rollup so these links are not directly readable on L1. Also, seen it is possible to attach multiple aztec contracts to the same portal, the portal must ensure that the sender is as planned. + +One way to do this, is to compute the addresses before deployment and have them as constants in the contract, however an easier solution is probably to have a `initialize` function in the portal contract which can be used to set the address of the aztec contract. This way, the portal contract can check that the sender matches the value it has in storage. + +To send a message to L1 from your Aztec contract, you must use the `message_portal` function on the `context`. It require just the `content` (again as field). + +```rust title="context.nr" + fn message_portal(&mut self, content: Field) +``` + +When sending a message from L2 to L1 we don't need to pass recipient, deadline, secret nor fees. Recipient is populated with the attached portal and the remaining values are not needed as the message is inserted into the outbox at the same time as it was included in a block (for the inbox it could be inserted and then only included in rollup block later). + +:::danger +Need to have access control on the L1 portal contract to not consume messages that were sent from the wrong L2 contract. +::: + +As earlier, we can use a token bridge as an example. In this case, we are burning tokens on L2 and sending a message to the portal to free them on L1. + +```rust title="NonNativeTokenContract.nr" +// burns token and sends a message to the portal +fn withdraw( + amount: Field, + sender: Field, + recipient: Field, + callerOnL1: Field, +) -> distinct pub abi::PrivateCircuitPublicInputs { + ... + let sender_balance = storage.balances.at(sender); + spend_notes(&mut context, sender_balance, amount, sender); + + let content = get_withdraw_content_hash(amount, recipient, callerOnL1); + context.message_portal(content); + ... +} +``` + +When the transaction is included in a rollup block it will be inserted into the `Outbox`, where the recipient portal can consume it from. When consuming, the `msg.sender` must match the `recipient` meaning that only portal can actually consume the message. + +```solidity title="IOutbox.sol" +struct L2ToL1Msg { + DataStructures.L2Actor sender; + DataStructures.L1Actor recipient; + bytes32 content; +} + +function consume(DataStructures.L2ToL1Msg memory _message) + external + returns (bytes32 entryKey); +``` + +As noted earlier, the portal contract should check that the sender is as expected. In the example below, we support only one sender contract (stored in `l2TokenAddress`) so we can just pass it as the sender, that way we will only be able to consume messages from that contract. If multiple senders are supported, you could use a have `mapping(address => bool) allowed` and check that `allowed[msg.sender]` is `true`. + +```solidity title="TokenPortal.sol" +function withdraw(uint256 _amount, address _recipient, bool _withCaller) + external + returns (bytes32) + { + // Create the message structure + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2TokenAddress, version), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: Hash.sha256ToField( + abi.encodeWithSignature( + "withdraw(uint256,address,address)", + _amount, + _recipient, + _withCaller ? msg.sender : address(0) + ) + ) + }); + + // Consume the message + bytes32 entryKey = registry.getOutbox().consume(message); + + // Transfer the tokens to the user + underlying.transfer(_recipient, _amount); + + return entryKey; + } +``` + + +## How to deploy a contract with a portal +- Deploy L1 +- Deploy l2 +- Initialize l1 with l2 address. + +## 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 to ensure that they are not making messages that could be misinterpreted by the recipient. + +An example of a bad format would 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. + +```solidity +// Don't to this! +bytes memory message = abi.encode( + _amount, + _token, + _to +); + +// Do this! +bytes memory message abi.encodeWithSignature( + "withdraw(uint256,address,address)", + _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 L1 part of a deposit might execute just fine, with the L2 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). + +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. +::: + +### Cancellations + +A special type of errors is underpriced transactions, here meaning that message is inserted on L1, but the fee available to the sequencer is too low to be included in a rollup block. + +For the case of token bridges, this could lead to funds being locked in the bridge forever, as the funds are locked but the message never arrives on L2 to mint the tokens. To address this, the `Inbox` supports cancelling messages after a deadline. However, this must be called by the portal itself, as it will need to "undo" the state changes is made (for example by sending the tokens back to the user). + +As this requires logic on the portal itself, it is not something that the protocol can enforce. It must be supported by the application builder when building the portal. + +The portal can call the `cancelL2Message` at the `Inbox` when `block.timestamp > deadline` for the message. + +```solidity title="IInbox.sol" +function cancelL2Message( + DataStructures.L1ToL2Msg memory _message, + address _feeCollector +) external returns (bytes32 entryKey); +``` + +Building on our token example from earlier, this can be called like: + +```solidity title="TokenPortal.sol" +function cancelL1ToAztecMessage( + bytes32 _to, + uint256 _amount, + uint32 _deadline, + bytes32 _secretHash, + uint64 _fee + ) external returns (bytes32) { + IInbox inbox = registry.getInbox(); + DataStructures.L1Actor memory l1Actor = DataStructures.L1Actor(address(this), block.chainid); + DataStructures.L2Actor memory l2Actor = DataStructures.L2Actor(l2TokenAddress, 1); + DataStructures.L1ToL2Msg memory message = DataStructures.L1ToL2Msg({ + sender: l1Actor, + recipient: l2Actor, + content: Hash.sha256ToField( + abi.encodeWithSignature("mint(uint256,bytes32,address)", _amount, _to, msg.sender) + ), + secretHash: _secretHash, + deadline: _deadline, + fee: _fee + }); + bytes32 entryKey = inbox.cancelL2Message(message, address(this)); + // Ensures that `msg.sender == canceller` by using `msg.sender` in the hash computation. + underlying.transfer(msg.sender, _amount); + return entryKey; + } +``` + +The example above ensure that the user can cancel his message if it is underpriced. + +### 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 token portals implemented as we have done throughout this page, and n Uniswap portal implementing the designated caller: + +```solidity title="UniswapPortal.sol" + function swap( + address _inputTokenPortal, + uint256 _inAmount, + uint24 _uniswapFeeTier, + address _outputTokenPortal, + uint256 _amountOutMinimum, + bytes32 _aztecRecipient, + bytes32 _secretHash, + uint32 _deadlineForL1ToL2Message, + address _canceller, + bool _withCaller + ) public payable returns (bytes32) { + // Withdraw the input asset from the portal with designated caller + TokenPortal(_inputTokenPortal).withdraw(_inAmount, address(this), true); + + // Consume message to Uniswap portal (self) + registry.getOutbox().consume(...); + + // swap... + uint256 amountOut = ROUTER.exactInputSingle(swapParams); + + // Deposit the output asset to the L2 via its portal + return TokenPortal(_outputTokenPortal).depositToAztec{value: msg.value}( + _aztecRecipient, amountOut, _deadlineForL1ToL2Message, _secretHash, _canceller + ); + } +``` + +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. +::: + +## Examples of portals + +- Non-native token (asset bridged from L1) + - [Portal contract](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/test/portals/TokenPortal.sol) + - [Aztec contract](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/main.nr) +- Uniswap (swap tokens on L1) + - [Portal contract](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/test/portals/UniswapPortal.sol) + - [Aztec contract](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr) \ No newline at end of file diff --git a/docs/docs/dev_docs/contracts/portals/outbox.md b/docs/docs/dev_docs/contracts/portals/outbox.md new file mode 100644 index 00000000000..46d072723c7 --- /dev/null +++ b/docs/docs/dev_docs/contracts/portals/outbox.md @@ -0,0 +1,90 @@ +--- +title: Outbox +--- + +The `Outbox` is a contract deployed on L1 that handle message passing from the rollup and back to L1. + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Outbox.sol). + +## `sendL1Messages()` + +Inserts multiple messages from the `Rollup`. + +```solidity +function sendL1Messages(bytes32[] memory _entryKey) external; +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKeys` | `bytes32[]` | A list of message hashes to insert into the outbox for later consumption | + +#### Edge cases + +- Will revert with `Registry__RollupNotRegistered(address rollup)` if `msg.sender` is not registered as a rollup on the [`Registry`](./registry.md) +- Will revert `Outbox__IncompatibleEntryArguments(bytes32 entryKey, uint64 storedFee, uint64 feePassed, uint32 storedVersion, uint32 versionPassed, uint32 storedDeadline, uint32 deadlinePassed)` if insertion is not possible due to invalid entry arguments. + +## `consume()` + +Allows a recipient to consume a message from the `Outbox`. + +```solidity +function consume( + DataStructures.L2ToL1Msg memory _message +) external returns (bytes32 entryKey); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_message` | `L2ToL1Msg` | The message to consume | +| ReturnValue | `bytes32` | The hash of the message | + +#### Edge cases + +- Will revert with `Outbox__Unauthorized()` if `msg.sender != _message.recipient.actor`. +- Will revert with `Outbox__InvalidChainId()` if `block.chainid != _message.recipient.chainId`. +- Will revert with `Outbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. +- Will revert with `Outbox__InvalidVersion(uint256 entry, uint256 message)` if the version of the entry and message sender don't match (wrong rollup). + +## `get()` +Retries the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. + +```solidity +function get(bytes32 _entryKey) + external view returns (DataStructures.Entry memory); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKey` | `bytes32` | The entry key (message hash) | +| ReturnValue | `Entry` | The entry for the given key | + +#### Edge cases +- Will revert with `Outbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. + +## `contains()` +Returns whether the key is found in the inbox. + +```solidity +function contains( + bytes32 _entryKey +) external view returns (bool); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_entryKey` | `bytes32` | The entry key (message hash)| +| ReturnValue | `bool` | True if contained, false otherwise| + +## `computeEntryKey()` +Computes the hash of a message. + +```solidity +function computeEntryKey( + DataStructures.L2ToL1Msg memory _message +) external pure returns (bytes32 entryKey); +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `_message` | `L2ToL1Msg` | The message to compute hash for | +| ReturnValue | `bytes32` | The hash of the message | \ No newline at end of file diff --git a/docs/docs/dev_docs/contracts/portals/registry.md b/docs/docs/dev_docs/contracts/portals/registry.md new file mode 100644 index 00000000000..ddcf34c3959 --- /dev/null +++ b/docs/docs/dev_docs/contracts/portals/registry.md @@ -0,0 +1,116 @@ +--- +title: Registry +--- + +The registry is a contract deployed on L1, that contains addresses for the `Rollup`, `Inbox` and `Outbox`. It also keeps track of the different versions that have been deployed and let you query prior deployments easily. + +**Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Registry.sol). + +## `numberOfVersions()` + +Retrieves the number of versions that have been deployed. + +```solidity +function numberOfVersions() external view returns (uint256); +``` + +| Name | Description | +| -------------- | ----------- | +| ReturnValue | The number of versions that have been deployed | + +## `getRollup()` +Retrieves the current rollup contract. + +```solidity +function getRollup() external view returns (IRollup); +``` + +| Name | Description | +| -------------- | ----------- | +| ReturnValue | The current rollup | + +## `getInbox()` + +Retrieves the current inbox contract. + +```solidity +function getInbox() external view returns (IInbox); +``` + +| Name | Description | +| -------------- | ----------- | +| ReturnValue | The current Inbox | + +## `getOutbox()` + +Retrieves the current inbox contract. + +```solidity +function getOutbox() external view returns (IOutbox); +``` + +| Name | Description | +| -------------- | ----------- | +| ReturnValue | The current Outbox | + +## `getVersionFor(address _rollup)` + +Retrieve the version of a specific rollup contract. +```solidity +function getVersionFor(address _rollup) external view returns (uint256); +``` + +| Name | Description | +| -------------- | ----------- | +| `_rollup` | The address of the rollup to lookup | +| ReturnValue | The version number of `_rollup` | + +#### Edge cases +Will revert with `Registry__RollupNotRegistered(_rollup)` if the rollup have not been registered. + +## `getSnapshot(uint256 _version)` + +Retrieve the snapshot of a specific version. + +```solidity +// Snippet from DataStructures.sol +struct RegistrySnapshot { + address rollup; + address inbox; + address outbox; + uint256 blockNumber; + } + +function getSnapshot(uint256 _version) + external + view + returns (DataStructures.RegistrySnapshot memory); +``` + +| Name | Description | +| -------------- | ----------- | +| `_version` | The version number to fetch data for | +| ReturnValue.rollup | The address of the `rollup` for the `_version` | +| ReturnValue.inbox | The address of the `inbox` for the `_version` | +| ReturnValue.outbox | The address of the `outbox` for the `_version` | +| ReturnValue.blockNumber | The block number of the snapshot creation | + + +## `getCurrentSnapshot()` + +Retrieves the snapshot for the current version. + +```solidity +function getCurrentSnapshot() + external + view + returns (DataStructures.RegistrySnapshot memory); +``` + +| Name | Description | +| -------------- | ----------- | +| ReturnValue.rollup | The address of the `rollup` for the current `_version` | +| ReturnValue.inbox | The address of the `inbox` for the current `_version` | +| ReturnValue.outbox | The address of the `outbox` for the current `_version` | +| ReturnValue.blockNumber | The block number of the snapshot creation | + diff --git a/docs/sidebars.js b/docs/sidebars.js index 0f5eefd7a19..d7816972c37 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -217,6 +217,19 @@ const sidebars = { "dev_docs/contracts/constrain", "dev_docs/contracts/compiling", "dev_docs/contracts/deploying", + { + label: "Portals", + type: "category", + link: { + type: "doc", + id: "dev_docs/contracts/portals/main", + }, + items: [ + "dev_docs/contracts/portals/registry", + "dev_docs/contracts/portals/inbox", + "dev_docs/contracts/portals/outbox", + ], + }, { label: "Resources", type: "category", diff --git a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol index 37e3bf3e94f..3c822b2823e 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol @@ -26,9 +26,9 @@ interface IOutbox { /** * @notice Inserts an array of entries into the Outbox * @dev Only callable by the rollup contract - * @param _entryKey - Array of entry keys (hash of the message) - computed by the L2 counterpart and sent to L1 via rollup block + * @param _entryKeys - Array of entry keys (hash of the message) - computed by the L2 counterpart and sent to L1 via rollup block */ - function sendL1Messages(bytes32[] memory _entryKey) external; + function sendL1Messages(bytes32[] memory _entryKeys) external; /** * @notice Consumes an entry from the Outbox diff --git a/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol b/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol index fb98708e0c6..a0152d5dfca 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol @@ -7,6 +7,8 @@ import {IInbox} from "./IInbox.sol"; import {IOutbox} from "./IOutbox.sol"; interface IRegistry { + function numberOfVersions() external view returns (uint256); + function upgrade(address _rollup, address _inbox, address _outbox) external returns (uint256); function getRollup() external view returns (IRollup); diff --git a/l1-contracts/src/core/messagebridge/Registry.sol b/l1-contracts/src/core/messagebridge/Registry.sol index c87551cf5ca..21dff5e821d 100644 --- a/l1-contracts/src/core/messagebridge/Registry.sol +++ b/l1-contracts/src/core/messagebridge/Registry.sol @@ -20,7 +20,8 @@ import {Errors} from "@aztec/core/libraries/Errors.sol"; * for L1<->L2 communication. */ contract Registry is IRegistry { - uint256 public numberOfVersions; + uint256 public override(IRegistry) numberOfVersions; + DataStructures.RegistrySnapshot internal currentSnapshot; mapping(uint256 version => DataStructures.RegistrySnapshot snapshot) internal snapshots; mapping(address rollup => uint256 version) internal rollupToVersion; diff --git a/yarn-project/noir-libs/noir-aztec/src/context.nr b/yarn-project/noir-libs/noir-aztec/src/context.nr index 0b1c41ad28b..eb022ec7801 100644 --- a/yarn-project/noir-libs/noir-aztec/src/context.nr +++ b/yarn-project/noir-libs/noir-aztec/src/context.nr @@ -139,8 +139,8 @@ impl Context { self.nullified_commitments.push(nullified_commitment); } - fn message_portal(&mut self, msg: Field) { - self.new_l2_to_l1_msgs.push(msg); + fn message_portal(&mut self, content: Field) { + self.new_l2_to_l1_msgs.push(content); } // PrivateContextInputs must be temporarily passed in to prevent too many unknowns From 8f94a1a8f0db54e12f381e8a56b5fa4391d94a05 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Tue, 8 Aug 2023 18:10:55 +0000 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20lint=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol b/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol index a0152d5dfca..580d92a309f 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IRegistry.sol @@ -7,8 +7,6 @@ import {IInbox} from "./IInbox.sol"; import {IOutbox} from "./IOutbox.sol"; interface IRegistry { - function numberOfVersions() external view returns (uint256); - function upgrade(address _rollup, address _inbox, address _outbox) external returns (uint256); function getRollup() external view returns (IRollup); @@ -25,4 +23,6 @@ interface IRegistry { returns (DataStructures.RegistrySnapshot memory); function getCurrentSnapshot() external view returns (DataStructures.RegistrySnapshot memory); + + function numberOfVersions() external view returns (uint256); } From 5418d986ad1d0e521c098fc1fd09a988d5f27b6e Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:45:07 +0200 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Maddiaa <47148561+Maddiaa0@users.noreply.github.com> --- docs/docs/dev_docs/contracts/portals/inbox.md | 18 +++---- docs/docs/dev_docs/contracts/portals/main.md | 52 +++++++++---------- .../docs/dev_docs/contracts/portals/outbox.md | 4 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/docs/dev_docs/contracts/portals/inbox.md b/docs/docs/dev_docs/contracts/portals/inbox.md index 32ea16213d3..07e18335949 100644 --- a/docs/docs/dev_docs/contracts/portals/inbox.md +++ b/docs/docs/dev_docs/contracts/portals/inbox.md @@ -2,7 +2,7 @@ title: Inbox --- -The `Inbox` is a contract deployed on L1 that handle message passing from L1 into the rollup (L2) +The `Inbox` is a contract deployed on L1 that handles message passing from L1 to the rollup (L2) **Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Inbox.sol). @@ -21,10 +21,10 @@ function sendL2Message( | Name | Type | Description | | -------------- | ------- | ----------- | -| Recipient | `L2Actor` | Who is to receive 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. | -| Deadline | `uint256` | The deadline for the message to be consumed. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | +| 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. | +| Deadline | `uint256` | The message consumption deadline. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | | 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`](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 to keep the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so keep track of the pre-images! 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. | +| 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 the [`computeMessageSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | | Fee (msg.value) | `uint256` | The fee to the sequencer for including the message. This is the amount of ETH that the sequencer will receive for including the message. Note that only values that can fit in `uint64` will be accepted | | ReturnValue | `bytes32` | The message hash, used as an identifier | @@ -61,7 +61,7 @@ function cancelL2Message( ## `batchConsume()` -Allows the `Rollup` to consume multiple messages from in a single transaction. +Allows the `Rollup` to consume multiple messages in a single transaction. ```solidity function batchConsume(bytes32[] memory _entryKeys, address _feeCollector) external; @@ -74,7 +74,7 @@ function batchConsume(bytes32[] memory _entryKeys, address _feeCollector) extern #### Edge cases - Will revert with `Registry__RollupNotRegistered(address rollup)` if `msg.sender` is not registered as a rollup on the [`Registry`](./registry.md). -- Will revert with `Inbox__InvalidVersion(uint256 entry, uint256 rollup)` if the rollup version does not match the version of the message. +- Will revert with `Inbox__InvalidVersion(uint256 entry, uint256 rollup)` if the rollup version does not match the version specified in the message. - Will revert with `Inbox__PastDeadline()` if the message deadline has passed. - Will revert with `Inbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. @@ -93,7 +93,7 @@ function withdrawFees() external; - Will revert with `Inbox__FailedToWithdrawFees()` if the transfer call fails. ## `get()` -Retries the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. +Retrieves the `entry` for a given message. The entry contains fee, number of occurrences, deadline and version information. ```solidity function get(bytes32 _entryKey) @@ -103,14 +103,14 @@ function get(bytes32 _entryKey) | Name | Type | Description | | -------------- | ------- | ----------- | | `_entryKey` | `bytes32` | The entry key (message hash) | -| ReturnValue | `Entry` | The entry for the given key | +| ReturnValue | `Entry` | The entry object for the given key | #### Edge cases - Will revert with `Inbox__NothingToConsume(bytes32 entryKey)` if the message does not exist. ## `contains()` -Returns whether the key is found in the inbox. +Returns whether the key exists in the inbox. ```solidity function contains( diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md index b433e00526c..eb019d19d73 100644 --- a/docs/docs/dev_docs/contracts/portals/main.md +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -10,7 +10,7 @@ As outlined in the [foundational concepts](../../../concepts/foundation/communic ## Passing data to the rollup -Whether it is tokens or other information that is to be passed to the rollup, the portal should use the [`Inbox`](./inbox.md) to do it. +Whether it is tokens or other information being passed to the rollup, the portal should use the [`Inbox`](./inbox.md) to do it. The `Inbox` can be seen as a mailbox to the rollup, portals put messages into the box, and the sequencers then decide which of these message they want to include in their blocks (each message has a fee attached to it, so there is a fee market here). @@ -18,9 +18,9 @@ When sending messages, we need to specify quite a bit of information beyond just | Name | Type | Description | | -------------- | ------- | ----------- | -| Recipient | `L2Actor` | Who is to receive 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. | -| Deadline | `uint256` | The deadline for the message to be consumed. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | -| 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`](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/libraries/Hash.sol) utilities with `sha256ToField` functions) | +| Recipient | `L2Actor` | The message recipient. 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. | +| Deadline | `uint256` | The deadline for the message to be consumed. If the message has not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | +| 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. 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`](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 to keep the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so keep track of the pre-images! 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. | | Fee | `uint64` | The fee to the sequencer for including the message. This is the amount of ETH that the sequencer will receive for including the message. Note that it is not a full `uint256` but only `uint64`| @@ -28,7 +28,7 @@ When sending messages, we need to specify quite a bit of information beyond just Should we discard the use of the L2Actor struct and use the individual fields instead? Might make integrations a tiny bit more pleasent to work with. ::: -With all that information at hand, we can call the `sendL2Message` function on the Inbox. The function will return a `field` (inside `bytes32`) that is the hash of the message, which is the hash of the message which can be used as an identifier to spot when your message is included in a rollup block. +With all that information at hand, we can call the `sendL2Message` function on the Inbox. The function will return a `field` (inside `bytes32`) that is the hash of the message. This hash can be used as an identifier to spot when your message has been included in a rollup block. ```solidity title="IInbox.sol" function sendL2Message( @@ -39,10 +39,10 @@ With all that information at hand, we can call the `sendL2Message` function on t ) external payable returns (bytes32); ``` -Time passes by, a sequencer will see your tx and the juicy fee that you provide and include it in a rollup block. At this point in time, it is removed from L1, and now available to be consumed on L2. +As time passes, a sequencer will see your tx, the juicy fee provided and include it in a rollup block. Upon inclusion, it is removed from L1, and made available to be consumed on L2. To consume the message, we can use the `consume_l1_to_l2_message` function on the `context`. -The `msg_key` is the hash of the message that we got from the `sendL2Message` call, the `content` is the content of the message, and the `secret` is the secret that is hashed to compute the `secretHash`. +The `msg_key` is the hash of the message produced from the `sendL2Message` call, the `content` is the content of the message, and the `secret` is the pre-image hashed to compute the `secretHash`. ```rust title="context.nr" fn consume_l1_to_l2_message( @@ -54,13 +54,13 @@ The `msg_key` is the hash of the message that we got from the `sendL2Message` ca ) ``` -Computing the `content` might be a little unhandy in the contracts, as we don't have a lot of byte utilities yet. A good example would be to look at the [Non-native token example](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/hash.nr). +Computing the `content` might be a little clunky in its current form, as we are still adding a number of bytes utilities. A good example exists within the [Non-native token example](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/non_native_token_contract/src/hash.nr). :::info The `inputs` that is passed into `consume_l1_to_l2_message` are only temporary, and will be removed in the future when we improve syntax. ::: -An example of where this could be used would be a token bridge, where the Aztec contract has a `mint` function that consumes a message from L1 and mints tokens to the user. Note that we are using a private function, as we don't want to expose the user that is receiving the tokens to the public. +An example usage of this flow is a token bridge, in the Aztec contract a `mint` function consumes a message from L1 and mints tokens to a user. Note that we are using a private function, as we don't want to expose the user that is receiving the tokens publicly. ```rust title="NonNativeTokenContract.nr" // mints token upon consuming valid messages from portal @@ -84,19 +84,19 @@ fn mint( } ``` -After the transaction is mined the message is consumed (and cannot be consumed again), the tokens are minted and the user can now use them on L2. +After the transaction has been mined the message is consumed (and cannot be consumed again), the tokens have been minted and the user can use them on L2. ## Passing data data to L1 -To pass data to L1, we use the `Outbox`. The `Outbox` is the mailbox for L2 to L1 messages, and is the location on L1 where all the messages that have been sent from L2 will live, and can be consumed from. +To pass data to L1, we use the `Outbox`. The `Outbox` is the mailbox for L2 to L1 messages. This is the location on L1 where all the messages from L2 will live, and where they can be consumed from. -Similarly to messages going into the L2 from L1, a message can only be consumed by the recipient, however note that it is up to the portal contract to ensure that the sender is as expected! +Similarly to messages going to L2 from L1, a message can only be consumed by the recipient, however note that it is up to the portal contract to ensure that the sender is as expected! -Recall that we mentioned that the Aztec contract is specifying what portal it is attached to at deployment. This value is stored in the contract tree which lives in the rollup so these links are not directly readable on L1. Also, seen it is possible to attach multiple aztec contracts to the same portal, the portal must ensure that the sender is as planned. +Recall, we mentioned the Aztec contract specifies what portal it is attached to at deployment. This value is stored in the rollup's contract tree, hence these links are not directly readable on L1. Also, it is possible to attach multiple aztec contracts to the same portal, the portal must ensure that the sender is as expected. -One way to do this, is to compute the addresses before deployment and have them as constants in the contract, however an easier solution is probably to have a `initialize` function in the portal contract which can be used to set the address of the aztec contract. This way, the portal contract can check that the sender matches the value it has in storage. +One way to do this, is to compute the addresses before deployment and store them as constants in the contract, however a more flexible solution is to have an `initialize` function in the portal contract which can be used to set the address of the Aztec contract. In this model, the portal contract can check that the sender matches the value it has in storage. -To send a message to L1 from your Aztec contract, you must use the `message_portal` function on the `context`. It require just the `content` (again as field). +To send a message to L1 from your Aztec contract, you must use the `message_portal` function on the `context`. When messaging to L1, only the `content` is required (as field). ```rust title="context.nr" fn message_portal(&mut self, content: Field) @@ -105,7 +105,7 @@ To send a message to L1 from your Aztec contract, you must use the `message_port When sending a message from L2 to L1 we don't need to pass recipient, deadline, secret nor fees. Recipient is populated with the attached portal and the remaining values are not needed as the message is inserted into the outbox at the same time as it was included in a block (for the inbox it could be inserted and then only included in rollup block later). :::danger -Need to have access control on the L1 portal contract to not consume messages that were sent from the wrong L2 contract. +Access control on the L1 portal contract is essential to prevent consumption of messages sent from the wrong L2 contract. ::: As earlier, we can use a token bridge as an example. In this case, we are burning tokens on L2 and sending a message to the portal to free them on L1. @@ -128,7 +128,7 @@ fn withdraw( } ``` -When the transaction is included in a rollup block it will be inserted into the `Outbox`, where the recipient portal can consume it from. When consuming, the `msg.sender` must match the `recipient` meaning that only portal can actually consume the message. +When the transaction is included in a rollup block the message will be inserted into the `Outbox`, where the recipient portal can consume it from. When consuming, the `msg.sender` must match the `recipient` meaning that only portal can actually consume the message. ```solidity title="IOutbox.sol" struct L2ToL1Msg { @@ -182,7 +182,7 @@ function withdraw(uint256 _amount, address _recipient, bool _withCaller) ## 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 to ensure that they are not making messages that could be misinterpreted by the recipient. +The application developer should consider creating messages that follow a function call structure e.g., using a function signature and arguments. This will make it easier to prevent producing messages that could be misinterpreted by the recipient. An example of a bad format would 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. @@ -205,9 +205,9 @@ bytes memory message abi.encodeWithSignature( ### 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 L1 part of a deposit might execute just fine, with the L2 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. +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 L1 part of a deposit might execute just fine, with the L2 part failing. If this is not handled well, the funds may be lost forever! The contract builder should therefore consider ways their application can fail cross chain, and handle all 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. +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 is atomic, the failure also reverts consumption. 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). @@ -219,9 +219,9 @@ Error handling for cross chain messages is handled by the application contract a ### Cancellations -A special type of errors is underpriced transactions, here meaning that message is inserted on L1, but the fee available to the sequencer is too low to be included in a rollup block. +A special type of error is an underpriced transaction, in this context it means that a message is inserted on L1, but the attached fee is too low to be included in a rollup block. -For the case of token bridges, this could lead to funds being locked in the bridge forever, as the funds are locked but the message never arrives on L2 to mint the tokens. To address this, the `Inbox` supports cancelling messages after a deadline. However, this must be called by the portal itself, as it will need to "undo" the state changes is made (for example by sending the tokens back to the user). +For the case of token bridges, this could lead to funds being locked in the bridge forever, as funds are locked but the message never arrives on L2 to mint the tokens. To address this, the `Inbox` supports cancelling messages after a deadline. However, this must be called by the portal itself, as it will need to "undo" the state changes is made (for example by sending the tokens back to the user). As this requires logic on the portal itself, it is not something that the protocol can enforce. It must be supported by the application builder when building the portal. @@ -264,16 +264,16 @@ function cancelL1ToAztecMessage( } ``` -The example above ensure that the user can cancel his message if it is underpriced. +The example above ensure that the user can cancel their message if it is underpriced. ### 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. +Designating a caller grants 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 shortly. -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. +When performing multiple cross-chain calls in one action it is important to consider the order of the calls. Say for example, that you want to perform 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: +As 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( diff --git a/docs/docs/dev_docs/contracts/portals/outbox.md b/docs/docs/dev_docs/contracts/portals/outbox.md index 46d072723c7..bf33f06a38b 100644 --- a/docs/docs/dev_docs/contracts/portals/outbox.md +++ b/docs/docs/dev_docs/contracts/portals/outbox.md @@ -2,7 +2,7 @@ title: Outbox --- -The `Outbox` is a contract deployed on L1 that handle message passing from the rollup and back to L1. +The `Outbox` is a contract deployed on L1 that handles message passing from the rollup and to L1. **Links**: [Interface](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol), [Implementation](https://github.com/AztecProtocol/aztec-packages/blob/master/l1-contracts/src/core/messagebridge/Outbox.sol). @@ -46,7 +46,7 @@ function consume( - Will revert with `Outbox__InvalidVersion(uint256 entry, uint256 message)` if the version of the entry and message sender don't match (wrong rollup). ## `get()` -Retries the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. +Retrieves the `entry` for a given message. The entry contains fee, occurrences, deadline and version information. ```solidity function get(bytes32 _entryKey) From 6af29ccec52e0fd221381b3e51f4f4908e17e277 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Fri, 11 Aug 2023 10:49:38 +0000 Subject: [PATCH 4/8] chore: address comments --- docs/docs/dev_docs/contracts/portals/inbox.md | 6 +++--- docs/docs/dev_docs/contracts/portals/main.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/docs/dev_docs/contracts/portals/inbox.md b/docs/docs/dev_docs/contracts/portals/inbox.md index 07e18335949..5a954a76a7c 100644 --- a/docs/docs/dev_docs/contracts/portals/inbox.md +++ b/docs/docs/dev_docs/contracts/portals/inbox.md @@ -30,10 +30,10 @@ function sendL2Message( #### Edge cases -- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient is too large to be included in a field (~254 bits). +- Will revert with `Inbox__ActorTooLarge(bytes32 actor)` if the recipient is larger than the field size (~254 bits). - Will revert with `Inbox__DeadlineBeforeNow()` if the deadline is before the current block. -- Will revert with `Inbox__ContentTooLarge(bytes32 content)` if the content is too large to be included in a field (~254 bits). -- Will revert with `Inbox__SecretHashTooLarge(bytes32 secretHash)` if the secret hash is too large to be included in a field (~254 bits). +- 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__FeeTooHigh()` if the fee is larger than `type(uint64).max`. - Will revert `Inbox__IncompatibleEntryArguments(bytes32 entryKey, uint64 storedFee, uint64 feePassed, uint32 storedVersion, uint32 versionPassed, uint32 storedDeadline, uint32 deadlinePassed)` if insertion is not possible due to invalid entry arguments. diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md index eb019d19d73..cbb6178c615 100644 --- a/docs/docs/dev_docs/contracts/portals/main.md +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -6,7 +6,7 @@ title: Portals A portal is the point of contact between L1 and a specific contract on Aztec. For applications such as token bridges, this is the point where the tokens are are held on L1 while used in L2. -As outlined in the [foundational concepts](../../../concepts/foundation/communication/cross_chain_calls.md), an Aztec L2 contract is linked to *ONE* L1 contract at time of deployment (specified by the developer). This L1 contract is the only that can send messages to that specific L2 contract, and the only party that can receive messages sent from the L2 contract to L1. Note, that a portal don't actually need to be a contract, it could be any address on L1. We say that an Aztec contract is attached to a portal. +As outlined in the [foundational concepts](../../../concepts/foundation/communication/cross_chain_calls.md), an Aztec L2 contract is linked to *ONE* L1 address at time of deployment (specified by the developer). This L1 address is the only address that can send messages to that specific L2 contract, and the only address that can receive messages sent from the L2 contract to L1. Note, that a portal don't actually need to be a contract, it could be any address on L1. We say that an Aztec contract is attached to a portal. ## Passing data to the rollup @@ -21,7 +21,7 @@ When sending messages, we need to specify quite a bit of information beyond just | Recipient | `L2Actor` | The message recipient. 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. | | Deadline | `uint256` | The deadline for the message to be consumed. If the message has not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | | 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. 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`](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 to keep the consumption private. To consume the message the caller must know the pre-image (the value that was hashed) - so keep track of the pre-images! 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. | +| 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 the [`computeMessageSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | | Fee | `uint64` | The fee to the sequencer for including the message. This is the amount of ETH that the sequencer will receive for including the message. Note that it is not a full `uint256` but only `uint64`| :::warning @@ -41,7 +41,7 @@ With all that information at hand, we can call the `sendL2Message` function on t As time passes, a sequencer will see your tx, the juicy fee provided and include it in a rollup block. Upon inclusion, it is removed from L1, and made available to be consumed on L2. -To consume the message, we can use the `consume_l1_to_l2_message` function on the `context`. +To consume the message, we can use the `consume_l1_to_l2_message` function within the `context` struct. The `msg_key` is the hash of the message produced from the `sendL2Message` call, the `content` is the content of the message, and the `secret` is the pre-image hashed to compute the `secretHash`. ```rust title="context.nr" From 19b1ad8489de1a6719ba71e028205eb89671d7a0 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Fri, 11 Aug 2023 11:13:26 +0000 Subject: [PATCH 5/8] chore: add data structures + minor fixes. --- .../contracts/portals/data_structures.md | 127 ++++++++++++++++++ docs/docs/dev_docs/contracts/portals/main.md | 2 +- docs/sidebars.js | 1 + 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 docs/docs/dev_docs/contracts/portals/data_structures.md diff --git a/docs/docs/dev_docs/contracts/portals/data_structures.md b/docs/docs/dev_docs/contracts/portals/data_structures.md new file mode 100644 index 00000000000..5d7de278e9f --- /dev/null +++ b/docs/docs/dev_docs/contracts/portals/data_structures.md @@ -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. + +```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). | +| `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. + +```solidity title="DataStructures.sol" + struct L1Actor { + address actor; + uint256 chainId; + } +``` + +| Name | Type | Description | +| -------------- | ------- | ----------- | +| `actor` | `address` | The ethereum 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 chainid for the entity. Used when specifying sender/recipient with an entity that is on L2. + +```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. | + +## `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. | +| `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. | +| `deadline` | `uint32` | The message consumption-deadline time in seconds. | +| `fee` | `uint64` | The fee that the sequencer will be paid for inclusion of the message. | + +## `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. | + + + + diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md index cbb6178c615..9cc54a8f4a4 100644 --- a/docs/docs/dev_docs/contracts/portals/main.md +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -86,7 +86,7 @@ fn mint( After the transaction has been mined the message is consumed (and cannot be consumed again), the tokens have been minted and the user can use them on L2. -## Passing data data to L1 +## Passing data to L1 To pass data to L1, we use the `Outbox`. The `Outbox` is the mailbox for L2 to L1 messages. This is the location on L1 where all the messages from L2 will live, and where they can be consumed from. diff --git a/docs/sidebars.js b/docs/sidebars.js index d7816972c37..e23a09a5160 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -225,6 +225,7 @@ const sidebars = { id: "dev_docs/contracts/portals/main", }, items: [ + "dev_docs/contracts/portals/data_structures", "dev_docs/contracts/portals/registry", "dev_docs/contracts/portals/inbox", "dev_docs/contracts/portals/outbox", From 36088a86d516acc2ee017fe8b888de8584c01731 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:47:01 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Rahul Kothari --- .../docs/dev_docs/contracts/portals/data_structures.md | 10 +++++----- docs/docs/dev_docs/contracts/portals/inbox.md | 6 +++--- docs/docs/dev_docs/contracts/portals/main.md | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs/dev_docs/contracts/portals/data_structures.md b/docs/docs/dev_docs/contracts/portals/data_structures.md index 5d7de278e9f..2a723a6045f 100644 --- a/docs/docs/dev_docs/contracts/portals/data_structures.md +++ b/docs/docs/dev_docs/contracts/portals/data_structures.md @@ -21,7 +21,7 @@ An entry for the messageboxes multi-sets. | Name | Type | Description | | -------------- | ------- | ----------- | -| `fee` | `uint64` | The fee provided to sequencer for including in the inbox. 0 if Outbox (as not applicable). | +| `fee` | `uint64` | The fee provided to the sequencer for including the message in the inbox. 0 if Outbox (as it is not applicable). | | `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. | @@ -40,7 +40,7 @@ An entity on L1, specifying the address and the chainid for the entity. Used whe | Name | Type | Description | | -------------- | ------- | ----------- | -| `actor` | `address` | The ethereum address of the actor | +| `actor` | `address` | The L1 address of the actor | | `chainId` | `uint256` | The chainId of the actor. Defines the blockchain that the actor lives on. | @@ -58,7 +58,7 @@ An entity on L2, specifying the address and the chainid for the entity. Used whe | 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. | +| `version` | `uint256` | The version of Aztec that the actor lives on. | ## `L1ToL2Message` @@ -82,7 +82,7 @@ A message that is sent from L1 to L2. | `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. | | `deadline` | `uint32` | The message consumption-deadline time in seconds. | -| `fee` | `uint64` | The fee that the sequencer will be paid for inclusion of the message. | +| `fee` | `uint64` | The fee that the sequencer will be paid for the inclusion of the message. | ## `L2ToL1Message` @@ -120,7 +120,7 @@ A snapshot of the registry values. | `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. | +| `blockNumber` | `uint256` | The block number at which the snapshot was created. | diff --git a/docs/docs/dev_docs/contracts/portals/inbox.md b/docs/docs/dev_docs/contracts/portals/inbox.md index 5a954a76a7c..41551eabe0e 100644 --- a/docs/docs/dev_docs/contracts/portals/inbox.md +++ b/docs/docs/dev_docs/contracts/portals/inbox.md @@ -21,7 +21,7 @@ function sendL2Message( | 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. | +| 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. | | Deadline | `uint256` | The message consumption deadline. If the message have not been removed from the `Inbox` and included in a rollup block by this point, it can be *cancelled* by the portal (the portal must implement logic to cancel). | | 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`](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 the [`computeMessageSecretHash`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/aztec.js/src/utils/secrets.ts) to compute it from a secret. | @@ -38,7 +38,7 @@ function sendL2Message( - Will revert `Inbox__IncompatibleEntryArguments(bytes32 entryKey, uint64 storedFee, uint64 feePassed, uint32 storedVersion, uint32 versionPassed, uint32 storedDeadline, uint32 deadlinePassed)` if insertion is not possible due to invalid entry arguments. ## `cancelL2Message()` -Cancels a message that have not yet been consumed. +Cancels a message that has not yet been consumed. ```solidity function cancelL2Message( @@ -80,7 +80,7 @@ function batchConsume(bytes32[] memory _entryKeys, address _feeCollector) extern ## `withdrawFees()` -Will claim the fees that have accrued for the `msg.sender` from consuming messages. +Will claim the fees that has accrued to the `msg.sender` from consuming messages. Let the sequencer withdraw fees from the inbox. diff --git a/docs/docs/dev_docs/contracts/portals/main.md b/docs/docs/dev_docs/contracts/portals/main.md index 9cc54a8f4a4..4af8dcdb944 100644 --- a/docs/docs/dev_docs/contracts/portals/main.md +++ b/docs/docs/dev_docs/contracts/portals/main.md @@ -84,7 +84,7 @@ fn mint( } ``` -After the transaction has been mined the message is consumed (and cannot be consumed again), the tokens have been minted and the user can use them on L2. +After the transaction has been mined, the message is consumed, and the tokens have been minted on Aztec and are ready for use by the user. A consumed message cannot be consumed again. ## Passing data to L1 @@ -92,9 +92,9 @@ To pass data to L1, we use the `Outbox`. The `Outbox` is the mailbox for L2 to L Similarly to messages going to L2 from L1, a message can only be consumed by the recipient, however note that it is up to the portal contract to ensure that the sender is as expected! -Recall, we mentioned the Aztec contract specifies what portal it is attached to at deployment. This value is stored in the rollup's contract tree, hence these links are not directly readable on L1. Also, it is possible to attach multiple aztec contracts to the same portal, the portal must ensure that the sender is as expected. +Recall that we mentioned the Aztec contract specifies what portal it is attached to at deployment. This value is stored in the rollup's contract tree, hence these links are not directly readable on L1. Also, it is possible to attach multiple aztec contracts to the same portal. -One way to do this, is to compute the addresses before deployment and store them as constants in the contract, however a more flexible solution is to have an `initialize` function in the portal contract which can be used to set the address of the Aztec contract. In this model, the portal contract can check that the sender matches the value it has in storage. +The portal must ensure that the sender is as expected. One way to do this, is to compute the addresses before deployment and store them as constants in the contract, however a more flexible solution is to have an `initialize` function in the portal contract which can be used to set the address of the Aztec contract. In this model, the portal contract can check that the sender matches the value it has in storage. To send a message to L1 from your Aztec contract, you must use the `message_portal` function on the `context`. When messaging to L1, only the `content` is required (as field). @@ -219,7 +219,7 @@ Error handling for cross chain messages is handled by the application contract a ### Cancellations -A special type of error is an underpriced transaction, in this context it means that a message is inserted on L1, but the attached fee is too low to be included in a rollup block. +A special type of error is an underpriced transaction - it means that a message is inserted on L1, but the attached fee is too low to be included in a rollup block. For the case of token bridges, this could lead to funds being locked in the bridge forever, as funds are locked but the message never arrives on L2 to mint the tokens. To address this, the `Inbox` supports cancelling messages after a deadline. However, this must be called by the portal itself, as it will need to "undo" the state changes is made (for example by sending the tokens back to the user). From e13912114ee84b4d3a56b16c818eecc1ac8dfd97 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Mon, 14 Aug 2023 07:54:49 +0000 Subject: [PATCH 7/8] chore: expand on secretHash in data structures --- docs/docs/dev_docs/contracts/portals/data_structures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/dev_docs/contracts/portals/data_structures.md b/docs/docs/dev_docs/contracts/portals/data_structures.md index 2a723a6045f..b8e21dea115 100644 --- a/docs/docs/dev_docs/contracts/portals/data_structures.md +++ b/docs/docs/dev_docs/contracts/portals/data_structures.md @@ -29,7 +29,7 @@ An entry for the messageboxes multi-sets. ## `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 for the entity. Used when specifying sender/recipient with an entity that is on L1. ```solidity title="DataStructures.sol" struct L1Actor { @@ -46,7 +46,7 @@ An entity on L1, specifying the address and the chainid for the entity. Used whe ## `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. +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. ```solidity title="DataStructures.sol" struct L2Actor { @@ -80,7 +80,7 @@ A message that is sent from L1 to L2. | `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. | +| `secretHash` | `field (~254 bits)` | The hash of a secret pre-image that must be known to consume the message on L2. 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. | | `fee` | `uint64` | The fee that the sequencer will be paid for the inclusion of the message. | From f267d43b7441215f2cdae6bb912d5a09a0324cc8 Mon Sep 17 00:00:00 2001 From: LHerskind Date: Mon, 14 Aug 2023 09:23:59 +0000 Subject: [PATCH 8/8] chore: fix cross_chain_calls naming for consistency --- .../communication/cross_chain_calls.md | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/docs/concepts/foundation/communication/cross_chain_calls.md b/docs/docs/concepts/foundation/communication/cross_chain_calls.md index 12b28f209c4..fe279f29874 100644 --- a/docs/docs/concepts/foundation/communication/cross_chain_calls.md +++ b/docs/docs/concepts/foundation/communication/cross_chain_calls.md @@ -98,45 +98,43 @@ For the sake of cross-chain messages, this means inserting and nullifying L1 $\r While a message could theoretically be arbitrary 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. 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(LxLyCrossMsg)`) on chain or in the trees, this way, we need only update a single storage slot per message. +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: actorAddress, - uint256: chainid, + address: actor, + uint256: chainId, } struct L2Actor { - bytes32: actorAddress, + bytes32: actor, uint256: version, } -struct L1L2CrossMsg { +struct L1ToL2Msg { L1Actor: sender, L2Actor: recipient, bytes32: content, bytes32: secretHash, + uint32 deadline, + uint64 fee, } -struct L2L1CrossMsg { +struct L2ToL1Msg { L2Actor: sender, L1Actor: recipient, bytes32: content, } ``` :::info -The 32 bytes might practically need to be a field element. -```solidity -uint256 p = 21888242871839275222246405745257275088548364400416034343698204186575808495617; -bytes32 content = bytes32(uint256(sha256(message)) % p); -``` +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 `L1L2CrossMsg` can be 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 `L1L2CrossMsg` on L1 to know when it is consumed on L2. +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. Also, we include the `deadline` and `fee` values to have a fee-market for message inclusion and to ensure that messages are not stuck in the pending set forever. ::: ## Combined Architecture