Raw File
tx_rollup_state_repr.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 Marigold <contact@marigold.dev>                        *)
(* Copyright (c) 2022 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2022 Oxhead Alpha <info@oxhead-alpha.com>                   *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

open Tx_rollup_errors_repr

type range =
  | Interval of {
      oldest : Tx_rollup_level_repr.t;
      newest : Tx_rollup_level_repr.t;
    }
  | Empty of {next : Tx_rollup_level_repr.t}

let range_newest = function Interval {newest; _} -> Some newest | _ -> None

let range_oldest = function Interval {oldest; _} -> Some oldest | _ -> None

let extend = function
  | Empty {next} -> (Interval {oldest = next; newest = next}, next)
  | Interval {oldest; newest} ->
      let newest = Tx_rollup_level_repr.succ newest in
      (Interval {oldest; newest}, newest)

let shrink = function
  | Empty _ -> error (Internal_error "cannot shrink range")
  | Interval {oldest; newest} when Tx_rollup_level_repr.(oldest < newest) ->
      ok (Interval {oldest = Tx_rollup_level_repr.succ oldest; newest})
  | Interval {newest; oldest = _} ->
      ok (Empty {next = Tx_rollup_level_repr.succ newest})

let belongs_to range level =
  match range with
  | Empty _ -> false
  | Interval {oldest; newest} ->
      Tx_rollup_level_repr.(oldest <= level && level <= newest)

let right_cut range level =
  match Tx_rollup_level_repr.pred level with
  | None -> ok (Empty {next = Tx_rollup_level_repr.root})
  | Some predecessor -> (
      match range with
      | Interval {oldest; newest = _} when belongs_to range level ->
          if Tx_rollup_level_repr.(oldest <= predecessor) then
            ok (Interval {oldest; newest = predecessor})
          else ok (Empty {next = level})
      | _ -> error (Internal_error "cannot cut range"))

let left_extend range level =
  match range with
  | Interval {oldest = _; newest} -> ok (Interval {oldest = level; newest})
  | Empty {next} ->
      let newest =
        Option.value ~default:level (Tx_rollup_level_repr.pred next)
      in
      ok (Interval {oldest = level; newest})

let range_count = function
  | Empty _ -> 0
  | Interval {oldest; newest} ->
      Int32.(succ @@ Tx_rollup_level_repr.diff newest oldest |> to_int)

let range_encoding : range Data_encoding.t =
  Data_encoding.(
    union
      [
        case
          (Tag 0)
          ~title:"empty"
          (obj1 (req "next" Tx_rollup_level_repr.encoding))
          (function Empty {next} -> Some next | _ -> None)
          (fun next -> Empty {next});
        case
          (Tag 1)
          ~title:"interval"
          (obj2
             (req "newest" Tx_rollup_level_repr.encoding)
             (req "oldest" Tx_rollup_level_repr.encoding))
          (function
            | Interval {newest; oldest} -> Some (newest, oldest) | _ -> None)
          (fun (newest, oldest) -> Interval {newest; oldest});
      ])

let pp_range fmt = function
  | Empty {next} -> Format.(fprintf fmt "next: %a" Tx_rollup_level_repr.pp next)
  | Interval {oldest; newest} ->
      Format.(
        fprintf
          fmt
          "oldest: %a newest: %a"
          Tx_rollup_level_repr.pp
          oldest
          Tx_rollup_level_repr.pp
          newest)

type watermark = Tx_rollup_level_repr.t option

let is_above_watermark watermark level =
  match watermark with
  | Some watermark -> Tx_rollup_level_repr.(watermark < level)
  | None -> true

let make_watermark level = Some level

(** The state of a transaction rollup is composed of [burn_per_byte]
    and [inbox_ema] fields. [initial_state] introduces their initial
    values. Both values are updated by [update_burn_per_byte] as the
    rollup progresses.

    [burn_per_byte] state the cost of burn per byte to be paid for
    each byte submitted to a transaction rollup inbox. [inbox_ema]
    is a key factor to impact the update of [burn_per_byte].

    [inbox_ema] is the N-block EMA to react to recent N-inbox size
    changes. N-block EMA is an exponential moving average (EMA), that
    is a type of moving average that places a greater weight and
    significance on the most N data points. The purpose of [inbox_ema]
    is to get lessened volatility of burn, that is more resistant to
    spurious spikes of [burn_per_byte].

   The state of the transaction rollup also keeps track of four pointers
   to four different rollup levels.

    - The [commitment_oldest_level] is the level of the oldest
      finalized commitment still stored in the layer-1 storage.

    - The [commitment_newest_level] is the level of the most recent
      unfinalized commitment in the layer-1 storage.

    - The [oldest_inbox_level] is the level of the oldest inbox still stored
      in the layer-1 storage.

    - The [newest_level] is the level of the most recent inbox in the
      layer-1 storage.
*)
type t = {
  last_removed_commitment_hashes :
    (Tx_rollup_message_result_hash_repr.t * Tx_rollup_commitment_repr.Hash.t)
    option;
  finalized_commitments : range;
  unfinalized_commitments : range;
  uncommitted_inboxes : range;
  commitment_newest_hash : Tx_rollup_commitment_repr.Hash.t option;
  tezos_head_level : Raw_level_repr.t option;
  burn_per_byte : Tez_repr.t;
  inbox_ema : int;
  allocated_storage : Z.t;
  occupied_storage : Z.t;
  commitments_watermark : watermark;
}

(*

   The main use of a transaction rollup state is to keep track of four
   pointers to four different rollup levels (see above).

   When the rollup is created, these four pointers are initialized with
   the [None] value, because no inboxes or commitments have been created
   yet. Because inboxes and commitments can be removed from the layer-1
   context under certain circumstances, they can be reset to [None].

   The state allows us to keep track of three intervals: the finalized
   commitments (whose inboxes have been removed from the layer-1
   storage), the unfinalized commitments (whose inboxes are still in
   the layer-1 storage), and uncommitted inboxes (that is, inboxes
   which are still waiting for a commitment).

     finalized     uncommitted
       ^^^^^^        ^^^^^^^^
       [------------]            commitments
             [--------------]    inboxes
             ^^^^^^^^
            unfinalized

    Note that this layout is not the only one that we can witness in
    the layer-1 storage, even if it is the more common. It is possible
    for instance that there is no unfinalized commitments at a given
    time.

     finalized
       ^^^^^^
       [----]                    commitments
             [--------------]    inboxes
             ^^^^^^^^^^^^^^^^
                uncommitted

     Or that we have no more inboxes, but only finalized commitments.

     finalized
       ^^^^^^
      CT
       [-----]              commitments
                            inboxes

 *)

let initial_state ~pre_allocated_storage =
  {
    last_removed_commitment_hashes = None;
    finalized_commitments = Empty {next = Tx_rollup_level_repr.root};
    unfinalized_commitments = Empty {next = Tx_rollup_level_repr.root};
    uncommitted_inboxes = Empty {next = Tx_rollup_level_repr.root};
    commitment_newest_hash = None;
    tezos_head_level = None;
    burn_per_byte = Tez_repr.zero;
    inbox_ema = 0;
    allocated_storage = pre_allocated_storage;
    occupied_storage = Z.zero;
    commitments_watermark = None;
  }

let encoding : t Data_encoding.t =
  let open Data_encoding in
  conv
    (fun {
           last_removed_commitment_hashes;
           finalized_commitments;
           unfinalized_commitments;
           uncommitted_inboxes;
           commitment_newest_hash;
           tezos_head_level;
           burn_per_byte;
           allocated_storage;
           occupied_storage;
           inbox_ema;
           commitments_watermark;
         } ->
      ( ( last_removed_commitment_hashes,
          finalized_commitments,
          unfinalized_commitments,
          uncommitted_inboxes,
          commitment_newest_hash,
          tezos_head_level,
          burn_per_byte,
          allocated_storage,
          occupied_storage,
          inbox_ema ),
        commitments_watermark ))
    (fun ( ( last_removed_commitment_hashes,
             finalized_commitments,
             unfinalized_commitments,
             uncommitted_inboxes,
             commitment_newest_hash,
             tezos_head_level,
             burn_per_byte,
             allocated_storage,
             occupied_storage,
             inbox_ema ),
           commitments_watermark ) ->
      {
        last_removed_commitment_hashes;
        finalized_commitments;
        unfinalized_commitments;
        uncommitted_inboxes;
        commitment_newest_hash;
        tezos_head_level;
        burn_per_byte;
        allocated_storage;
        occupied_storage;
        inbox_ema;
        commitments_watermark;
      })
    (merge_objs
       (obj10
          (req
             "last_removed_commitment_hashes"
             (option
             @@ obj2
                  (req
                     "last_message_hash"
                     Tx_rollup_message_result_hash_repr.encoding)
                  (req
                     "commitment_hash"
                     Tx_rollup_commitment_repr.Hash.encoding)))
          (req "finalized_commitments" range_encoding)
          (req "unfinalized_commitments" range_encoding)
          (req "uncommitted_inboxes" range_encoding)
          (req
             "commitment_newest_hash"
             (option Tx_rollup_commitment_repr.Hash.encoding))
          (req "tezos_head_level" (option Raw_level_repr.encoding))
          (req "burn_per_byte" Tez_repr.encoding)
          (req "allocated_storage" n)
          (req "occupied_storage" n)
          (req "inbox_ema" int31))
       (obj1
          (req "commitments_watermark" @@ option Tx_rollup_level_repr.encoding)))

let pp fmt
    {
      last_removed_commitment_hashes;
      finalized_commitments;
      unfinalized_commitments;
      uncommitted_inboxes;
      commitment_newest_hash;
      tezos_head_level;
      burn_per_byte;
      allocated_storage;
      occupied_storage;
      inbox_ema;
      commitments_watermark;
    } =
  Format.(
    fprintf
      fmt
      "cost_per_byte: %a inbox_ema: %d finalized_commitments: %a \
       unfinalized_commitments: %a uncommitted_inboxes: %a \
       commitment_newest_hash: %a tezos_head_level: %a \
       last_removed_commitment_hashes: %a allocated_storage: %a \
       occupied_storage: %a commitments_watermark: %a"
      Tez_repr.pp
      burn_per_byte
      inbox_ema
      pp_range
      finalized_commitments
      pp_range
      unfinalized_commitments
      pp_range
      uncommitted_inboxes
      (pp_print_option Tx_rollup_commitment_repr.Hash.pp)
      commitment_newest_hash
      (pp_print_option Raw_level_repr.pp)
      tezos_head_level
      (pp_print_option (fun fmt (m, c) ->
           fprintf
             fmt
             "(message result: %a, commitment: %a)"
             Tx_rollup_message_result_hash_repr.pp
             m
             Tx_rollup_commitment_repr.Hash.pp
             c))
      last_removed_commitment_hashes
      Z.pp_print
      allocated_storage
      Z.pp_print
      occupied_storage
      (pp_print_option Tx_rollup_level_repr.pp)
      commitments_watermark)

let adjust_storage_allocation : t -> delta:Z.t -> (t * Z.t) tzresult =
 fun state ~delta ->
  if Z.(equal zero delta) then ok (state, Z.zero)
  else
    let occupied_storage' = Z.add state.occupied_storage delta in
    if Compare.Z.(occupied_storage' < Z.zero) then
      (* returns [Internal_error] if [delta < 0] and [| delta | > state.occupied_storage].
         This error should never happen. *)
      error
      @@ Internal_error
           "Storage size should be positive after occupied space is freed."
    else
      let diff = Z.sub occupied_storage' state.allocated_storage in
      if Compare.Z.(diff > Z.zero) then
        let state =
          {
            state with
            occupied_storage = occupied_storage';
            allocated_storage = occupied_storage';
          }
        in
        ok (state, diff)
      else
        let state = {state with occupied_storage = occupied_storage'} in
        ok (state, Z.zero)

let update_burn_per_byte_helper :
    t -> factor:int -> final_size:int -> hard_limit:int -> t =
 fun ({burn_per_byte; inbox_ema; _} as state) ~factor ~final_size ~hard_limit ->
  let threshold_increase = 90 in
  let threshold_decrease = 80 in
  let variation_factor = 5L in
  let smoothing = 2 in
  (* The formula of the multiplier of EMA :

       smoothing / (1 + N)

     Suppose the period we want to observe is given by the
     [factor]. The common choice of smoothing is 2.
  *)
  let inbox_ema =
    inbox_ema + ((final_size - inbox_ema) * smoothing / (1 + factor))
  in
  let percentage = inbox_ema * 100 / hard_limit in
  let computation =
    let open Compare.Int in
    if threshold_decrease < percentage && percentage <= threshold_increase then
      (* constant case *)
      ok burn_per_byte
    else
      Tez_repr.(burn_per_byte *? variation_factor >>? fun x -> x /? 100L)
      >>? fun variation ->
      let variation =
        if Tez_repr.(variation = zero) then Tez_repr.one_mutez else variation
      in
      (* increase case *)
      if threshold_increase < percentage then
        Tez_repr.(burn_per_byte +? variation)
      else if percentage < threshold_decrease && Tez_repr.(zero < burn_per_byte)
      then
        (* decrease case, and strictly positive burn *)
        Tez_repr.(burn_per_byte -? variation)
      else (* decrease case, and burn equals zero *)
        ok burn_per_byte
  in
  match computation with
  | Ok burn_per_byte -> {state with burn_per_byte; inbox_ema}
  (* In the (very unlikely) event of an overflow, we force the burn to
     be the maximum amount. *)
  | Error _ -> {state with burn_per_byte = Tez_repr.max_mutez; inbox_ema}

let rec update_burn_per_byte :
    t -> elapsed:int -> factor:int -> final_size:int -> hard_limit:int -> t =
 fun state ~elapsed ~factor ~final_size ~hard_limit ->
  (* factor is expected to be a low number ~ 100 *)
  if Compare.Int.(elapsed > factor) then
    (* We do not need to compute precisely the new state. *)
    {state with burn_per_byte = Tez_repr.zero; inbox_ema = 0}
  else if Compare.Int.(elapsed <= 0) then
    (* Base case, we take into a account the [final_size] once. *)
    update_burn_per_byte_helper state ~factor ~final_size ~hard_limit
  else
    (* For all the blocks that do not contain inboxes, we act as if
       the inbox size was [0]. *)
    let state' =
      update_burn_per_byte_helper state ~factor ~final_size:0 ~hard_limit
    in
    let elapsed = elapsed - 1 in
    update_burn_per_byte state' ~elapsed ~factor ~final_size ~hard_limit

let has_valid_commitment_at {finalized_commitments; unfinalized_commitments; _}
    level =
  belongs_to finalized_commitments level
  || belongs_to unfinalized_commitments level

let inboxes_count {unfinalized_commitments; uncommitted_inboxes; _} =
  range_count unfinalized_commitments + range_count uncommitted_inboxes

let uncommitted_inboxes_count {uncommitted_inboxes; _} =
  range_count uncommitted_inboxes

let commitments_count {finalized_commitments; unfinalized_commitments; _} =
  range_count unfinalized_commitments + range_count finalized_commitments

let record_inbox_creation t level =
  (match t.tezos_head_level with
  | Some tezos_lvl ->
      error_when
        Raw_level_repr.(level <= tezos_lvl)
        (Internal_error "Trying to create an inbox in the past")
  | None -> ok ())
  >>? fun () ->
  let uncommitted_inboxes, new_level = extend t.uncommitted_inboxes in
  adjust_storage_allocation t ~delta:Tx_rollup_inbox_repr.size
  >>? fun (t, diff) ->
  ok
    ( {t with tezos_head_level = Some level; uncommitted_inboxes},
      new_level,
      diff )

let next_commitment_predecessor state = state.commitment_newest_hash

let finalized_commitment_oldest_level state =
  range_oldest state.finalized_commitments

let next_commitment_level state current_level =
  match
    ( range_oldest state.uncommitted_inboxes,
      range_newest state.uncommitted_inboxes )
  with
  | Some oldest_level, Some newest_level -> (
      if
        (* We want to return an error if there is only one inbox in the
           storage, and this inbox has been created in the current
           block. *)
        Tx_rollup_level_repr.(oldest_level < newest_level)
      then
        (* If [oldest_level < newest_level], we know we are not in
           this setup, and we can safely return [oldest_level]. *)
        ok oldest_level
      else
        (* Otherwise, we know that [oldest_level = newest_level], and we
           need to check at which Tezos level is has been created. *)
        match state.tezos_head_level with
        | Some newest_inbox_creation ->
            error_when
              Raw_level_repr.(current_level <= newest_inbox_creation)
              No_uncommitted_inbox
            >>? fun () -> ok oldest_level
        | None -> error (Internal_error "tezos_head_level was not properly set")
      )
  | None, None -> error No_uncommitted_inbox
  | Some _, None | None, Some _ ->
      error (Internal_error "rollup state is inconsistent")

let next_commitment_to_finalize state =
  range_oldest state.unfinalized_commitments

let next_commitment_to_remove state = range_oldest state.finalized_commitments

let record_inbox_deletion state candidate =
  match range_oldest state.unfinalized_commitments with
  | Some level when Tx_rollup_level_repr.(candidate = level) ->
      shrink state.unfinalized_commitments >>? fun unfinalized_commitments ->
      let finalized_commitments, _ = extend state.finalized_commitments in
      ok {state with unfinalized_commitments; finalized_commitments}
  | _ -> error (Internal_error "Trying to delete the wrong inbox")

let record_commitment_creation state level hash =
  match range_oldest state.uncommitted_inboxes with
  | Some oldest ->
      error_unless
        Tx_rollup_level_repr.(level = oldest)
        (Internal_error "Trying to create the wrong commitment")
      >>? fun () ->
      shrink state.uncommitted_inboxes >>? fun uncommitted_inboxes ->
      let unfinalized_commitments, _ = extend state.unfinalized_commitments in
      let state =
        {
          state with
          uncommitted_inboxes;
          unfinalized_commitments;
          commitment_newest_hash = Some hash;
        }
      in
      if is_above_watermark state.commitments_watermark level then
        (* See {{Note inbox}} in [Tx_rollup_commitment_storage] for
           why it is safe to “free” the inbox storage when it is
           committed too. *)
        adjust_storage_allocation state ~delta:(Z.neg Tx_rollup_inbox_repr.size)
        >>? fun (state, _) ->
        ok {state with commitments_watermark = make_watermark level}
      else ok state
  | None ->
      error (Internal_error "Cannot create a commitment due to lack of inbox")

let record_commitment_rejection state level predecessor_hash =
  let unwrap_option msg = function
    | Some x -> ok x
    | _ -> error (Internal_error msg)
  in
  let check_none msg = function
    | None -> ok ()
    | Some _ -> error (Internal_error msg)
  in
  left_extend state.uncommitted_inboxes level >>? fun uncommitted_inboxes ->
  let state = {state with uncommitted_inboxes} in
  right_cut state.unfinalized_commitments level
  >>? fun unfinalized_commitments ->
  match Tx_rollup_level_repr.pred level with
  | Some pred_level
    when belongs_to state.unfinalized_commitments pred_level
         || belongs_to state.finalized_commitments pred_level ->
      (* Case 1. Predecessor level of the rejected commitments has a commitment in the storage *)
      unwrap_option "Missing predecessor commitment" predecessor_hash
      >>? fun predecessor_hash ->
      ok
        {
          state with
          unfinalized_commitments;
          commitment_newest_hash = Some predecessor_hash;
        }
  | Some _ ->
      (* Case 2. Predecessor level of the rejected commitments has its
         commitment removed from the storage *)
      check_none "Unexpected predecessor hash" predecessor_hash >>? fun () ->
      unwrap_option
        "Missing commitment hash"
        state.last_removed_commitment_hashes
      >>? fun (_, pred_hash) ->
      ok
        {
          state with
          unfinalized_commitments;
          commitment_newest_hash = Some pred_hash;
        }
  | None ->
      (* Case 3. The rejected commitment is the commitment of the root level *)
      ok {state with unfinalized_commitments; commitment_newest_hash = None}

let record_commitment_deletion state level hash message_hash =
  match range_oldest state.finalized_commitments with
  | Some oldest when Tx_rollup_level_repr.(level = oldest) ->
      shrink state.finalized_commitments >>? fun finalized_commitments ->
      ok
        {
          state with
          finalized_commitments;
          last_removed_commitment_hashes = Some (message_hash, hash);
        }
  | _ -> error (Internal_error "Trying to remove an incorrect commitment")

let burn_cost ~limit state size =
  Tez_repr.(state.burn_per_byte *? Int64.of_int size) >>? fun burn ->
  match limit with
  | Some limit when Tez_repr.(limit >= burn) ->
      error (Submit_batch_burn_exceeded {burn; limit})
  | _ -> ok burn

let finalized_commitments_range state =
  match
    ( range_oldest state.finalized_commitments,
      range_newest state.finalized_commitments )
  with
  | Some oldest, Some newest -> Some (oldest, newest)
  | _ -> None

let check_level_can_be_rejected state level =
  match
    ( range_oldest state.unfinalized_commitments,
      range_newest state.unfinalized_commitments )
  with
  | Some oldest, Some newest ->
      error_unless Tx_rollup_level_repr.(oldest <= level && level <= newest)
      @@ Cannot_reject_level
           {provided = level; accepted_range = Some (oldest, newest)}
  | _ -> error @@ Cannot_reject_level {provided = level; accepted_range = None}

let last_removed_commitment_hashes state = state.last_removed_commitment_hashes

let head_levels state =
  match (state.uncommitted_inboxes, state.tezos_head_level) with
  | Empty {next = l}, Some tz_level ->
      Option.map (fun l -> (l, tz_level)) (Tx_rollup_level_repr.pred l)
  | Interval {newest; _}, Some tz_level -> Some (newest, tz_level)
  | _ -> None

module Internal_for_tests = struct
  let make :
      ?burn_per_byte:Tez_repr.t ->
      ?inbox_ema:int ->
      ?last_removed_commitment_hashes:
        Tx_rollup_message_result_hash_repr.t * Tx_rollup_commitment_repr.Hash.t ->
      ?finalized_commitments:Tx_rollup_level_repr.t * Tx_rollup_level_repr.t ->
      ?unfinalized_commitments:Tx_rollup_level_repr.t * Tx_rollup_level_repr.t ->
      ?uncommitted_inboxes:Tx_rollup_level_repr.t * Tx_rollup_level_repr.t ->
      ?commitment_newest_hash:Tx_rollup_commitment_repr.Hash.t ->
      ?tezos_head_level:Raw_level_repr.t ->
      ?occupied_storage:Z.t ->
      ?commitments_watermark:Tx_rollup_level_repr.t ->
      allocated_storage:Z.t ->
      unit ->
      t =
   fun ?(burn_per_byte = Tez_repr.zero)
       ?(inbox_ema = 0)
       ?last_removed_commitment_hashes
       ?finalized_commitments
       ?unfinalized_commitments
       ?uncommitted_inboxes
       ?commitment_newest_hash
       ?tezos_head_level
       ?(occupied_storage = Z.zero)
       ?commitments_watermark
       ~allocated_storage
       () ->
    let to_range = function
      | Some (oldest, newest) ->
          assert (Tx_rollup_level_repr.(oldest <= newest)) ;
          Interval {oldest; newest}
      | _ -> Empty {next = Tx_rollup_level_repr.root}
    in

    let unfinalized_commitments = to_range unfinalized_commitments in
    let finalized_commitments = to_range finalized_commitments in
    let uncommitted_inboxes = to_range uncommitted_inboxes in

    {
      last_removed_commitment_hashes;
      burn_per_byte;
      occupied_storage;
      allocated_storage;
      inbox_ema;
      finalized_commitments;
      unfinalized_commitments;
      uncommitted_inboxes;
      commitment_newest_hash;
      tezos_head_level;
      commitments_watermark;
    }

  let get_inbox_ema : t -> int = fun {inbox_ema; _} -> inbox_ema

  let get_occupied_storage : t -> Z.t =
   fun {occupied_storage; _} -> occupied_storage

  let set_occupied_storage : Z.t -> t -> t =
   fun occupied_storage st -> {st with occupied_storage}

  let get_allocated_storage : t -> Z.t =
   fun {allocated_storage; _} -> allocated_storage

  let set_allocated_storage : Z.t -> t -> t =
   fun allocated_storage st -> {st with allocated_storage}

  let reset_commitments_watermark : t -> t =
   fun st -> {st with commitments_watermark = None}

  let get_commitments_watermark : t -> Tx_rollup_level_repr.t option =
   fun st -> st.commitments_watermark
end
back to top