Raw File
interpreter.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 TriliTech <contact@trili.tech>                         *)
(*                                                                           *)
(* 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

let get_boot_sector (module Plugin : Protocol_plugin_sig.PARTIAL) block_hash
    (node_ctxt : _ Node_context.t) =
  let open Lwt_result_syntax in
  match node_ctxt.boot_sector_file with
  | None -> Plugin.Layer1_helpers.get_boot_sector block_hash node_ctxt
  | Some boot_sector_file ->
      let*! boot_sector = Lwt_utils_unix.read_file boot_sector_file in
      let*? boot_sector =
        Option.value_e
          ~error:
            [
              Rollup_node_errors.Unparsable_boot_sector {path = boot_sector_file};
            ]
          (Plugin.Pvm.parse_boot_sector node_ctxt.kind boot_sector)
      in
      return boot_sector

let genesis_state (module Plugin : Protocol_plugin_sig.PARTIAL) block_hash
    node_ctxt ctxt =
  let open Lwt_result_syntax in
  let* boot_sector = get_boot_sector (module Plugin) block_hash node_ctxt in
  let*! initial_state = Plugin.Pvm.initial_state node_ctxt.kind in
  let*! genesis_state =
    Plugin.Pvm.install_boot_sector node_ctxt.kind initial_state boot_sector
  in
  let*! ctxt = Context.PVMState.set ctxt genesis_state in
  return (ctxt, genesis_state)

let state_of_head plugin node_ctxt ctxt Layer1.{hash; level} =
  let open Lwt_result_syntax in
  let*! state = Context.PVMState.find ctxt in
  match state with
  | None ->
      let genesis_level = node_ctxt.Node_context.genesis_info.level in
      if level = genesis_level then genesis_state plugin hash node_ctxt ctxt
      else tzfail (Rollup_node_errors.Missing_PVM_state (hash, level))
  | Some state -> return (ctxt, state)

(** [transition_pvm plugin node_ctxt ctxt predecessor head] runs a PVM at the
    previous state from block [predecessor] by consuming as many messages as
    possible from block [head]. *)
let transition_pvm (module Plugin : Protocol_plugin_sig.PARTIAL) node_ctxt ctxt
    predecessor Layer1.{hash = _; _} inbox_messages =
  let open Lwt_result_syntax in
  (* Retrieve the previous PVM state from store. *)
  let* ctxt, predecessor_state =
    state_of_head (module Plugin) node_ctxt ctxt predecessor
  in
  let* eval_result =
    Plugin.Pvm.Fueled.Free.eval_block_inbox
      ~fuel:(Fuel.Free.of_ticks 0L)
      node_ctxt
      inbox_messages
      predecessor_state
  in
  let* {
         state = {state; state_hash; inbox_level; tick; _};
         num_messages;
         num_ticks;
       } =
    Delayed_write_monad.apply node_ctxt eval_result
  in
  let*! ctxt = Context.PVMState.set ctxt state in
  let*! initial_tick = Plugin.Pvm.get_tick node_ctxt.kind predecessor_state in
  (* Produce events. *)
  let*! () =
    Interpreter_event.transitioned_pvm inbox_level state_hash tick num_messages
  in
  return (ctxt, num_messages, Z.to_int64 num_ticks, initial_tick)

(** [process_head plugin node_ctxt ctxt ~predecessor head inbox_and_messages] runs the PVM for the given
    head. *)
let process_head plugin (node_ctxt : _ Node_context.t) ctxt
    ~(predecessor : Layer1.header) (head : Layer1.header) inbox_and_messages =
  let open Lwt_result_syntax in
  let first_inbox_level = node_ctxt.genesis_info.level |> Int32.succ in
  if head.Layer1.level >= first_inbox_level then
    transition_pvm
      plugin
      node_ctxt
      ctxt
      (Layer1.head_of_header predecessor)
      (Layer1.head_of_header head)
      inbox_and_messages
  else if head.Layer1.level = node_ctxt.genesis_info.level then
    let* ctxt, state = genesis_state plugin head.hash node_ctxt ctxt in
    let*! ctxt = Context.PVMState.set ctxt state in
    return (ctxt, 0, 0L, Z.zero)
  else return (ctxt, 0, 0L, Z.zero)

(** Returns the starting evaluation before the evaluation of the block. It
    contains the PVM state at the end of the execution of the previous block and
    the messages the block ([remaining_messages]). *)
let start_state_of_block plugin node_ctxt (block : Sc_rollup_block.t) =
  let open Lwt_result_syntax in
  let pred_level = Int32.pred block.header.level in
  let* ctxt =
    Node_context.checkout_context node_ctxt block.header.predecessor
  in
  let* _ctxt, state =
    state_of_head
      plugin
      node_ctxt
      ctxt
      Layer1.{hash = block.header.predecessor; level = pred_level}
  in
  let* inbox = Node_context.get_inbox node_ctxt block.header.inbox_hash in
  let* {is_first_block; predecessor; predecessor_timestamp; messages} =
    Node_context.get_messages node_ctxt block.header.inbox_witness
  in
  let inbox_level = Octez_smart_rollup.Inbox.inbox_level inbox in
  let module Plugin = (val plugin) in
  let*! tick = Plugin.Pvm.get_tick node_ctxt.kind state in
  let*! state_hash = Plugin.Pvm.state_hash node_ctxt.kind state in
  let messages =
    Plugin.Pvm.start_of_level_serialized
    ::
    (if is_first_block then
     Option.to_list Plugin.Pvm.protocol_migration_serialized
    else [])
    @ Plugin.Pvm.info_per_level_serialized ~predecessor ~predecessor_timestamp
      :: messages
    @ [Plugin.Pvm.end_of_level_serialized]
  in
  return
    Pvm_plugin_sig.
      {
        state;
        state_hash;
        inbox_level;
        tick;
        message_counter_offset = 0;
        remaining_fuel = Fuel.Accounted.of_ticks 0L;
        remaining_messages = messages;
      }

(** [run_for_ticks plugin node_ctxt start_state tick_distance] starts the
    evaluation of messages in the [start_state] for at most [tick_distance]. *)
let run_to_tick (module Plugin : Protocol_plugin_sig.PARTIAL) node_ctxt
    start_state tick =
  let open Delayed_write_monad.Lwt_result_syntax in
  let tick_distance =
    Z.sub tick start_state.Pvm_plugin_sig.tick |> Z.to_int64
  in
  let>+ eval_result =
    Plugin.Pvm.Fueled.Accounted.eval_messages
      node_ctxt
      {start_state with remaining_fuel = Fuel.Accounted.of_ticks tick_distance}
  in
  eval_result.state

let state_of_tick_aux plugin node_ctxt ~start_state (event : Sc_rollup_block.t)
    tick =
  let open Lwt_result_syntax in
  let* start_state =
    match start_state with
    | Some start_state
      when start_state.Pvm_plugin_sig.inbox_level = event.header.level ->
        return start_state
    | _ ->
        (* Recompute start state on level change or if we don't have a
           starting state on hand. *)
        start_state_of_block plugin node_ctxt event
  in
  (* TODO: #3384
     We should test that we always have enough blocks to find the tick
     because [state_of_tick] is a critical function. *)
  let* result_state = run_to_tick plugin node_ctxt start_state tick in
  let result_state = Delayed_write_monad.ignore result_state in
  return result_state

(* The cache allows cache intermediate states of the PVM in e.g. dissections. *)
module Tick_state_cache =
  Aches_lwt.Lache.Make
    (Aches.Rache.Transfer
       (Aches.Rache.LRU)
       (struct
         type t = Z.t * Block_hash.t

         let equal (t1, b1) (t2, b2) = Z.equal t1 t2 && Block_hash.(b1 = b2)

         let hash (tick, block) = (Z.hash tick * 13) + Block_hash.hash block
       end))

let tick_state_cache = Tick_state_cache.create 64 (* size of 2 dissections *)

(* Memoized version of [state_of_tick_aux]. *)
let memo_state_of_tick_aux plugin node_ctxt ~start_state
    (event : Sc_rollup_block.t) tick =
  Tick_state_cache.bind_or_put
    tick_state_cache
    (tick, event.header.block_hash)
    (fun (tick, _hash) ->
      state_of_tick_aux plugin node_ctxt ~start_state event tick)
    Lwt.return

(** [state_of_tick plugin node_ctxt ?start_state ~tick level] returns [Some
    end_state] for [tick] if [tick] happened before
    [level]. Otherwise, returns [None].*)
let state_of_tick plugin node_ctxt ?start_state ~tick level =
  let open Lwt_result_syntax in
  let* event = Node_context.block_with_tick node_ctxt ~max_level:level tick in
  match event with
  | None -> return_none
  | Some event ->
      assert (event.header.level <= level) ;
      let* result_state =
        if Node_context.is_loser node_ctxt then
          (* TODO: https://gitlab.com/tezos/tezos/-/issues/5253
             The failures/loser mode does not work properly when restarting
             from intermediate states. *)
          state_of_tick_aux plugin node_ctxt ~start_state:None event tick
        else memo_state_of_tick_aux plugin node_ctxt ~start_state event tick
      in
      return_some result_state
back to top