owned this note
owned this note
Published
Linked with GitHub
# SCORU Communication Layer
**Status**: Awaiting feedback
This document outlines a design of cross-layer communication,
including deposits and withdrawals of assets.
We want to provide minimal protocol support on top of which more advanced rollup specific protocols can be built.
For more info and a break-down of the work, see the [SCORU Communication Layer](https://gitlab.com/tezos/tezos/-/milestones/76#tab-issues) milestone.
## Requirements
We need to support:
- L1 contracts (explicit accounts) calling and transferring assets to L2 contracts.
- L2 contracts calling and transferring assets to L1 contracts (explicit accounts)
## Overview
Message passing from L1 to L2 is ultimately achieved by L1 contracts emitting
SCORU operations which are included in a particular SCORU's *inbox*.
The operations in the inbox are detected by all SCORU nodes executing the
corresponding rollup by monitoring internal operations.
Once the inbox has been committed to L1 and the
refutation period has passed, all operations that are contained in the cemented
state have been processed.
Likewise, an L2 contract can send a (asynchronous) message to an L1 contract
by emitting an output-effect which may be processed in L1.
Once the SCORU state including the effect has been cemented it is available for
execution.
## Representing assets
An important aspect of cross-layer communication is depositing and withdrawing
*assets*. Tickets can be used for representing such assets. Only tickets with
a positive amount is permitted as ownership of zero-amount tickets are not tracked.
Each SCORU has a unique address which may hold tickets. The SCORU balance for tickets
are tracked by the
[ticket-balance table](https://gitlab.com/tezos/tezos/-/milestones/38).
A smart contract on L1 may deposit tickets to the rollup by a transaction the rollup address.
The tickets are contained in the parameters. As far as the L1 protocol is
concerned, the SCORU address is the sole owner of the tickets.
## Typed SCORUs
Similar to a smart contract, a SCORU needs to be registered with a concrete type.
That way smart-contract calls a SCORU can be type checked just like regular
contract calls. Allowing any type may be less efficient for rollups that want to restrict
their parameter type (typically TORU currently allows only `exists t. ticket t`). Using `bytes` would
push pressure on L1 Michelson smart-contracts to encode data for L2.
We plan to support a subset of Michelson types — essentially
everything that is `passable` except `big_map` and `sapling_state`. We don't support `big_map`
because they are references to values that exist in the L1 context and `sapling_state`
does not carry any meaning for the rollup.
As an extension we also consider to introduce an existential Michelson type for providing
more flexibility. For instance, that could be used to allow SCORUs to accept
tickets of an arbitrary payload type.
Note that it's possible for rollups to design an arbitrary encoding scheme
by registering the type as `bytes`.
## Rollup Management Protocol
The communication layer between the L1 protocol and any L2 kernel must follow the
specification of a Rollup Management Protocol. In particular to describe such
actions as:
- Sending messages from L1 to L2 (including ticket deposits).
- Sending messages from L2 to L1 (including ticket withdrawals).
- Minting ticket-tokens in L1 based on kernel authorization — for example to represent a minted L2 asset.
- Other effects authorized by the L2 kernel, such as a hardware/ISA upgrade.
This protocol is meant to be shared between kernels and PVMs.
To enable the L1/L2 communication we need at least specifications for an *L1 to L2 message*
and a *L2 to L1 message*.
In the L1 protocol we can represent a message from L1 to L2 — as:
```ocaml
type inbox_message =
Sc_message of
{
payload : Script_repr.expr;
(** A Micheline value containing the parameters passed to the rollup. *)
sender : Alpha_context.Contract.t; (** The L1 caller contract. *)
source : Signature.public_key_hash;
(** The implicit account that originated the transaction. *)
}
```
For L2 to L1 we support grouping of multiple L2 to L1 messages in atomic batches:
```ocaml
type transaction = {
unparsed_parameters_ty : Script_repr.expr; (** The type of the parameters. *)
unparsed_parameters : Script_repr.expr; (** The payload. *)
destination : Destination.t; (** The recipient contract or rollup. *)
entrypoint : Entrypoint.t; (** Entrypoint of the destination. *)
}
type outbox_message =
| Atomic_transaction_batch of { transactions : transaction list}
```
Why support atomic batches? One reason is that it gives the rollup the ability to
choose whether or not atomicity is required for multiple withdrawals.
Another reaons is it requires fewer L1 transaction to execute.
For the protocol layer we will expose functions for encoding values of
these types. Further, any rollup needs to be able to emit and consume
such values.
:::warning
The encoding scheme for the the Rollup Management Protocl types is yet to be decided. Can we use
the standard data-encoding library and serialize *Micheline* expressions to
binary format?
:::
## L1 to L2 messaging
An L1 contract is able to call a contract in L2 by emitting a
*scoru-transaction-operation* where destination is set to a SCORU contract.
A SCORU transaction is similar to a regular (internal) transaction. For
technical reasons we may choose factor it out as a separate internal
manager operation:
```ocaml
and 'kind manager_operation =
...
| Sc_rollup_transaction :
{
destination : Sc_rollup_t.;
entrypoint : Entrypoint.t;
parameters_ty : 'a ty;
parameters : 'a
} ->
-> Kind.sc_rollup_transaction manager_operation
```
The effect of applying the operation in L1 is:
- Transfer ownership of any tickets in the parameters from the callee
contract to the SCORU.
- Emit a "Deposit" message as defined by the Rollup Management Protocol, and
append it to the SCORU's inbox.
:::info
When a SCORU node processes a transaction from the inbox containing tickets,
it may mint a corresponding amount of tickets in L2, in order to represent the
asset there.
::::
### Atomicity
What happens when an execution succeeds on L1 but fails on L2?
Imagine an execution that is successfully submitted to the inbox in L1
but fails to be processed by L2. This could happen if, for example, the
callee contract provided insufficient funds.
From the Tezos protocol's perspective, this is a valid scenario and we do not
intend to protect against it in the protocol layer.
:::info
It is still possible to build SCORUs that support cancellation/re-try logic. For
instance by having L2 emit an output-effect that communicates the failed status of
the transaction as well as potential refunds.
:::
### An example
Following is an example of the execution of an L1 to L2 transaction.
We here assume:
- A SCORU with address `SCORU1`
- A contract, `KT1`
Suppose the contract `KT1` holds some tickets, reflected in the ticket-balance
table:
| Owner x Ticketer x Content | Amount |
| -----------------------------|-------:|
| `(KT1, KTX, Unit)` | 200|
Consider a call to the `KT1` contract that outputs a transaction operation
to `SCORU1`:
```ocaml=
Sc_rollup_transaction {
destination = Destination.scoru_address "SCORU1";
entrypoint = Entrypoint.default;
parameters_ty = pair string (ticket unit);
parameters =
("SCORU_ADDRESS_42",
{ticketer = KTX; content = (); amount = 50});
}
```
The ticket from the parameter is moved from the `KT1` address to the `SCORU1`
address. The new ticket-balance is updated and now contains the following entries:
| Owner x Ticketer x Content | Amount |
| -------------------------------|-------:|
| `(KT1, KTX, Unit)` | 150 |
| `(SCORU1, KTX, Unit)` | 50 |
L1 emits a message which is appended to the `SCORU1`'s inbox. The message is an
encoded version of a Rollup Management Protocol deposit message:
```ocaml
let l1_to_l2_message = Sc_mesage {
payload = seq ["SCORU_ADDRESS_42", pair KTX unit 50}];
sender = KT1
}
```
An ORU node processes the inbox that includes the deposit message and applies the
operation in L2.
:::info
How the transaction is applied in L2 is opaque to L1. One can imagine the
following thing happening on L2 side:
- Mint `50 KTX tokens` and credit `SCORU_ADDRESS_42` (internal L2 address).
- Execute the transaction using the corresponding `KT1` address in L2 to charge for fees.
:::
## L2 to L1 messaging
The way L2 calls L1 is not transparent from the Tezos protocol's point of view.
The important thing to note is that a SCORU can emit effects to its *outbox*.
As mentioned above, calls to smart contracts on L1 can be batched as described
by the `atomic_transaction_batch` Rollup Management Protocol effect. Once a
committed state is cemented — that is when the refutation period has
ended — any such effect may be executed, within a time window, via a
manager operation in L1.
For this purpose we will extend the external `manager_operation` as in:
```ocaml
type _ manager_operation =
| ...
| Sc_rollup_atomic_batch {
sc_rollup : Sc_rollup.t;
lcc_level : Sc_rollup.Commitment.t;
outbox_level : int;
message_index : int;
inclusion_proof : inclusion_proof;
atomic_transaction_batch : Rollup_management_protocol.atomic_transaction_batch
}
```
The `Sc_atomic_batch` operation contains a batch of transfers which involve
executing smart contract calls to the destination contracts. If any of these
invocations fail, for instance because they don't type check, or insufficient
gas was provided, the whole batch is rejected.
Note that anyone can submit rollup withdrawal operation and needs to pay the
gas fees for all contract calls that are included in the batch.
:::warning
Then we need to make sure this is not attackable.
E.g. if some limit (gas, burn) is not enough, it should not burn the right to try to withdraw again.
The source of the operation cannot be the payer, otherwise it may have a change of semantics!
(this reminds me of a proposal for having operations paid by someone else)
:::
### Keeping tracking of processed messages
To prevent messages from being processed multiple times we need to keep track
of the processed ones. Here, we follow a similar approach to TORU with a storage
API for recording messages based on rollup, commitment and index:
```ocaml
val record :
context ->
Sc_rollup.t ->
Sc_rollup.Commitment.t ->
message_position:int ->
context tzresult Lwt.t
```
The underlying implementation should use a `bitset` to minimize overhead.
:::warning
Because `Bitset.add` is in O(`message_position`) we need to make sure the position is legitimate before we call `record`.
:::
Whenever a message has been processed we record it.
Since we want to bound the number of recorded messages, we impose:
- A maximum number of levels in which rollup withdrawal for a particular commit may be executed.
- A cap on the total number of active levels for which we keep an "applied message" record.
- A cap on the maximum number of outbox messages per level.
The total size of the records for applied messages for a rollup is bounded by:
```
maximum-number-of-active-levels * maximum-number-of-outbox-messages-per-level
```
The maximum storage cost must be paid for upfront when originating the rollup.
It's conceivable to make this configurable by the rollup itself.
Beyond this deadline, we may safely remove old records. For this purpose we provide an
additional manager operation for removing old commitments. It may be called by
anyone who care about preventing the rollup from getting stuck due to exceeding
the maximum number of recorded messages.
We will provide a new manager operation for removing old commitments that involve
cleaning up recorded messages:
```ocaml
type _ manager_operation =
| ...
| Sc_rollup_remove_commitment : {
sc_rollup : Sc_rollup.t;
}
```
### An example
Following is an example workflow, assuming an L2 implementation with accounts
and smart contracts:
1) An L2 contract, `SCORU_ADDRESS_42`, emits an L2-to-L1 withdrawal message that transfers
20 `(KTX, Unit)` ticket-tokens to `KT2`.
2) The effect is included in an `atomic_transaction_batch`.
3) The rollup proceeds and a state containing the emitted withdrawal batch in its outbox is committed.
4) The refutation period passes and the new state is cemented.
5) A user executes a `Sc_rollup_withdraw` operation on L1 for the corresponding
withdrawal batch by providing an inclusion proof.
Upon successful execution of the manager operation, the protocol registers that
the outbox message has been processed.
Since the withdrawal batch included a ticket transfer to `KT2`, the ticket-table
is updated:
| Owner x Ticketer x Content | Amount |
| -------------------------------|-------:|
| `(KT1, KTX, Unit)` | 150 |
| `(SCORU1, KTX, Unit)` | 30 |
| `(KT2, KTX, Unit)` | 20 |