---
tags: ORU
title: Gas implementation in ORU
status: Draft
author: Marigold
type:
created:
date:
version: 1
---
# Note of the ORU gas implementation
There are some current limitations in the rollup implementation:
- Full state limit: it is not a part of protocol but in the rollup semantics. This feature is not implemented yet. The main idea is that the rollup should be store in RAM, for instance, we have 8GB of rollup in RAM, then it is easier to call from this RAM storage and compute the state in rollup. This help to have scalable for the rollup. Right now, it just store inside the state validator, asking state validator to have more and more RAM, but it is not the best solution.
Currently, the entire state is loaded into a memory, and it can cause to break the OCaml compiler (RAM is full).
- Refutation proof size limit: in rollup library and protocol. This is a max size of the rejection proofs. It has a max size because it is a size of tezos operation. Tezos operation has down on the size, so that why we need to have a refutation proof size limit. Because if someone want to send an invalid operation, etc. we need to be able to check it, replay it, we need to have a refutation proof bigger than the tezos operation size limit. Then we will never be able to refuse the bad hash.
How does it work? In the rollup node, always generate the proof, and evaluation on the operation, and check if it is bigger than the proof size limit. If it is, then we consider the operation is an bad operation and refuse to be injected. Right now, it just rejected.
In the protocol: if we say that we have a proof that is too long (too big?), we can proof the semantic of the operations is just a node. It has no effect and it has the same hash.
In the rollup library: if the refutation proof size is too big, it will be refused. It is because, in general we will aggregate multiple operations.
- Gas limit: in rollup semantics and in protocol. The reason we want to have it in rollup because we need to replay them. So that why we need to play them on-chain, so we want to have a gas limit that within the operation can submit.This is deal with both in the rollup semantics (that has to define the gas cost, various operations) and in the protocol (because the protocol will check the gas limit that will not cross up). The rollup semantics have to define it owns gas.
- Effect gas limit: this is only in the protocol. An operation can trigger an operation effect, and the effect will have to be run by the protocol. There is an effect gas limit at the block level and when we add an operation it will fairly effect gas limit.
- Operation size: in protocol. There are two operations size. The first one is the defactor limit (it means there is size limit in Tezos, which the size limit we cannot submit many operations and many too big operations.). This operation size in rollup is its own size limit. The reason is that we are storing the operation on-chain so we want to have a limit, even if it is high, so that people cannot fill the context forever.
Code implementation of the gas in rollup:
- `rollup_repr.ml`: the module `Pre_block` the type `t`
```ocaml
type t =
{
rev_content : operation list;
tezos_level : Level_repr.t;
(*
Gas limit mention above
*)
cumulated_run_gas : Z.t;
(*
Effect gas limit mention above
*)
cumulated_effect_gas : Z.t;
(*
Operation size
*)
cumulated_size : Z.t
}
```
If the user use more size, then they have to pay more xtz. We can send a burn rate, so if they want to load a state for instance 10GB, they will need to be able to pay for the entire file. It is the strategy in level 1 as well.
- For the refutation size limit implementation:
For the current implementation: when doing the replay, if there is proof that is too big, it will stop checking the proofs and return the initial hash.
`rollup_apply.ml` at the pattern
```ocaml
| Reject_committed_block block_rejection ->
(* We do not process the backlog on rejctions because:
- Rejections are already expensive in gas, we want to save
as much as possible.
- It is not necessary to process the backlog to deal with rejections. *)
...
(* relay operation *)
let Rollup_operation_rejection.{operation_index; state_trace} =
rejection_content
in
...
(* gas limit for the replace transaction implementation *)
....
(* replay the transition with the state trace, returning the hash,
effects and checking that the gas limit is respected.
NOOP if it is not. *)
let (after_hash', effect') =
try (
let state_trace = New_storage.Stream_produce.to_consume state_trace in
(*in replay we set the ~run_gas_limit, the function in
`rollup_consume.ml` module InfraTransition, function main() will
be called to consume until reach the gas limit.
*)
return @@ Rollup.replay_rollup_transition
~hash:before_hash ~run_gas_limit ~effect_gas_limit
~operation ~state_trace
) with
| ..
```
- Gas limit implementation: `rollup_batcher.ml`: at the functor `Transition`
```ocaml
module Make (P: BATCHER_PARAMETER) = struct
include MakeOperations (P: InfraOperation)
module Transition =
functor (M: ROLLUP_STATEFUL_STORAGE) (G: ROLLUP_STATEFUL_GAS) -> struct
...
```
The `Transition` define the `G: ROLLUP_STATEFUL_GAS` because in rollup semantics will define how we consume gas. The name `_STATEFUL_` is due to the nature that this gas is relating to rollup state. It has not been define in the `tx_only`, it is defined in the `rollup_counter.ml` as an example.
```ocaml
module InfraTransition =
functor (M:ROLLUP_STATEFUL_STORAGE) (G: ROLLUP_STATEFUL_GAS) -> struct
....
(* define the semantics of counter rollup *)
let main = fun ~(parameter) ->
match parameter with
| Add z ->
(
(* get current counter *)
let counter = get () in
(* change the value of the counter *)
set (Z.add counter z);
(* then we consume some gas, it is straightforward and simple *)
G.consume (Z.div z (Z.of_int 100));
```
In `rollup_sigs.ml` it defines the consume gas:
```ocaml
(* to change the gas limit: it is not done *)
let make_empty () = ..
exception Out_of_gas
(* it consumes the gas until we reach the gas limit *)
let consume : t -> t' -> unit = fun t diff ->
let x = get t in
let x' = Z.sub x diff in
...
```
It was implemented that way (to consume the gas until it reaches the limit) because the rejection proof can be done in a reasonable time which is the gas a single tezos operation has.
All the constants for the rollup is just abiratry, that why it needs a lot of benchmark to have a good constants. Those constants are defined in `rollup_apply.mli`.
```ocaml
val rejection_delay_limit: Int32.t
val run_gas_limit_per_block : Z.t.
...
```
- The effect gas limit implementation:
In the `rollup_repr.ml` module `Rollup_operation`
```ocaml
module Rollup_operation = struct
type t = {
content : bytes;
(* whenever include an operation,
it has to run the run_gas_limit and
effect_gas_limit. The reason is that
there is a limit per block, we want a limit per block
because we want the validating size of validator is bounded.
So that someone will not be able to just fill the block with
thousands of operations that will take each whole minutes to run.*)
(*
Each operation will commit to its run_gas_limit every time
effect_gas_limit, and then we check in block that it won't go its
gas setting (?). It is defined in module Genesis, function pre_block.
It is the same things with effect_gas_limit and the operation size.
*)
run_gas_limit : Z.t;
effect_gas_limit: Z.t;
}
...
module Genesis = struct
...
let pre_block : Pre_block = {
..
cumulated_run_gas = Z.zero;
cumulated_effect_gas = Z.zero;
cumulated_size = Z.zero.
}
```
For the effect gas limit it is simpler than the gas limit. It is simpler because it does not depend on the semantics of the rollup. Because the effect is evaluated on-chain so that we don't need to change the semantics of the rollup, we don't even need to change anything. It is defined in
`rollup_apply.ml` when we cross the effect at the function
```ocaml
(* it process of effect single (op: Rollup_effect.Single.t),
right now the limit is not enforce, TODO:
what we should do for this function is to consume the effect gas.
TODO: It consumes the protocol gas, fit within the effect gas limit that
we have. It does consume gas but the function does not check it yet.
*)
let process_effect_single (ctxt: Alpha_context.t) rollup_id
(op: Rollup_effect.Single.t) : Alpha_context.t.tzresult lwt.t =
...
```
Right now the user is paying for the effect gas, the function in the pattern `Submit_rollup_operation`
```ocaml
| Submit_rollup_operation {content; fee; rollup_id} -> (
...
(* pay the fees, update the effect gas,but it does not consume it yet
and check that it is not too big. *)
let* (ctxt, fee_paid) =
let Pending_operation_queue.{current_run_gas_rate;
current_effect_gas_rate; _} =
...
```
The effect gas is actually a constant of operation, the function `process_effect_single`, at pattern `Withdraw` . Withdraw and we don't need to execute the operation at pattern `Event _` is a to do thing to know how much gas we consumed. Withdraw does the transaction, the gas consumed is bounded. We don't need to consume it dynamicly. The gas is consume directly at the moment (in the function `transition` `let effect_gas = Rollup_effect_gas.pre_compute lst in` (this function will pre compute the gas before the effected run, produce it and consume the effected gas run). We don't need to wait until the effected running, to consume its gas. This defined in `rollup_batcher.ml`.
- Operation size limit implementation
Every time we have an operation, submit it, we check that we don't go over the size limit per block. It defines at the pattern `Submit_rollup_operation` at the function
```ocaml
| Submit_rollup_operation {content; fee; rollup_id} ->
(
...
(* size *)
let* () =
let size = Rollup_opration.size content in
...
in
```
There are two checks:
- The two check before (the function at run gas, effect gas) the compute function `size`: they are actually looking at the operation limit, each operation submit its own limit, then we check that they don't go over the block limit. If it is too big then it is an NOOP. (no operation?)
That is all about the gas. There is something else that we need to keep in mind is **how this gas is pay for?**
Basically, we are following Etherium, **EIP 1559.**
How the gas price is set? we could say it is the same price as Tezos gas. Then how we set the price for the operation size?
The way the current implementation deal with it is the gas cost will be computed automatically, so that the baker or any block validator does not need to look into the operations on the rollup to choose which block they should bake (like which block pay more fee, etc.). It is at the function `compute_fees` in the file `rollup_apply.ml`
```ocaml
(* we look at the current run_gas_limit and effect_gas_limit
and the current_run_gas_rate and then we apply and add them.
But then the question is how is the rate current_run_gas_rate
is computed.
*)
let compute_fee ~current_run_gas_rate ~run_gas_limit ~current_effect_gas_fee
~effect_gas_limit =
...
```
We have block that are filled with operations. At the end of the block we can see how much gas and how much effect gas were consumed. For instance, at the end we have 50% gas consumed and 95% effect gas consumed.
The idea of 50% and 95% is that we have a fix rate of block.
We can say for instance 1 mtez/gas is the initial rate of a block, and 0,4 mtez/effect gas. These are what we have in the beginning of the block. We have a target for gas consumption per block, for instance 90% so what we said at the end of the example block in this example is that we only have 50% of the gas consume in this whole block. So it mean the gas is priced too expensively, because of the current price of the block as 50%. So what we do at the end of this block is that update the price of gas so that for instance 1mtez/gas for instance to 0.9 mtez/gas, so that people consume more gas for the next block. We do this until we reach to 90% (the assumption that we have).
For the effect gas, it is the opposite. It has 95%, and we never want it happens, because we want more operations that are willing to pay for money to get their operations accepted. For it we will increase the price so that the next block we can get something for instance at 92% until we are around 95%.
The implementations of them is defined in `rollup_apply.ml` at function `update_run_gas_rate` and `update_effect_gas_rate`
```ocaml
(* we have a current limit ~limit, we have the current gas rate ~gas_rate
and we have the gas that is actually consumed ~actual *)
let update_run_gas_rate ~(gas_rate: Tez_repr.t) ~actual ~limit =
...
(* We compute the percentage,
if we are in the acceptable range [80; 90],
then we don't change anything
else if we are lower than 80
then we reduce the cost
if we are higher
we will increase the cost
*)
let percentage = Z.to_int Z.(div (mul actual (of_int 100)) limit) in
let open Compare.Int in
(* check if we are in the acceptable range *)
if (80 <= percentage && percentage <= 90)
then gas_rate (* yes, it is, then don't change anything *)
else if (percentage <= 80) (* if we are lower than the range *)
then (
(* we reduce the cost *)
...)
else (
(* if we are higher than the range >= 90 *)
(* we will increase the cost *)
...
)
(* and similar for the effect_gas *)
let update_effect_gas_rate ~(gas_rate: Tez_repr.t) ~actual ~limit =
...
```
We may also want to do the same thing for the size limit `size_limit_per_block`.
To Do:
- Effect gas consume
- State proof
In the file `rollup_apply.ml` the function `main` and the pattern `Touch` it is to process the backlog. The real result of the rollup is then effected in the `apply.ml` at the pattern `Rollup_content` , function `Rollup_apply.main ...` . Each operation has a result in the `receipt` in this result we will say how much gas was take, `consumed_gas` for the transactions, how much storage, etc. it is used by the client library.