Revision ae79a999e445690595d03fcdce3f4178aee8dbe9 authored by Marge Bot on 13 September 2022, 08:50:37 UTC, committed by Marge Bot on 13 September 2022, 08:50:37 UTC
Co-authored-by: Valentin Chaboche <valentin.chaboche@nomadic-labs.com>

Approved-by: François Thiré <francois.thire@nomadic-labs.com>
Approved-by: Yann Regis-Gianas <946787-yrg@users.noreply.gitlab.com>

See merge request https://gitlab.com/tezos/tezos/-/merge_requests/6305
2 parent s 4b11277 + 7d9bb57
Raw File
sc_rollup.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2021-2022 Nomadic Labs <contact@nomadic-labs.com>           *)
(* 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(* Testing
   -------
   Component:    Smart Contract Optimistic Rollups
   Invocation:   dune exec tezt/tests/main.exe -- --file sc_rollup.ml
*)

open Base

let hooks = Tezos_regression.hooks

(*

   Helpers
   =======

*)

let hex_encode (input : string) : string =
  match Hex.of_string input with `Hex s -> s

(* [read_kernel filename] reads binary encoded WebAssembly module (e.g. `foo.wasm`)
   and returns a hex-encoded Wasm PVM boot sector, suitable for passing to
   [originate_sc_rollup] or [with_fresh_rollup].

   See also [wasm_incomplete_kernel_boot_sector].

   Note that this uses [Tezos_scoru_wasm.Gather_floppies.Complete_kernel], so
   the kernel must fit into a single Tezos operation.
*)
let read_kernel name : string =
  let module G = Tezos_scoru_wasm.Gather_floppies in
  let open Tezt.Base in
  let kernel_file =
    project_root // Filename.dirname __FILE__
    // "../../src/proto_alpha/lib_protocol/test/integration/wasm_kernel"
    // (name ^ ".wasm")
  in
  hex_encode
  @@ Data_encoding.Binary.to_string_exn G.origination_message_encoding
  @@ G.Complete_kernel (Bytes.of_string @@ read_file kernel_file)

(* Number of levels needed to process a head as finalized. This value should
   be the same as `node_context.block_finality_time`, where `node_context` is
   the `Node_context.t` used by the rollup node. For Tenderbake, the
   block finality time is 2. *)
let block_finality_time = 2

type sc_rollup_constants = {
  origination_size : int;
  challenge_window_in_blocks : int;
  max_number_of_messages_per_commitment_period : int;
  stake_amount : Tez.t;
  commitment_period_in_blocks : int;
  max_lookahead_in_blocks : int32;
  max_active_outbox_levels : int32;
  max_outbox_messages_per_level : int;
  number_of_sections_in_dissection : int;
  timeout_period_in_blocks : int;
}

(** [boot_sector_of k] returns a valid boot sector for a PVM of
    kind [kind]. *)
let boot_sector_of = function
  | "arith" -> ""
  | "wasm_2_0_0" -> Constant.wasm_incomplete_kernel_boot_sector
  | kind -> raise (Invalid_argument kind)

let get_sc_rollup_constants client =
  let* json =
    RPC.Client.call client @@ RPC.get_chain_block_context_constants ()
  in
  let open JSON in
  let origination_size = json |-> "sc_rollup_origination_size" |> as_int in
  let challenge_window_in_blocks =
    json |-> "sc_rollup_challenge_window_in_blocks" |> as_int
  in
  let max_number_of_messages_per_commitment_period =
    json |-> "sc_rollup_max_number_of_messages_per_commitment_period" |> as_int
  in
  let stake_amount =
    json |-> "sc_rollup_stake_amount" |> as_string |> Int64.of_string
    |> Tez.of_mutez_int64
  in
  let commitment_period_in_blocks =
    json |-> "sc_rollup_commitment_period_in_blocks" |> as_int
  in
  let max_lookahead_in_blocks =
    json |-> "sc_rollup_max_lookahead_in_blocks" |> as_int32
  in
  let max_active_outbox_levels =
    json |-> "sc_rollup_max_active_outbox_levels" |> as_int32
  in
  let max_outbox_messages_per_level =
    json |-> "sc_rollup_max_outbox_messages_per_level" |> as_int
  in
  let number_of_sections_in_dissection =
    json |-> "sc_rollup_number_of_sections_in_dissection" |> as_int
  in
  let timeout_period_in_blocks =
    json |-> "sc_rollup_timeout_period_in_blocks" |> as_int
  in
  return
    {
      origination_size;
      challenge_window_in_blocks;
      max_number_of_messages_per_commitment_period;
      stake_amount;
      commitment_period_in_blocks;
      max_lookahead_in_blocks;
      max_active_outbox_levels;
      max_outbox_messages_per_level;
      number_of_sections_in_dissection;
      timeout_period_in_blocks;
    }

(* List of scoru errors messages used in tests below. *)

let commit_too_recent =
  "Attempted to cement a commitment before its refutation deadline"

let parent_not_lcc = "Parent is not the last cemented commitment"

let disputed_commit = "Attempted to cement a disputed commitment"

let commit_doesnt_exit = "Commitment scc\\w+\\sdoes not exist"

let make_parameter name value =
  Option.map (fun v -> ([name], Option.some @@ Int.to_string v)) value
  |> Option.to_list

let test ~__FILE__ ?(tags = []) title f =
  let tags = "sc_rollup" :: tags in
  Protocol.register_test ~__FILE__ ~title ~tags f

let regression_test ~__FILE__ ?(tags = []) title f =
  let tags = "sc_rollup" :: tags in
  Protocol.register_regression_test ~__FILE__ ~title ~tags f

let setup ?commitment_period ?challenge_window ?timeout f ~protocol =
  let parameters =
    make_parameter "sc_rollup_commitment_period_in_blocks" commitment_period
    @ make_parameter "sc_rollup_challenge_window_in_blocks" challenge_window
    @ make_parameter "sc_rollup_timeout_period_in_blocks" timeout
    @ [(["sc_rollup_enable"], Some "true")]
  in
  let base = Either.right (protocol, None) in
  let* parameter_file = Protocol.write_parameter_file ~base parameters in
  let nodes_args =
    Node.[Synchronisation_threshold 0; History_mode Archive; No_bootstrap_peers]
  in
  let* node, client =
    Client.init_with_protocol ~parameter_file `Client ~protocol ~nodes_args ()
  in
  let operator = Constant.bootstrap1.alias in
  f node client operator

let get_sc_rollup_commitment_period_in_blocks client =
  let* constants = get_sc_rollup_constants client in
  return constants.commitment_period_in_blocks

let sc_rollup_node_rpc sc_node service =
  let* curl = RPC.Curl.get () in
  match curl with
  | None -> return None
  | Some curl ->
      let url =
        Printf.sprintf "%s/%s" (Sc_rollup_node.endpoint sc_node) service
      in
      let* response = curl ~url in
      return (Some response)

type test = {variant : string; tags : string list; description : string}

(** This helper injects an SC rollup origination via tezos-client. Then it
    bakes to include the origination in a block. It returns the address of the
    originated rollup *)
let originate_sc_rollup ?(hooks = hooks) ?(burn_cap = Tez.(of_int 9999999))
    ?(src = "bootstrap1") ?(kind = "arith") ?(parameters_ty = "string")
    ?(boot_sector = boot_sector_of kind) client =
  let* sc_rollup =
    Client.Sc_rollup.(
      originate ~hooks ~burn_cap ~src ~kind ~parameters_ty ~boot_sector client)
  in
  let* () = Client.bake_for_and_wait client in
  return sc_rollup

(* Configuration of a rollup node
   ------------------------------

   A rollup node has a configuration file that must be initialized.
*)
let with_fresh_rollup ?kind ?boot_sector f tezos_node tezos_client operator =
  let* sc_rollup =
    originate_sc_rollup ?kind ?boot_sector ~src:operator tezos_client
  in
  let sc_rollup_node =
    Sc_rollup_node.create
      Operator
      tezos_node
      tezos_client
      ~default_operator:operator
  in
  let* configuration_filename =
    Sc_rollup_node.config_init sc_rollup_node sc_rollup
  in
  f sc_rollup sc_rollup_node configuration_filename

(* TODO: https://gitlab.com/tezos/tezos/-/issues/2933
   Many tests can be refactored using test_scenario. *)
let test_scenario ~kind ?boot_sector ?commitment_period ?challenge_window
    ?timeout {variant; tags; description} scenario =
  let tags = tags @ [kind; variant] in
  regression_test
    ~__FILE__
    ~tags
    (Printf.sprintf "%s - %s (%s)" kind description variant)
    (fun protocol ->
      setup ?commitment_period ?challenge_window ~protocol ?timeout
      @@ fun node client ->
      ( with_fresh_rollup ~kind ?boot_sector
      @@ fun sc_rollup sc_rollup_node _filename ->
        scenario protocol sc_rollup_node sc_rollup node client )
        node
        client)

let inbox_level (_hash, (commitment : Sc_rollup_client.commitment), _level) =
  commitment.inbox_level

let number_of_ticks (_hash, (commitment : Sc_rollup_client.commitment), _level)
    =
  commitment.number_of_ticks

let last_cemented_commitment_hash_with_level ~sc_rollup client =
  let* json =
    RPC.Client.call client
    @@ RPC
       .get_chain_block_context_sc_rollup_last_cemented_commitment_hash_with_level
         sc_rollup
  in
  let hash = JSON.(json |-> "hash" |> as_string) in
  let level = JSON.(json |-> "level" |> as_int) in
  return (hash, level)

let get_staked_on_commitment ~sc_rollup ~staker client =
  let* json =
    RPC.Client.call client
    @@ RPC.get_chain_block_context_sc_rollup_staker_staked_on_commitment
         ~sc_rollup
         staker
  in
  let hash = JSON.(json |-> "hash" |> as_string) in
  return hash

let hash (hash, (_ : Sc_rollup_client.commitment), _level) = hash

let first_published_at_level (_hash, (_ : Sc_rollup_client.commitment), level) =
  level

let predecessor (_hash, {Sc_rollup_client.predecessor; _}, _level) = predecessor

let cement_commitment ?(src = "bootstrap1") ?fail ~sc_rollup ~hash client =
  let p =
    Client.Sc_rollup.cement_commitment ~hooks ~dst:sc_rollup ~src ~hash client
  in
  match fail with
  | None ->
      let*! () = p in
      Client.bake_for_and_wait client
  | Some failure ->
      let*? process = p in
      Process.check_error ~msg:(rex failure) process

let publish_commitment ?(src = Constant.bootstrap1.public_key_hash) ~commitment
    client sc_rollup =
  let ({compressed_state; inbox_level; predecessor; number_of_ticks}
        : Sc_rollup_client.commitment) =
    commitment
  in
  Client.Sc_rollup.publish_commitment
    ~hooks
    ~src
    ~sc_rollup
    ~compressed_state
    ~inbox_level
    ~predecessor
    ~number_of_ticks
    client

(*

   Tests
   =====

*)

(* Originate a new SCORU
   ---------------------

   - Rollup addresses are fully determined by operation hashes and origination nonce.
*)
let test_origination ~kind =
  regression_test
    ~tags:["sc_rollup"; kind]
    ~__FILE__
    (Format.asprintf "%s - origination of a SCORU executes without error" kind)
    (fun protocol ->
      setup ~protocol @@ fun _node client bootstrap1_key ->
      let* _sc_rollup = originate_sc_rollup ~kind ~src:bootstrap1_key client in
      unit)

(* Initialize configuration
   ------------------------

   Can use CLI to initialize the rollup node config file
*)
let test_rollup_node_configuration ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"]
    "configuration of a smart contract optimistic rollup node"
    (fun protocol ->
      setup ~protocol @@ with_fresh_rollup ~kind
      @@ fun _sc_rollup _sc_rollup_node filename ->
      let read_configuration =
        let open Ezjsonm in
        match from_channel (open_in filename) with
        | `O fields ->
            (* Remove "data-dir" and "rpc-port" as they are non deterministic. *)
            `O
              (List.filter
                 (fun (s, _) ->
                   match s with "data-dir" | "rpc-port" -> false | _ -> true)
                 fields)
            |> to_string
        | _ ->
            failwith "The configuration file does not have the expected format."
      in
      Log.info "Read configuration:\n %s" read_configuration ;
      return ())

(* Launching a rollup node
   -----------------------

   A running rollup node can be asked the address of the rollup it is
   interacting with.
*)
let test_rollup_node_running ~kind =
  test
    ~__FILE__
    ~tags:["sc_rollup"; "run"; kind]
    (Format.asprintf "%s - running a smart contract rollup node" kind)
    (fun protocol ->
      setup ~protocol @@ with_fresh_rollup ~kind
      @@ fun sc_rollup sc_rollup_node _filename ->
      let* () = Sc_rollup_node.run sc_rollup_node in
      let* sc_rollup_from_rpc =
        sc_rollup_node_rpc sc_rollup_node "global/sc_rollup_address"
      in
      match sc_rollup_from_rpc with
      | None ->
          (* No curl, no check. *)
          failwith "Please install curl"
      | Some sc_rollup_from_rpc ->
          let sc_rollup_from_rpc = JSON.as_string sc_rollup_from_rpc in
          if sc_rollup_from_rpc <> sc_rollup then
            failwith
              (Printf.sprintf
                 "Expecting %s, got %s when we query the sc rollup node RPC \
                  address"
                 sc_rollup
                 sc_rollup_from_rpc)
          else return ())

(* Interacting with a rollup node through a rollup client
   ------------------------------------------------------

   When a rollup node is running, a rollup client can ask this
   node its rollup address.
*)
let test_rollup_client_gets_address ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "run"; "client"]
    "getting a smart-contract rollup address through the client"
    (fun protocol ->
      setup ~protocol @@ with_fresh_rollup ~kind
      @@ fun sc_rollup sc_rollup_node _filename ->
      let* () = Sc_rollup_node.run sc_rollup_node in
      let sc_client = Sc_rollup_client.create sc_rollup_node in
      let* sc_rollup_from_client =
        Sc_rollup_client.sc_rollup_address sc_client
      in
      if sc_rollup_from_client <> sc_rollup then
        failwith
          (Printf.sprintf
             "Expecting %s, got %s when the client asks for the sc rollup \
              address"
             sc_rollup
             sc_rollup_from_client) ;
      return ())

(* Fetching the initial level of a sc rollup
    -----------------------------------------

   We can fetch the level when a smart contract rollup was
   originated from the context.
*)
let test_rollup_get_genesis_info ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "genesis_info"; kind]
    (Format.asprintf "%s - get genesis info of a sc rollup" kind)
    (fun protocol ->
      setup ~protocol @@ fun node client bootstrap ->
      let* current_level =
        RPC.Client.call client @@ RPC.get_chain_block_helper_current_level ()
      in
      ( with_fresh_rollup ~kind @@ fun sc_rollup _sc_rollup_node _filename ->
        (* Bake 10 blocks to be sure that the initial level of rollup is different
           from the current level. *)
        let* _ = repeat 10 (fun () -> Client.bake_for_and_wait client) in
        let* genesis_info =
          RPC.Client.call client
          @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
        in
        (* 1 Block for activating alpha + 1 block for originating the rollup
           the rollup initial level should be 2 *)
        Check.(
          (JSON.(genesis_info |-> "level" |> as_int)
          = JSON.as_int (JSON.get "level" current_level) + 1)
            int
            ~error_msg:"expected value %L, got %R") ;
        return () )
        node
        client
        bootstrap)

(* Fetching the last cemented commitment info for a sc rollup
    ----------------------------------------------------------

   We can fetch the hash and level of the last cemented commitment. Initially,
   this corresponds to `(Sc_rollup.Commitment_hash.zero, origination_level)`.
*)

(* TODO: https://gitlab.com/tezos/tezos/-/issues/2944
   Revisit this test once the rollup node can cement commitments. *)
let test_rollup_get_chain_block_context_sc_rollup_last_cemented_commitment_hash_with_level
    ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "lcc_hash_with_level"; kind]
    (Format.asprintf
       "%s - get last cemented commitment hash and inbox level of a sc rollup"
       kind)
    (fun protocol ->
      setup ~protocol @@ fun node client bootstrap ->
      ( with_fresh_rollup ~kind @@ fun sc_rollup _sc_rollup_node _filename ->
        let* origination_level =
          RPC.Client.call client @@ RPC.get_chain_block_helper_current_level ()
        in

        (* Bake 10 blocks to be sure that the origination_level of rollup is different
           from the level of the head node. *)
        let* () = repeat 10 (fun () -> Client.bake_for_and_wait client) in
        let* hash, level =
          last_cemented_commitment_hash_with_level ~sc_rollup client
        in
        let* genesis_info =
          RPC.Client.call client
          @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
        in
        let genesis_hash =
          JSON.(genesis_info |-> "commitment_hash" |> as_string)
        in
        Check.(
          (hash = genesis_hash) string ~error_msg:"expected value %L, got %R") ;
        (* The level of the last cemented commitment should correspond to the
           rollup origination level. *)
        Check.(
          (level = JSON.(origination_level |-> "level" |> as_int))
            int
            ~error_msg:"expected value %L, got %R") ;
        return () )
        node
        client
        bootstrap)

(* Pushing message in the inbox
   ----------------------------

   A message can be pushed to a smart-contract rollup inbox through
   the Tezos node. Then we can observe that the messages are included in the
   inbox.
*)
let send_message client sc_rollup msg =
  let* () =
    Client.Sc_rollup.send_message
      ~hooks
      ~src:Constant.bootstrap2.alias
      ~dst:sc_rollup
      ~msg
      client
  in
  Client.bake_for_and_wait client

let send_messages ?batch_size n sc_rollup client =
  let messages =
    List.map
      (fun i ->
        let batch_size = match batch_size with None -> i | Some v -> v in
        let json =
          `A (List.map (fun _ -> `String "CAFEBABE") (range 1 batch_size))
        in
        "text:" ^ Ezjsonm.to_string json)
      (range 1 n)
  in
  Lwt_list.iter_s (fun msg -> send_message client sc_rollup msg) messages

let to_text_messages_arg msgs =
  let json = Ezjsonm.list Ezjsonm.string msgs in
  "text:" ^ Ezjsonm.to_string ~minify:true json

let send_text_messages client sc_rollup msgs =
  send_message client sc_rollup (to_text_messages_arg msgs)

let parse_inbox json =
  let go () =
    let inbox = JSON.as_object json in
    return
      ( List.assoc "current_level_hash" inbox |> JSON.as_string,
        List.assoc "nb_messages_in_commitment_period" inbox |> JSON.as_int )
  in
  Lwt.catch go @@ fun exn ->
  failwith
    (Printf.sprintf
       "Unable to parse inbox %s\n%s"
       (JSON.encode json)
       (Printexc.to_string exn))

let get_inbox_from_tezos_node sc_rollup client =
  let* inbox =
    RPC.Client.call client
    @@ RPC.get_chain_block_context_sc_rollup_inbox sc_rollup
  in
  parse_inbox inbox

let get_inbox_from_sc_rollup_node sc_rollup_node =
  let* inbox = sc_rollup_node_rpc sc_rollup_node "global/inbox" in
  match inbox with
  | None -> failwith "Unable to retrieve inbox from sc rollup node"
  | Some inbox -> parse_inbox inbox

let test_rollup_inbox_size ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "inbox"; kind]
    (Format.asprintf
       "%s - pushing messages in the inbox - check inbox size"
       kind)
    (fun protocol ->
      setup ~protocol @@ fun node client ->
      ( with_fresh_rollup ~kind @@ fun sc_rollup _sc_rollup_node _filename ->
        let n = 10 in
        let* () = send_messages n sc_rollup client in
        let* _, inbox_msg_during_commitment_period =
          get_inbox_from_tezos_node sc_rollup client
        in
        return
        @@ Check.(
             (inbox_msg_during_commitment_period = n * (n + 1) / 2)
               int
               ~error_msg:"expected value %R, got %L") )
        node
        client)

module Sc_rollup_inbox = struct
  open Tezos_context_encoding.Context

  module Store = struct
    module Maker = Irmin_pack_mem.Maker (Conf)
    include Maker.Make (Schema)
    module Schema = Tezos_context_encoding.Context.Schema
  end

  include Tezos_context_helpers.Context.Make_tree (Conf) (Store)

  (* An external message is prefixed with a tag whose length is one byte, and
     whose value is 1. *)
  let encode_external_message message =
    let prefix = "\001" in
    Bytes.of_string (prefix ^ message)

  (*
      The hash for empty messages is the hash of empty bytes, and not of an empty
      tree.

      The hash for non-empty messages is the hash of the tree, where each message
      payload sits at the key [[message_index, "payload"]], where [message_index]
      is the index of the current message relative to the first message.

      The [message_counter] is reset to zero when the inbox level increments (and
      therefore [current_messages] are zero-indexed in the tree).
  *)
  let rec build_current_messages_tree counter tree messages =
    match messages with
    | [] -> return tree
    | message :: rest ->
        let key = Data_encoding.Binary.to_string_exn Data_encoding.z counter in
        let payload = encode_external_message message in
        let* tree = add tree ["message"; key] payload in
        build_current_messages_tree (Z.succ counter) tree rest

  module P = Tezos_protocol_alpha.Protocol

  let predict_current_messages_hash level current_messages =
    let open P.Alpha_context.Sc_rollup in
    let open Lwt.Syntax in
    let level_bytes =
      Data_encoding.Binary.to_bytes_exn
        P.Raw_level_repr.encoding
        (P.Raw_level_repr.of_int32_exn level)
    in
    let number_of_messages_bytes =
      Data_encoding.Binary.to_bytes_exn
        Data_encoding.n
        (Z.of_int (List.length current_messages))
    in
    let* tree = add (empty ()) ["level"] level_bytes in
    let* tree = add tree ["number_of_messages"] number_of_messages_bytes in
    let* tree = build_current_messages_tree Z.zero tree current_messages in
    let context_hash = hash tree in
    let test =
      Data_encoding.Binary.to_bytes_exn
        Tezos_base.TzPervasives.Context_hash.encoding
        context_hash
    in
    return (Inbox.Hash.of_bytes_exn test)
end

let fetch_messages_from_block sc_rollup client =
  let* ops = RPC.Client.call client @@ RPC.get_chain_block_operations () in
  let messages =
    ops |> JSON.as_list
    |> List.concat_map JSON.as_list
    |> List.concat_map (fun op -> JSON.(op |-> "contents" |> as_list))
    |> List.filter_map (fun op ->
           if
             JSON.(op |-> "kind" |> as_string) = "sc_rollup_add_messages"
             && JSON.(op |-> "rollup" |> as_string) = sc_rollup
           then Some JSON.(op |-> "message" |> as_list)
           else None)
    |> List.hd
    |> List.map (fun message -> JSON.(message |> as_string))
  in
  return messages

let test_rollup_inbox_current_messages_hash ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "inbox"; kind]
    (Format.asprintf
       "%s - pushing messages in the inbox - current messages hash"
       kind)
    (fun protocol ->
      setup ~protocol @@ fun node client ->
      ( with_fresh_rollup ~kind @@ fun sc_rollup _sc_rollup_node _filename ->
        let gen_message_batch from until =
          List.map
            (fun x ->
              Printf.sprintf "hello, message number %s" (Int.to_string x))
            (range from until)
        in
        let prepare_batch messages =
          messages
          |> List.map (Printf.sprintf "\"%s\"")
          |> String.concat ", " |> Printf.sprintf "text:[%s]"
        in
        let open Tezos_protocol_alpha.Protocol.Alpha_context.Sc_rollup in
        (* no messages have been sent *)
        let* pristine_hash, nb_available_messages =
          get_inbox_from_tezos_node sc_rollup client
        in
        let () =
          Check.((nb_available_messages = 0) int)
            ~error_msg:"0 messages expected in the inbox"
        in
        let* expected = Sc_rollup_inbox.predict_current_messages_hash 0l [] in
        let () =
          Check.(
            (Inbox.Hash.to_b58check expected = pristine_hash)
              string
              ~error_msg:"FIRST: expected pristine hash %L, got %R")
        in
        (*
           send messages, and assert that
           - the hash has changed
           - the hash matches the 'predicted' hash from the messages we sent
        *)
        let fst_batch = gen_message_batch 0 4 in
        let* () = send_message client sc_rollup @@ prepare_batch fst_batch in
        let* fst_batch_hash, _ = get_inbox_from_tezos_node sc_rollup client in
        let () =
          Check.(
            (pristine_hash <> fst_batch_hash)
              string
              ~error_msg:
                "expected current messages hash to change when messages sent")
        in
        let* expected =
          Sc_rollup_inbox.predict_current_messages_hash 3l fst_batch
        in
        let () =
          Check.(
            (Inbox.Hash.to_b58check expected = fst_batch_hash)
              string
              ~error_msg:"2 expected first batch hash %L, got %R")
        in
        (*
           send more messages, and assert that
           - the messages can be retrieved from the latest block
           - the hash matches the 'predicted' hash from the messages we sent
        *)
        let snd_batch = gen_message_batch 5 10 in
        let* () = send_message client sc_rollup @@ prepare_batch snd_batch in
        let* messages = fetch_messages_from_block sc_rollup client in
        let () =
          Check.(
            (messages = snd_batch)
              (list string)
              ~error_msg:"expected messages:\n%R\nretrieved:\n%L")
        in
        let* snd_batch_hash, _ = get_inbox_from_tezos_node sc_rollup client in
        let* expected =
          Sc_rollup_inbox.predict_current_messages_hash 4l snd_batch
        in
        let () =
          Check.(
            (Inbox.Hash.to_b58check expected = snd_batch_hash)
              string
              ~error_msg:"expected second batch hash %L, got %R")
        in
        unit )
        node
        client)

(* Synchronizing the inbox in the rollup node
   ------------------------------------------

   For each new head set by the Tezos node, the rollup node retrieves
   the messages of its rollup and maintains its internal inbox in a
   persistent state stored in its data directory. This process can
   handle Tezos chain reorganization and can also catch up to ensure a
   tight synchronization between the rollup and the layer 1 chain.

   In addition, this maintenance includes the computation of a Merkle
   tree which must have the same root hash as the one stored by the
   protocol in the context.
*)
let test_rollup_inbox_of_rollup_node variant scenario ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "inbox"; "node"; variant; kind]
    (Printf.sprintf
       "%s - maintenance of inbox in the rollup node (%s)"
       kind
       variant)
    (fun protocol ->
      setup ~protocol @@ fun node client ->
      ( with_fresh_rollup ~kind @@ fun sc_rollup sc_rollup_node _filename ->
        let* () = scenario protocol sc_rollup_node sc_rollup node client in
        let* inbox_from_sc_rollup_node =
          get_inbox_from_sc_rollup_node sc_rollup_node
        in
        let* inbox_from_tezos_node =
          get_inbox_from_tezos_node sc_rollup client
        in
        return
        @@ Check.(
             (inbox_from_sc_rollup_node = inbox_from_tezos_node)
               (tuple2 string int)
               ~error_msg:"expected value %R, got %L") )
        node
        client)

let basic_scenario _protocol sc_rollup_node sc_rollup _node client =
  let num_messages = 2 in
  let expected_level =
    (* We start at level 2 and each message also bakes a block. With 2 messages being sent, we
       must end up at level 4. *)
    4
  in
  let* () = Sc_rollup_node.run sc_rollup_node in
  Log.info "before sending messages\n" ;
  let* () = send_messages num_messages sc_rollup client in
  let* level = Client.level client in
  Log.info "level: %d\n" level ;
  let* _ =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node expected_level
  in
  return ()

(* Reactivate when the following TODO is fixed:

   FIXME: https://gitlab.com/tezos/tezos/-/issues/3205

   The rollup node should be able to restart properly after an
   abnormal interruption at every point of its process.  Currently,
   the state is not persistent enough and the processing is not
   idempotent enough to achieve that property. *)
let _sc_rollup_node_stops_scenario _protocol sc_rollup_node sc_rollup _node
    client =
  let num_messages = 2 in
  let expected_level =
    (* We start at level 2 and each message also bakes a block. With 2 messages being sent twice, we
       must end up at level 6. *)
    6
  in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let* () = send_messages num_messages sc_rollup client in
  let* () = Sc_rollup_node.terminate sc_rollup_node in
  let* () = send_messages num_messages sc_rollup client in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let* _ =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node expected_level
  in
  return ()

let sc_rollup_node_disconnects_scenario _protocol sc_rollup_node sc_rollup node
    client =
  let num_messages = 2 in
  let level = Node.get_level node in
  Log.info "we are at level %d" level ;
  let* () = Sc_rollup_node.run sc_rollup_node in
  let* () = send_messages num_messages sc_rollup client in
  let* level =
    Sc_rollup_node.wait_for_level sc_rollup_node (level + num_messages)
  in
  Log.info "Terminating Tezos node" ;
  let* () = Node.terminate node in
  Log.info "Waiting before restarting Tezos node" ;
  let* () = Lwt_unix.sleep 3. in
  Log.info "Restarting Tezos node" ;
  let* () = Node.run node Node.[Connections 0; Synchronisation_threshold 0] in
  let* () = Node.wait_for_ready node in
  let* () = send_messages num_messages sc_rollup client in
  let* _ =
    Sc_rollup_node.wait_for_level sc_rollup_node (level + num_messages)
  in
  return ()

let sc_rollup_node_handles_chain_reorg protocol sc_rollup_node sc_rollup node
    client =
  let num_messages = 1 in

  setup ~protocol @@ fun node' client' _ ->
  let* () = Client.Admin.trust_address client ~peer:node'
  and* () = Client.Admin.trust_address client' ~peer:node in
  let* () = Client.Admin.connect_address client ~peer:node' in

  let* () = Sc_rollup_node.run sc_rollup_node in
  let* () = send_messages num_messages sc_rollup client in
  (* Since we start at level 2, sending 1 message (which also bakes a block) must cause the nodes to
     observe level 3. *)
  let* _ = Node.wait_for_level node 3 in
  let* _ = Node.wait_for_level node' 3 in
  let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node 3 in
  Log.info "Nodes are synchronized." ;

  let divergence () =
    let* identity' = Node.wait_for_identity node' in
    let* () = Client.Admin.kick_peer client ~peer:identity' in
    let* () = send_messages num_messages sc_rollup client in
    (* +1 block for [node] *)
    let* _ = Node.wait_for_level node 4 in

    let* () = send_messages num_messages sc_rollup client' in
    let* () = send_messages num_messages sc_rollup client' in
    (* +2 blocks for [node'] *)
    let* _ = Node.wait_for_level node' 5 in
    Log.info "Nodes are following distinct branches." ;
    return ()
  in

  let trigger_reorg () =
    let* () = Client.Admin.connect_address client ~peer:node' in
    let* _ = Node.wait_for_level node 5 in
    Log.info "Nodes are synchronized again." ;
    return ()
  in

  let* () = divergence () in
  let* () = trigger_reorg () in
  (* After bringing [node'] back, our SCORU node should see that there is a more attractive head at
     level 5. *)
  let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node 5 in
  return ()

(* One can retrieve the list of originated SCORUs.
   -----------------------------------------------
*)
let with_fresh_rollups ~kind n f node client operator =
  let rec go n addrs k =
    if n < 1 then k addrs
    else
      with_fresh_rollup
        ~kind
        (fun addr _ _ -> go (n - 1) (String_set.add addr addrs) k)
        node
        client
        operator
  in
  go n String_set.empty f

let test_rollup_list ~kind =
  let open Lwt.Syntax in
  let go node client bootstrap1 =
    let* rollups =
      RPC.Client.call client @@ RPC.get_chain_block_context_sc_rollup ()
    in
    let rollups = JSON.as_list rollups in
    let () =
      match rollups with
      | _ :: _ ->
          failwith "Expected initial list of originated SCORUs to be empty"
      | [] -> ()
    in

    with_fresh_rollups
      ~kind
      10
      (fun scoru_addresses ->
        let* () = Client.bake_for_and_wait client in
        let+ rollups =
          RPC.Client.call client @@ RPC.get_chain_block_context_sc_rollup ()
        in
        let rollups =
          JSON.as_list rollups |> List.map JSON.as_string |> String_set.of_list
        in
        Check.(
          (rollups = scoru_addresses)
            (comparable_module (module String_set))
            ~error_msg:"%L %R"))
      node
      client
      bootstrap1
  in

  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "list"]
    "list originated rollups"
    (fun protocol -> setup ~protocol go)

(* Make sure the rollup node boots into the initial state.
   -------------------------------------------------------

   When a rollup node starts, we want to make sure that in the absence of
   messages it will boot into the initial state.
*)
let test_rollup_node_boots_into_initial_state ~kind =
  let go client sc_rollup sc_rollup_node =
    let* genesis_info =
      RPC.Client.call ~hooks client
      @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
    in
    let init_level = JSON.(genesis_info |-> "level" |> as_int) in

    let* () = Sc_rollup_node.run sc_rollup_node in
    let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in

    let* level =
      Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
    in
    Check.(level = init_level)
      Check.int
      ~error_msg:"Current level has moved past origination level (%L = %R)" ;

    let* ticks = Sc_rollup_client.total_ticks ~hooks sc_rollup_client in
    Check.(ticks = 0)
      Check.int
      ~error_msg:"Unexpected initial tick count (%L = %R)" ;

    let* status = Sc_rollup_client.status ~hooks sc_rollup_client in
    let expected_status =
      match kind with
      | "arith" -> "Halted"
      | "wasm_2_0_0" -> "Computing"
      | _ -> raise (Invalid_argument kind)
    in
    Check.(status = expected_status)
      Check.string
      ~error_msg:"Unexpected PVM status (%L = %R)" ;

    Lwt.return_unit
  in

  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "run"; "node"; kind]
    (Format.asprintf "%s - node boots into the initial state" kind)
    (fun protocol ->
      setup ~protocol @@ fun node client ->
      with_fresh_rollup
        ~kind
        (fun sc_rollup sc_rollup_node _filename ->
          go client sc_rollup sc_rollup_node)
        node
        client)

let test_rollup_node_advances_pvm_state protocols ~test_name ~boot_sector
    ~internal ~kind =
  let go ~internal client sc_rollup sc_rollup_node =
    let* genesis_info =
      RPC.Client.call ~hooks client
      @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
    in
    let init_level = JSON.(genesis_info |-> "level" |> as_int) in

    let* () = Sc_rollup_node.run sc_rollup_node in
    let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in

    let* level =
      Sc_rollup_node.wait_for_level ~timeout:30. sc_rollup_node init_level
    in
    Check.(level = init_level)
      Check.int
      ~error_msg:"Current level has moved past origination level (%L = %R)" ;
    let* level, forwarder =
      if not internal then return (level, None)
      else
        (* Originate forwarder contract to send internal messages to rollup *)
        let* contract_id =
          Client.originate_contract
            ~alias:"rollup_deposit"
            ~amount:Tez.zero
            ~src:Constant.bootstrap1.alias
            ~prg:"file:./tezt/tests/contracts/proto_alpha/sc_rollup_forward.tz"
            ~init:"Unit"
            ~burn_cap:Tez.(of_int 1)
            client
        in
        let* () = Client.bake_for_and_wait client in
        Log.info
          "The forwarder %s contract was successfully originated"
          contract_id ;
        return (level + 1, Some contract_id)
    in
    (* Called with monotonically increasing [i] *)
    let test_message i =
      let* prev_state_hash =
        Sc_rollup_client.state_hash ~hooks sc_rollup_client
      in
      let* prev_ticks = Sc_rollup_client.total_ticks ~hooks sc_rollup_client in
      let message = sf "%d %d + value" i ((i + 2) * 2) in
      let* () =
        match forwarder with
        | None ->
            (* External message *)
            send_message client sc_rollup (sf "[%S]" message)
        | Some forwarder ->
            (* Internal message through forwarder *)
            let* () =
              Client.transfer
                client
                ~amount:Tez.zero
                ~giver:Constant.bootstrap1.alias
                ~receiver:forwarder
                ~arg:(sf "Pair %S %S" sc_rollup message)
            in
            Client.bake_for_and_wait client
      in
      let* _ =
        Sc_rollup_node.wait_for_level ~timeout:30. sc_rollup_node (level + i)
      in

      (* specific per kind PVM checks *)
      let* () =
        match kind with
        | "arith" ->
            let* encoded_value =
              Sc_rollup_client.state_value
                ~hooks
                sc_rollup_client
                ~key:"vars/value"
            in
            let value =
              match Data_encoding.(Binary.of_bytes int31) @@ encoded_value with
              | Error error ->
                  failwith
                    (Format.asprintf
                       "The arithmetic PVM has an unexpected state: %a"
                       Data_encoding.Binary.pp_read_error
                       error)
              | Ok x -> x
            in
            Check.(
              (value = i + ((i + 2) * 2))
                int
                ~error_msg:"Invalid value in rollup state (%L <> %R)") ;
            return ()
        | "wasm_2_0_0" ->
            (* TODO: https://gitlab.com/tezos/tezos/-/issues/3729

                Add an appropriate check for various test kernels

                computation.wasm               - Gets into eval state
                no_parse_random.wasm           - Stuck state due to parse error
                no_parse_bad_fingerprint.wasm  - Stuck state due to parse error
            *)
            return ()
        | _otherwise -> raise (Invalid_argument kind)
      in

      let* state_hash = Sc_rollup_client.state_hash ~hooks sc_rollup_client in
      Check.(state_hash <> prev_state_hash)
        Check.string
        ~error_msg:"State hash has not changed (%L <> %R)" ;

      let* ticks = Sc_rollup_client.total_ticks ~hooks sc_rollup_client in
      Check.(ticks >= prev_ticks)
        Check.int
        ~error_msg:"Tick counter did not advance (%L >= %R)" ;

      Lwt.return_unit
    in
    let* () = Lwt_list.iter_s test_message (range 1 10) in

    Lwt.return_unit
  in

  if not internal then
    regression_test
      ~__FILE__
      ~tags:["sc_rollup"; "run"; "node"; kind]
      test_name
      (fun protocol ->
        setup ~protocol @@ fun node client ->
        with_fresh_rollup
          ~kind
          ?boot_sector
          (fun sc_rollup_address sc_rollup_node _filename ->
            go ~internal:false client sc_rollup_address sc_rollup_node)
          node
          client)
      protocols
  else
    regression_test
      ~__FILE__
      ~tags:["sc_rollup"; "run"; "node"; "internal"; kind]
      test_name
      (fun protocol ->
        setup ~protocol @@ fun node client ->
        with_fresh_rollup
          ~kind
          ?boot_sector
          (fun sc_rollup_address sc_rollup_node _filename ->
            go ~internal:true client sc_rollup_address sc_rollup_node)
          node
          client)
      protocols

let test_rollup_node_run_with_kernel protocols ~kind ~kernel_name ~internal =
  test_rollup_node_advances_pvm_state
    protocols
    ~test_name:(Format.asprintf "%s - runs with kernel - %s" kind kernel_name)
    ~boot_sector:(Some (read_kernel kernel_name))
    ~internal
    ~kind

(* Ensure the PVM is transitioning upon incoming messages.
      -------------------------------------------------------

      When the rollup node receives messages, we like to see evidence that the PVM
      has advanced.

      Specifically [test_rollup_node_advances_pvm_state ?boot_sector protocols ~kind]

      * Originates a SCORU of [kind]
      * Originates a L1 contract to send internal messages from
      * Sends internal or external messages to the rollup

   After each a PVM kind-specific test is run, asserting the validity of the new state.
*)
let test_rollup_node_advances_pvm_state protocols ~kind ~boot_sector ~internal =
  test_rollup_node_advances_pvm_state
    protocols
    ~test_name:
      (Format.asprintf
         "%s - node advances PVM state with %smessages"
         kind
         (if internal then "internal " else ""))
    ~boot_sector
    ~internal
    ~kind

(* Ensure that commitments are stored and published properly.
   ----------------------------------------------------------

   Every 20 level, a commitment is computed and stored by the
   rollup node. The rollup node will also publish previously
   computed commitments on the layer1, in a first in first out
   fashion. To ensure that commitments are robust to chain
   reorganisations, only finalized block are processed when
   trying to publish a commitment.
*)

let bake_levels ?hook n client =
  fold n () @@ fun i () ->
  let* () = match hook with None -> return () | Some hook -> hook i in
  Client.bake_for_and_wait client

let eq_commitment_typ =
  Check.equalable
    (fun ppf (c : Sc_rollup_client.commitment) ->
      Format.fprintf
        ppf
        "@[<hov 2>{ predecessor: %s,@,\
         state: %s,@,\
         inbox level: %d,@,\
         ticks: %d }@]"
        c.predecessor
        c.compressed_state
        c.inbox_level
        c.number_of_ticks)
    ( = )

let check_commitment_eq (commitment, name) (expected_commitment, exp_name) =
  Check.((commitment = expected_commitment) (option eq_commitment_typ))
    ~error_msg:
      (sf
         "Commitment %s differs from the one %s.\n%s: %%L\n%s: %%R"
         name
         exp_name
         (String.capitalize_ascii name)
         (String.capitalize_ascii exp_name))

let tezos_client_get_commitment client sc_rollup commitment_hash =
  let* output =
    Client.rpc
      Client.GET
      [
        "chains";
        "main";
        "blocks";
        "head";
        "context";
        "sc_rollup";
        sc_rollup;
        "commitment";
        commitment_hash;
      ]
      client
  in
  Lwt.return @@ Sc_rollup_client.commitment_from_json output

let check_published_commitment_in_l1 ?(allow_non_published = false)
    ?(force_new_level = true) sc_rollup client published_commitment =
  let* () =
    if force_new_level then
      (* Triggers injection into the L1 context *)
      bake_levels 1 client
    else Lwt.return_unit
  in
  let* commitment_in_l1 =
    match published_commitment with
    | None ->
        if not allow_non_published then
          Test.fail "No commitment has been published" ;
        Lwt.return_none
    | Some (hash, _commitment, _level) ->
        tezos_client_get_commitment client sc_rollup hash
  in
  let published_commitment =
    Option.map (fun (_, c, _) -> c) published_commitment
  in
  check_commitment_eq
    (commitment_in_l1, "in L1")
    (published_commitment, "published") ;
  Lwt.return_unit

let test_commitment_scenario ?commitment_period ?challenge_window
    ?(extra_tags = []) variant =
  test_scenario
    ?commitment_period
    ?challenge_window
    {
      tags = ["commitment"; "node"] @ extra_tags;
      variant;
      description = "rollup node - correct handling of commitments";
    }

let commitment_stored _protocol sc_rollup_node sc_rollup _node client =
  (* The rollup is originated at level `init_level`, and it requires
     `sc_rollup_commitment_period_in_blocks` levels to store a commitment.
     There is also a delay of `block_finality_time` before storing a
     commitment, to avoid including wrong commitments due to chain
     reorganisations. Therefore the commitment will be stored and published
     when the [Commitment] module processes the block at level
     `init_level + sc_rollup_commitment_period_in_blocks +
     levels_to_finalise`.
  *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let store_commitment_level =
    init_level + levels_to_commitment + block_finality_time
  in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () =
    (* at init_level + i we publish i messages, therefore at level
       init_level + i a total of 1+..+i = (i*(i+1))/2 messages will have been
       sent.
    *)
    send_messages levels_to_commitment sc_rollup client
  in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + levels_to_commitment)
  in
  (* Bake [block_finality_time] additional levels to ensure that block number
     [init_level + sc_rollup_commitment_period_in_blocks] is
     processed by the rollup node as finalized. *)
  let* () = bake_levels block_finality_time client in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      store_commitment_level
  in
  let* stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let stored_inbox_level = Option.map inbox_level stored_commitment in
  Check.(stored_inbox_level = Some (levels_to_commitment + init_level))
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been stored at a level different than expected (%L = %R)" ;
  (* Bake one level for commitment to be included *)
  let* () = Client.bake_for_and_wait client in
  let* published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  check_commitment_eq
    (Option.map (fun (_, c, _) -> c) stored_commitment, "stored")
    (Option.map (fun (_, c, _) -> c) published_commitment, "published") ;
  check_published_commitment_in_l1 sc_rollup client published_commitment

let mode_publish mode publishes protocol sc_rollup_node sc_rollup node client =
  setup ~protocol @@ fun other_node other_client _ ->
  let* () = Client.Admin.trust_address client ~peer:other_node
  and* () = Client.Admin.trust_address other_client ~peer:node in
  let* () = Client.Admin.connect_address client ~peer:other_node in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let level = Node.get_level node in
  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let* () = send_messages levels_to_commitment sc_rollup client in
  let* level =
    Sc_rollup_node.wait_for_level sc_rollup_node (level + levels_to_commitment)
  in
  Log.info "Starting other rollup node." ;
  let purposes = ["publish"; "cement"; "add_messages"] in
  let operators =
    List.mapi
      (fun i purpose ->
        (purpose, Constant.[|bootstrap3; bootstrap5; bootstrap4|].(i).alias))
      purposes
  in
  let sc_rollup_other_node =
    (* Other rollup node *)
    Sc_rollup_node.create
      mode
      other_node
      other_client
      ~operators
      ~default_operator:Constant.bootstrap3.alias
  in
  let sc_rollup_other_client = Sc_rollup_client.create sc_rollup_other_node in
  let* _configuration_filename =
    Sc_rollup_node.config_init sc_rollup_other_node sc_rollup
  in
  let* () = Sc_rollup_node.run sc_rollup_other_node in
  let* _level = Sc_rollup_node.wait_for_level sc_rollup_other_node level in
  Log.info "Other rollup node synchronized." ;
  let* () = send_messages levels_to_commitment sc_rollup client in
  let* level =
    Sc_rollup_node.wait_for_level sc_rollup_node (level + levels_to_commitment)
  in
  let* _ = Sc_rollup_node.wait_for_level sc_rollup_node level
  and* _ = Sc_rollup_node.wait_for_level sc_rollup_other_node level in
  Log.info "Both rollup nodes have reached level %d." level ;
  let* state_hash = Sc_rollup_client.state_hash sc_rollup_client
  and* state_hash_other = Sc_rollup_client.state_hash sc_rollup_other_client in
  Check.((state_hash = state_hash_other) string)
    ~error_msg:
      "State hash of other rollup node is %R but the first rollup node has %L" ;
  let* published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  let* other_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_other_client
  in
  if published_commitment = None then
    Test.fail "Operator has not published a commitment but should have." ;
  if other_published_commitment = None = publishes then
    Test.fail
      "Other has%s published a commitment but should%s."
      (if publishes then " not" else "")
      (if publishes then " have" else " never do so") ;
  unit

let commitment_not_stored_if_non_final _protocol sc_rollup_node sc_rollup _node
    client =
  (* The rollup is originated at level `init_level`, and it requires
     `sc_rollup_commitment_period_in_blocks` levels to store a commitment.
     There is also a delay of `block_finality_time` before storing a
     commitment, to avoid including wrong commitments due to chain
     reorganisations. Therefore the commitment will be stored and published
     when the [Commitment] module processes the block at level
     `init_level + sc_rollup_commitment_period_in_blocks +
     levels_to_finalise`. At the level before, the commitment will not be
     neither stored nor published.
  *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let levels_to_finalize = block_finality_time - 1 in
  let store_commitment_level = init_level + levels_to_commitment in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () = send_messages levels_to_commitment sc_rollup client in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      store_commitment_level
  in
  let* () = bake_levels levels_to_finalize client in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (store_commitment_level + levels_to_finalize)
  in
  let* commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let stored_inbox_level = Option.map inbox_level commitment in
  Check.(stored_inbox_level = None)
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been stored at a level different than expected (%L = %R)" ;
  let* commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  let published_inbox_level = Option.map inbox_level commitment in
  Check.(published_inbox_level = None)
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been published at a level different than expected (%L = \
       %R)" ;
  Lwt.return_unit

let commitments_messages_reset _protocol sc_rollup_node sc_rollup _node client =
  (* For `sc_rollup_commitment_period_in_blocks` levels after the sc rollup
     origination, i messages are sent to the rollup, for a total of
     `sc_rollup_commitment_period_in_blocks *
     (sc_rollup_commitment_period_in_blocks + 1)/2` messages. These will be
     the number of messages in the first commitment published by the rollup
     node. Then, for other `sc_rollup_commitment_period_in_blocks` levels,
     no messages are sent to the sc-rollup address. The second commitment
     published by the sc-rollup node will contain 0 messages. Finally,
     `block_finality_time` empty levels are baked which ensures that two
     commitments are stored and published by the rollup node.
  *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () =
    (* At init_level + i we publish i messages, therefore at level
       init_level + 20 a total of 1+..+20 = (20*21)/2 = 210 messages
       will have been sent.
    *)
    send_messages levels_to_commitment sc_rollup client
  in
  (* Bake other `sc_rollup_commitment_period_in_blocks +
     block_finality_time` levels with no messages. The first
     `sc_rollup_commitment_period_in_blocks` levels contribute to the second
     commitment stored by the rollup node. The last `block_finality_time`
     levels ensure that the second commitment is stored and published by the
     rollup node.
  *)
  let* () = bake_levels (levels_to_commitment + block_finality_time) client in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + (2 * levels_to_commitment) + block_finality_time)
  in
  let* stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let stored_inbox_level = Option.map inbox_level stored_commitment in
  Check.(stored_inbox_level = Some (init_level + (2 * levels_to_commitment)))
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been stored at a level different than expected (%L = %R)" ;
  (let stored_number_of_ticks = Option.map number_of_ticks stored_commitment in
   Check.(stored_number_of_ticks = Some 0)
     (Check.option Check.int)
     ~error_msg:
       "Number of messages processed by commitment is different from the \
        number of messages expected (%L = %R)") ;
  let* published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  check_commitment_eq
    (Option.map (fun (_, c, _) -> c) stored_commitment, "stored")
    (Option.map (fun (_, c, _) -> c) published_commitment, "published") ;
  check_published_commitment_in_l1 sc_rollup client published_commitment

let commitment_stored_robust_to_failures _protocol sc_rollup_node sc_rollup node
    client =
  (* This test uses two rollup nodes for the same rollup, tracking the same L1 node.
     Both nodes process heads from the L1. However, the second node is stopped
     one level before publishing a commitment, and then is restarted.
     We should not observe any difference in the commitments stored by the
     two rollup nodes.
  *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let bootstrap2_key = Constant.bootstrap2.public_key_hash in
  let* client' = Client.init ?endpoint:(Some (Node node)) () in
  let sc_rollup_node' =
    Sc_rollup_node.create Operator node client' ~default_operator:bootstrap2_key
  in
  let* _configuration_filename =
    Sc_rollup_node.config_init sc_rollup_node' sc_rollup
  in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let sc_rollup_client' = Sc_rollup_client.create sc_rollup_node' in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let* () = Sc_rollup_node.run sc_rollup_node' in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () =
    (* at init_level + i we publish i messages, therefore at level
       init_level + i a total of 1+..+i = (i*(i+1))/2 messages will have been
       sent.
    *)
    send_messages levels_to_commitment sc_rollup client
  in
  (* The line below works as long as we have a block finality time which is strictly positive,
     which is a safe assumption. *)
  let* () = bake_levels (block_finality_time - 1) client in
  let* level_before_storing_commitment =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + levels_to_commitment + block_finality_time - 1)
  in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node'
      level_before_storing_commitment
  in
  let* () = Sc_rollup_node.terminate sc_rollup_node' in
  let* () = Sc_rollup_node.run sc_rollup_node' in
  let* () = Client.bake_for_and_wait client in
  let* () = Sc_rollup_node.terminate sc_rollup_node' in
  let* () = Client.bake_for_and_wait client in
  let* () = Sc_rollup_node.run sc_rollup_node' in
  let* level_commitment_is_stored =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (level_before_storing_commitment + 1)
  in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node'
      level_commitment_is_stored
  in
  let* stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let* stored_commitment' =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client'
  in
  check_commitment_eq
    (Option.map (fun (_, c, _) -> c) stored_commitment, "stored in first node")
    (Option.map (fun (_, c, _) -> c) stored_commitment', "stored in second node") ;
  return ()

let commitments_reorgs protocol sc_rollup_node sc_rollup node client =
  (* No messages are published after origination, for
     `sc_rollup_commitment_period_in_blocks - 1` levels. Then a divergence
     occurs:  in the first branch one message is published for
     `block_finality_time - 1` blocks. In the second branch no messages are
     published for `block_finality_time` blocks. The second branch is
     the more attractive one, and will be chosen when a reorganisation occurs.
     One more level is baked to ensure that the rollup node stores and
     publishes the commitment. The final commitment should have
     no messages and no ticks.
  *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* levels_to_commitment =
    get_sc_rollup_commitment_period_in_blocks client
  in
  let num_empty_blocks = block_finality_time in
  let num_messages = 1 in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in

  setup ~protocol @@ fun node' client' _ ->
  let* () = Client.Admin.trust_address client ~peer:node'
  and* () = Client.Admin.trust_address client' ~peer:node in
  let* () = Client.Admin.connect_address client ~peer:node' in

  let* () = Sc_rollup_node.run sc_rollup_node in
  (* We bake `sc_rollup_commitment_period_in_blocks - 1` levels, which
     should cause both nodes to observe level
     `sc_rollup_commitment_period_in_blocks + init_level - 1 . *)
  let* () = bake_levels (levels_to_commitment - 1) client in
  let* _ = Node.wait_for_level node (init_level + levels_to_commitment - 1) in
  let* _ = Node.wait_for_level node' (init_level + levels_to_commitment - 1) in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + levels_to_commitment - 1)
  in
  Log.info "Nodes are synchronized." ;

  let divergence () =
    let* identity' = Node.wait_for_identity node' in
    let* () = Client.Admin.kick_peer client ~peer:identity' in
    let* () = send_messages num_messages sc_rollup client in
    (* `block_finality_time - 1` blocks with message for [node] *)
    let* _ =
      Node.wait_for_level
        node
        (init_level + levels_to_commitment - 1 + num_messages)
    in

    let* () = bake_levels num_empty_blocks client' in
    (* `block_finality_time` blocks with no messages for [node'] *)
    let* _ =
      Node.wait_for_level
        node'
        (init_level + levels_to_commitment - 1 + num_empty_blocks)
    in
    Log.info "Nodes are following distinct branches." ;
    return ()
  in

  let trigger_reorg () =
    let* () = Client.Admin.connect_address client ~peer:node' in
    let* _ =
      Node.wait_for_level
        node
        (init_level + levels_to_commitment - 1 + num_empty_blocks)
    in
    Log.info "Nodes are synchronized again." ;
    return ()
  in

  let* () = divergence () in
  let* () = trigger_reorg () in
  (* After triggering a reorganisation the node should see that there is a more
     attractive head at level `init_level +
     sc_rollup_commitment_period_in_blocks + block_finality_time - 1`.
  *)
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + levels_to_commitment - 1 + num_empty_blocks)
  in
  (* exactly one level left to finalize the commitment in the node. *)
  let* () = bake_levels (block_finality_time - num_empty_blocks + 1) client in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + levels_to_commitment + block_finality_time)
  in
  let* stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let stored_inbox_level = Option.map inbox_level stored_commitment in
  Check.(stored_inbox_level = Some (init_level + levels_to_commitment))
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been stored at a level different than expected (%L = %R)" ;
  (let stored_number_of_ticks = Option.map number_of_ticks stored_commitment in
   Check.(stored_number_of_ticks = Some 0)
     (Check.option Check.int)
     ~error_msg:
       "Number of messages processed by commitment is different from the \
        number of messages expected (%L = %R)") ;
  let* published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  check_commitment_eq
    (Option.map (fun (_, c, _) -> c) stored_commitment, "stored")
    (Option.map (fun (_, c, _) -> c) published_commitment, "published") ;
  check_published_commitment_in_l1 sc_rollup client published_commitment

type balances = {liquid : int; frozen : int}

let contract_balances ~pkh client =
  let*! liquid = RPC.Contracts.get_balance ~contract_id:pkh client in
  let*! frozen = RPC.Contracts.get_frozen_bonds ~contract_id:pkh client in
  return {liquid = JSON.as_int liquid; frozen = JSON.as_int frozen}

(** This helper allow to attempt recovering bond for SCORU rollup operator.
    if [expect_failure] is set to some string then, we expect the command to fail
    with an error that contains that string. *)
let attempt_withdraw_stake =
  let check_eq_int a b =
    Check.((a = b) int ~error_msg:"expected value %L, got %R")
  in
  fun ?expect_failure ~sc_rollup client ->
    (* placehoders *)
    (* TODO/Fixme:
        - Shoud provide the rollup operator key (bootstrap1_key) as an
          argument to scenarios.
    *)
    let bootstrap1_key = Constant.bootstrap1.public_key_hash in
    let* constants =
      RPC.Client.call ~hooks client @@ RPC.get_chain_block_context_constants ()
    in
    let recover_bond_unfreeze =
      JSON.(constants |-> "sc_rollup_stake_amount" |> as_int)
    in
    let recover_bond_fee = 1_000_000 in
    let inject_op () =
      Client.Sc_rollup.submit_recover_bond
        ~hooks
        ~rollup:sc_rollup
        ~src:bootstrap1_key
        ~fee:(Tez.of_mutez_int recover_bond_fee)
        client
    in
    match expect_failure with
    | None ->
        let*! () = inject_op () in
        let* old_bal = contract_balances ~pkh:bootstrap1_key client in
        let* () = Client.bake_for_and_wait ~keys:["bootstrap2"] client in
        let* new_bal = contract_balances ~pkh:bootstrap1_key client in
        let expected_liq_new_bal =
          old_bal.liquid - recover_bond_fee + recover_bond_unfreeze
        in
        check_eq_int new_bal.liquid expected_liq_new_bal ;
        check_eq_int new_bal.frozen (old_bal.frozen - recover_bond_unfreeze) ;
        unit
    | Some failure_string ->
        let*? p = inject_op () in
        Process.check_error ~msg:(rex failure_string) p

(* FIXME: https://gitlab.com/tezos/tezos/-/issues/2942
   Do not pass an explicit value for `?commitment_period until
   https://gitlab.com/tezos/tezos/-/merge_requests/5212 has been merged. *)
(* Test that nodes do not publish commitments before the last cemented commitment. *)
let commitment_before_lcc_not_published _protocol sc_rollup_node sc_rollup node
    client =
  let* constants = get_sc_rollup_constants client in
  let commitment_period = constants.commitment_period_in_blocks in
  let challenge_window = constants.challenge_window_in_blocks in
  (* Rollup node 1 processes messages, produces and publishes two commitments. *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in

  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () = bake_levels commitment_period client in
  let* commitment_inbox_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + commitment_period)
  in
  (* Bake `block_finality_time` additional level to ensure that block number
     `init_level + sc_rollup_commitment_period_in_blocks` is processed by
     the rollup node as finalized. *)
  let* () = bake_levels block_finality_time client in
  let* commitment_finalized_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (commitment_inbox_level + block_finality_time)
  in
  let* rollup_node1_stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client
  in
  let* rollup_node1_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  let () =
    Check.(
      Option.map inbox_level rollup_node1_published_commitment
      = Some commitment_inbox_level)
      (Check.option Check.int)
      ~error_msg:
        "Commitment has been published at a level different than expected (%L \
         = %R)"
  in
  (* Cement commitment manually: the commitment can be cemented after
     `challenge_window_levels` have passed since the commitment was published
     (that is at level `commitment_finalized_level`). Note that at this point
     we are already at level `commitment_finalized_level`, hence cementation of
     the commitment can happen. *)
  let levels_to_cementation = challenge_window + 1 in
  let cemented_commitment_hash =
    Option.map hash rollup_node1_published_commitment
    |> Option.value
         ~default:"scc12XhSULdV8bAav21e99VYLTpqAjTd7NU8Mn4zFdKPSA8auMbggG"
  in
  let* () = bake_levels levels_to_cementation client in
  let* cemented_commitment_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (commitment_finalized_level + levels_to_cementation)
  in

  (* Withdraw stake before cementing should fail *)
  let* () =
    attempt_withdraw_stake
      ~sc_rollup
      client
      ~expect_failure:
        "Attempted to withdraw while not staked on the last cemented \
         commitment."
  in

  let* () =
    cement_commitment client ~sc_rollup ~hash:cemented_commitment_hash
  in
  let* level_after_cementation =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (cemented_commitment_level + 1)
  in

  (* Withdraw stake after cementing should succeed *)
  let* () = attempt_withdraw_stake ~sc_rollup client in

  let* () = Sc_rollup_node.terminate sc_rollup_node in
  (* Rollup node 2 starts and processes enough levels to publish a commitment.*)
  let bootstrap2_key = Constant.bootstrap2.public_key_hash in
  let* client' = Client.init ?endpoint:(Some (Node node)) () in
  let sc_rollup_node' =
    Sc_rollup_node.create Operator node client' ~default_operator:bootstrap2_key
  in
  let sc_rollup_client' = Sc_rollup_client.create sc_rollup_node' in
  let* _configuration_filename =
    Sc_rollup_node.config_init sc_rollup_node' sc_rollup
  in
  let* () = Sc_rollup_node.run sc_rollup_node' in

  let* rollup_node2_catchup_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node'
      level_after_cementation
  in
  Check.(rollup_node2_catchup_level = level_after_cementation)
    Check.int
    ~error_msg:"Current level has moved past cementation inbox level (%L = %R)" ;
  (* Check that no commitment was published. *)
  let* rollup_node2_last_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client'
  in
  let rollup_node2_last_published_commitment_inbox_level =
    Option.map inbox_level rollup_node2_last_published_commitment
  in
  let () =
    Check.(rollup_node2_last_published_commitment_inbox_level = None)
      (Check.option Check.int)
      ~error_msg:
        "Commitment has been published at a level different than expected (%L \
         = %R)"
  in
  (* Check that the commitment stored by the second rollup node
     is the same commmitment stored by the first rollup node. *)
  let* rollup_node2_stored_commitment =
    Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client'
  in
  let () =
    Check.(
      Option.map hash rollup_node1_stored_commitment
      = Option.map hash rollup_node2_stored_commitment)
      (Check.option Check.string)
      ~error_msg:
        "Commitment stored by first and second rollup nodes differ (%L = %R)"
  in

  (* Bake other commitment_period levels and check that rollup_node2 is
     able to publish a commitment. *)
  let* () = bake_levels commitment_period client' in
  let commitment_inbox_level = commitment_inbox_level + commitment_period in
  let* _ =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node'
      (level_after_cementation + commitment_period)
  in
  let* rollup_node2_last_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client'
  in
  let rollup_node2_last_published_commitment_inbox_level =
    Option.map inbox_level rollup_node2_last_published_commitment
  in
  let () =
    Check.(
      rollup_node2_last_published_commitment_inbox_level
      = Some commitment_inbox_level)
      (Check.option Check.int)
      ~error_msg:
        "Commitment has been published at a level different than expected (%L \
         = %R)"
  in
  let () =
    Check.(
      Option.map predecessor rollup_node2_last_published_commitment
      = Some cemented_commitment_hash)
      (Check.option Check.string)
      ~error_msg:
        "Predecessor fo commitment published by rollup_node2 should be the \
         cemented commitment (%L = %R)"
  in
  return ()

(* Test that the level when a commitment was first published is fetched correctly
   by rollup nodes. *)
let first_published_level_is_global _protocol sc_rollup_node sc_rollup node
    client =
  (* Rollup node 1 processes messages, produces and publishes two commitments. *)
  let* genesis_info =
    RPC.Client.call ~hooks client
    @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
  in
  let init_level = JSON.(genesis_info |-> "level" |> as_int) in
  let* commitment_period = get_sc_rollup_commitment_period_in_blocks client in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
  let* level =
    Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
  in
  Check.(level = init_level)
    Check.int
    ~error_msg:"Current level has moved past origination level (%L = %R)" ;
  let* () = bake_levels commitment_period client in
  let* commitment_inbox_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (init_level + commitment_period)
  in
  (* Bake `block_finality_time` additional level to ensure that block number
     `init_level + sc_rollup_commitment_period_in_blocks` is processed by
     the rollup node as finalized. *)
  let* () = bake_levels block_finality_time client in
  let* commitment_finalized_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node
      (commitment_inbox_level + block_finality_time)
  in
  let* rollup_node1_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  Check.(
    Option.map inbox_level rollup_node1_published_commitment
    = Some commitment_inbox_level)
    (Check.option Check.int)
    ~error_msg:
      "Commitment has been published at a level different than expected (%L = \
       %R)" ;
  (* Bake an additional block for the commitment to be included. *)
  let* () = Client.bake_for_and_wait client in
  let* commitment_publish_level =
    Sc_rollup_node.wait_for_level sc_rollup_node (commitment_finalized_level + 1)
  in
  let* rollup_node1_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client
  in
  Check.(
    Option.bind rollup_node1_published_commitment first_published_at_level
    = Some commitment_publish_level)
    (Check.option Check.int)
    ~error_msg:
      "Level at which commitment has first been published (%L) is wrong. \
       Expected %R." ;
  let* () = Sc_rollup_node.terminate sc_rollup_node in
  (* Rollup node 2 starts and processes enough levels to publish a commitment.*)
  let bootstrap2_key = Constant.bootstrap2.public_key_hash in
  let* client' = Client.init ?endpoint:(Some (Node node)) () in
  let sc_rollup_node' =
    Sc_rollup_node.create Operator node client' ~default_operator:bootstrap2_key
  in
  let sc_rollup_client' = Sc_rollup_client.create sc_rollup_node' in
  let* _configuration_filename =
    Sc_rollup_node.config_init sc_rollup_node' sc_rollup
  in
  let* () = Sc_rollup_node.run sc_rollup_node' in

  let* rollup_node2_catchup_level =
    Sc_rollup_node.wait_for_level
      ~timeout:3.
      sc_rollup_node'
      commitment_finalized_level
  in
  Check.(rollup_node2_catchup_level = commitment_finalized_level)
    Check.int
    ~error_msg:"Current level has moved past cementation inbox level (%L = %R)" ;
  (* Check that no commitment was published. *)
  let* rollup_node2_published_commitment =
    Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client'
  in
  check_commitment_eq
    ( Option.map (fun (_, c, _) -> c) rollup_node1_published_commitment,
      "published by rollup node 1" )
    ( Option.map (fun (_, c, _) -> c) rollup_node2_published_commitment,
      "published by rollup node 2" ) ;
  let () =
    Check.(
      Option.bind rollup_node1_published_commitment first_published_at_level
      = Option.bind rollup_node2_published_commitment first_published_at_level)
      (Check.option Check.int)
      ~error_msg:
        "Rollup nodes do not agree on level when commitment was first \
         published (%L = %R)"
  in
  return ()

(* Check that the SC rollup is correctly originated with a boot sector.
   -------------------------------------------------------

   Originate a rollup with a custom boot sector and check if the RPC returns it.
*)
let test_rollup_arith_origination_boot_sector =
  let boot_sector = "10 10 10 + +" in

  let go client sc_rollup =
    let* client_boot_sector =
      RPC.Client.call ~hooks client
      @@ RPC.get_chain_block_context_sc_rollup_boot_sector sc_rollup
    in
    let client_boot_sector = JSON.as_string client_boot_sector in
    Check.(boot_sector = client_boot_sector)
      Check.string
      ~error_msg:"expected value %L, got %R" ;
    Lwt.return_unit
  in

  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "run"]
    (Format.asprintf "originate arith with boot sector")
    (fun protocol ->
      setup ~protocol @@ fun node client ->
      with_fresh_rollup
        ~kind:"arith"
        ~boot_sector
        (fun sc_rollup _sc_rollup_node _filename -> go client sc_rollup)
        node
        client)

(* Check that a node makes use of the boot sector.
   -------------------------------------------------------

   Originate 2 rollups with different boot sectors to check if the are
   actually different.
*)
let test_rollup_node_uses_arith_boot_sector =
  let go_boot client sc_rollup sc_rollup_node =
    let* genesis_info =
      RPC.Client.call ~hooks client
      @@ RPC.get_chain_block_context_sc_rollup_genesis_info sc_rollup
    in
    let init_level = JSON.(genesis_info |-> "level" |> as_int) in

    let* () = Sc_rollup_node.run sc_rollup_node in

    let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in
    let* level =
      Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node init_level
    in

    let* () = send_text_messages client sc_rollup ["10 +"] in
    let* _ =
      Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node (level + 1)
    in

    Sc_rollup_client.state_hash ~hooks sc_rollup_client
  in

  let with_booted ~boot_sector node client =
    with_fresh_rollup
      ~kind:"arith"
      ~boot_sector
      (fun sc_rollup sc_rollup_node _filename ->
        go_boot client sc_rollup sc_rollup_node)
      node
      client
  in

  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "run"; "node"]
    (Format.asprintf "ensure arith boot sector is used")
    (fun protocol ->
      setup ~protocol @@ fun node client x ->
      let* state_hash1 =
        with_booted ~boot_sector:"10 10 10 + +" node client x
      in
      let* state_hash2 = with_booted ~boot_sector:"31" node client x in
      Check.(state_hash1 <> state_hash2)
        Check.string
        ~error_msg:"State hashes should be different! (%L, %R)" ;

      Lwt.return_unit)

(* Initializes a client with an existing account being
   [Constants.tz4_account]. *)
let client_with_initial_keys ~protocol ~kind =
  setup ~protocol @@ with_fresh_rollup ~kind
  @@ fun _sc_rollup sc_rollup_node _filename ->
  let sc_client = Sc_rollup_client.create sc_rollup_node in
  let account = Constant.tz4_account in
  let* () = Sc_rollup_client.import_secret_key account sc_client in
  return (sc_client, account)

(* Check that the client can show the address of a registered account.
   -------------------------------------------------------------------
*)
let test_rollup_client_show_address ~kind =
  test
    ~__FILE__
    ~tags:["run"; "client"]
    "Shows the address of a registered account"
    (fun protocol ->
      let* sc_client, account = client_with_initial_keys ~protocol ~kind in
      let* shown_account =
        Sc_rollup_client.show_address
          ~alias:account.Account.aggregate_alias
          sc_client
      in
      if
        account.aggregate_public_key_hash
        <> shown_account.aggregate_public_key_hash
      then
        failwith
          (Printf.sprintf
             "Expecting %s, got %s as public key hash from the client."
             account.aggregate_public_key_hash
             shown_account.aggregate_public_key_hash)
      else if account.aggregate_public_key <> shown_account.aggregate_public_key
      then
        failwith
          (Printf.sprintf
             "Expecting %s, got %s as public key from the client."
             account.aggregate_public_key
             shown_account.aggregate_public_key)
      else if account.aggregate_secret_key <> shown_account.aggregate_secret_key
      then
        let (Unencrypted sk) = shown_account.aggregate_secret_key in
        let (Unencrypted expected_sk) = shown_account.aggregate_secret_key in
        failwith
          (Printf.sprintf
             "Expecting %s, got %s as secret key from the client."
             expected_sk
             sk)
      else return ())

(* Check that the client can generate keys.
   ----------------------------------------
*)
let test_rollup_client_generate_keys ~kind =
  test
    ~__FILE__
    ~tags:["run"; "client"]
    "Generates new tz4 keys"
    (fun protocol ->
      setup ~protocol @@ with_fresh_rollup ~kind
      @@ fun _sc_rollup sc_rollup_node _filename ->
      let sc_client = Sc_rollup_client.create sc_rollup_node in
      let alias = "test_key" in
      let* () = Sc_rollup_client.generate_keys ~alias sc_client in
      let* _account = Sc_rollup_client.show_address ~alias sc_client in
      return ())

(* Check that the client can list keys.
   ------------------------------------
*)
let test_rollup_client_list_keys ~kind =
  test
    ~__FILE__
    ~tags:["run"; "client"]
    "Lists known aliases in the client"
    (fun protocol ->
      let* sc_client, account = client_with_initial_keys ~kind ~protocol in
      let* maybe_keys = Sc_rollup_client.list_keys sc_client in
      let expected_keys =
        [(account.aggregate_alias, account.aggregate_public_key_hash)]
      in
      if List.equal ( = ) expected_keys maybe_keys then return ()
      else
        let pp ppf l =
          Format.pp_print_list
            ~pp_sep:(fun ppf () -> Format.fprintf ppf "\n")
            (fun ppf (a, k) -> Format.fprintf ppf "%s: %s" a k)
            ppf
            l
        in
        Test.fail
          ~__LOC__
          "Expecting\n@[%a@]\ngot\n@[%a@]\nas keys from the client."
          pp
          expected_keys
          pp
          maybe_keys)

let publish_dummy_commitment ?(number_of_ticks = 1) ~inbox_level ~predecessor
    ~sc_rollup ~src client =
  let commitment : Sc_rollup_client.commitment =
    {
      compressed_state = Constant.sc_rollup_compressed_state;
      inbox_level;
      predecessor;
      number_of_ticks;
    }
  in
  let*! () = publish_commitment ~src ~commitment client sc_rollup in
  let* () = Client.bake_for_and_wait client in
  get_staked_on_commitment ~sc_rollup ~staker:src client

let test_consecutive_commitments ~kind =
  regression_test
    ~__FILE__
    ~tags:["sc_rollup"; "l1"; "commitment"; kind]
    (Format.asprintf "%s - consecutive commitments" kind)
    (fun protocol ->
      setup ~protocol @@ fun _node client bootstrap1_key ->
      let* inbox_level = Client.level client in
      let* sc_rollup = originate_sc_rollup ~kind ~src:bootstrap1_key client in
      let operator = Constant.bootstrap1.public_key_hash in
      let* {commitment_period_in_blocks; _} = get_sc_rollup_constants client in
      (* As we did no publish any commitment yet, this is supposed to fail. *)
      let*? process =
        RPC.Client.spawn client
        @@ RPC.get_chain_block_context_sc_rollup_staker_staked_on_commitment
             ~sc_rollup
             operator
      in
      let* () = Process.check_error ~msg:(rex "Unknown staker") process in
      let* predecessor, _ =
        last_cemented_commitment_hash_with_level ~sc_rollup client
      in
      let* commit_hash =
        publish_dummy_commitment
          ~inbox_level:(inbox_level + commitment_period_in_blocks + 1)
          ~predecessor
          ~sc_rollup
          ~src:operator
          client
      in
      let* _commit_hash =
        publish_dummy_commitment
          ~inbox_level:(inbox_level + (2 * commitment_period_in_blocks) + 1)
          ~predecessor:commit_hash
          ~sc_rollup
          ~src:operator
          client
      in
      unit)

(* Refutation game scenarios
   -------------------------
*)

(*

   To check the refutation game logic, we evaluate a scenario with one
   honest rollup node and one dishonest rollup node configured as with
   a given [loser_mode].

   For a given sequence of [inputs], distributed amongst several
   levels, with some possible [empty_levels]. We check that at some
   [final_level], the crime does not pay: the dishonest node has losen
   its deposit while the honest one has not.

*)
let test_refutation_scenario ?commitment_period ?challenge_window variant ~kind
    (loser_mode, inputs, final_level, empty_levels, stop_loser_at) =
  test_scenario
    ?commitment_period
    ~kind
    ~timeout:10
    ?challenge_window
    {
      tags = ["refutation"; "node"];
      variant;
      description = "refutation games winning strategies";
    }
  @@ fun _protocol sc_rollup_node sc_rollup_address node client ->
  let bootstrap1_key = Constant.bootstrap1.public_key_hash in
  let bootstrap2_key = Constant.bootstrap2.public_key_hash in

  let sc_rollup_node2 =
    Sc_rollup_node.create Operator node client ~default_operator:bootstrap2_key
  in
  let* _configuration_filename =
    Sc_rollup_node.config_init ~loser_mode sc_rollup_node2 sc_rollup_address
  in
  let* () = Sc_rollup_node.run sc_rollup_node
  and* () = Sc_rollup_node.run sc_rollup_node2 in
  let sc_client1 = Sc_rollup_client.create sc_rollup_node in

  let start_level = Node.get_level node in

  let stop_loser level =
    if List.mem level stop_loser_at then
      Sc_rollup_node.terminate sc_rollup_node2
    else return ()
  in

  let rec consume_inputs i = function
    | [] -> return ()
    | inputs :: next_batches as all ->
        let level = start_level + i in
        let* () = stop_loser level in
        if List.mem level empty_levels then
          let* () = Client.bake_for_and_wait client in
          consume_inputs (i + 1) all
        else
          let* () =
            Lwt_list.iter_s (send_text_messages client sc_rollup_address) inputs
          in
          let* () = Client.bake_for_and_wait client in
          consume_inputs (i + 1) next_batches
  in
  let* () = consume_inputs 0 inputs in
  let* after_inputs_level = Client.level client in

  let hook i =
    let level = after_inputs_level + i in
    stop_loser level
  in
  let* () = bake_levels ~hook (final_level - List.length inputs) client in

  let*! honest_deposit =
    RPC.Contracts.get_frozen_bonds ~contract_id:bootstrap1_key client
  in
  let*! loser_deposit =
    RPC.Contracts.get_frozen_bonds ~contract_id:bootstrap2_key client
  in
  let* {stake_amount; _} = get_sc_rollup_constants client in

  Check.(
    (JSON.as_int honest_deposit = Tez.to_mutez stake_amount)
      int
      ~error_msg:"expecting deposit for honest participant = %R, got %L") ;
  Check.(
    (JSON.as_int loser_deposit = 0)
      int
      ~error_msg:"expecting loss for dishonest participant = %R, got %L") ;
  Log.info "Checking that we can still retrieve state from rollup node" ;
  (* This is a way to make sure the rollup node did not crash *)
  let* _value = Sc_rollup_client.state_hash ~hooks sc_client1 in
  return ()

let rec swap i l =
  if i <= 0 then l
  else match l with [_] | [] -> l | x :: y :: l -> y :: swap (i - 1) (x :: l)

let inputs_for n =
  List.init n @@ fun i ->
  [swap i ["3 3 +"; "1"; "1 1 x"; "3 7 8 + * y"; "2 2 out"]]

let test_refutation protocols ~kind =
  let challenge_window = 10 in
  [
    ("inbox_proof_at_genesis", ("3 0 0", inputs_for 10, 80, [], []));
    ("pvm_proof_at_genesis", ("3 0 1", inputs_for 10, 80, [], []));
    ("inbox_proof", ("5 0 0", inputs_for 10, 80, [], []));
    ("inbox_proof_one_empty_level", ("6 0 0", inputs_for 10, 80, [2], []));
    ( "inbox_proof_many_empty_levels",
      ("9 0 0", inputs_for 10, 80, [2; 3; 4], []) );
    ("pvm_proof_0", ("5 0 1", inputs_for 10, 80, [], []));
    ("pvm_proof_1", ("7 1 2", inputs_for 10, 80, [], []));
    ("pvm_proof_2", ("7 2 5", inputs_for 7, 80, [], []));
    ("pvm_proof_3", ("9 2 5", inputs_for 7, 80, [4; 5], []));
    ("timeout", ("5 0 1", inputs_for 10, 80, [], [35]));
  ]
  |> List.iter (fun (variant, inputs) ->
         test_refutation_scenario
           ~kind
           ~challenge_window
           variant
           inputs
           protocols)

(** Helper to check that the operation whose hash is given is successfully
    included (applied) in the current head block. *)
let check_op_included =
  let get_op_status op =
    JSON.(op |-> "metadata" |-> "operation_result" |-> "status" |> as_string)
  in
  fun ~oph client ->
    let* head = RPC.Client.call client @@ RPC.get_chain_block () in
    (* Operations in a block are encoded as a list of lists of operations
       [ consensus; votes; anonymous; manager ]. Manager operations are
       at index 3 in the list. *)
    let ops = JSON.(head |-> "operations" |=> 3 |> as_list) in
    let op_contents =
      match
        List.find_opt (fun op -> oph = JSON.(op |-> "hash" |> as_string)) ops
      with
      | None -> []
      | Some op -> JSON.(op |-> "contents" |> as_list)
    in
    match op_contents with
    | [op] ->
        let status = get_op_status op in
        if String.equal status "applied" then unit
        else
          Test.fail
            ~__LOC__
            "Unexpected operation %s status: got %S instead of 'applied'."
            oph
            status
    | _ ->
        Test.fail
          "Expected to have one operation with hash %s, but got %d"
          oph
          (List.length op_contents)

(** Helper function that allows to inject the given operation in a node, bake a
    block, and check that the operation is successfully applied in the baked
    block. *)
let bake_operation_via_rpc client op =
  let* (`OpHash oph) = Operation.Manager.inject [op] client in
  let* () = Client.bake_for_and_wait client in
  check_op_included ~oph client

(** This helper function constructs the following commitment tree by baking and
    publishing commitments (but without cementing them):
    ---- c1 ---- c2 ---- c31 ---- c311
                  \
                   \---- c32 ---- c321

   Commits c1, c2, c31 and c311 are published by [operator1]. The forking
   branch c32 -- c321 is published by [operator2].
*)
let mk_forking_commitments node client ~sc_rollup ~operator1 ~operator2 =
  let* {commitment_period_in_blocks; _} = get_sc_rollup_constants client in
  (* This is the starting level on top of wich we'll construct the tree. *)
  let starting_level = Node.get_level node in
  let mk_commit ~src ~ticks ~depth ~pred =
    (* Compute the inbox level for which we'd like to commit *)
    let inbox_level = starting_level + (commitment_period_in_blocks * depth) in
    (* d is the delta between the target inbox level and the current level *)
    let d = inbox_level - Node.get_level node in
    (* Bake sufficiently many blocks to be able to commit for the desired inbox
       level. We may actually bake no blocks if d <= 0 *)
    let* () = repeat d (fun () -> Client.bake_for_and_wait client) in
    publish_dummy_commitment
      ~inbox_level
      ~predecessor:pred
      ~sc_rollup
      ~number_of_ticks:ticks
      ~src
      client
  in
  (* Retrieve the latest commitment *)
  let* c0, _ = last_cemented_commitment_hash_with_level ~sc_rollup client in
  (* Construct the tree of commitments. Fork c32 and c321 is published by
     operator2. We vary ticks to have different hashes when commiting on top of
     the same predecessor. *)
  let* c1 = mk_commit ~ticks:1 ~depth:1 ~pred:c0 ~src:operator1 in
  let* c2 = mk_commit ~ticks:2 ~depth:2 ~pred:c1 ~src:operator1 in
  let* c31 = mk_commit ~ticks:31 ~depth:3 ~pred:c2 ~src:operator1 in
  let* c32 = mk_commit ~ticks:32 ~depth:3 ~pred:c2 ~src:operator2 in
  let* c311 = mk_commit ~ticks:311 ~depth:4 ~pred:c31 ~src:operator1 in
  let* c321 = mk_commit ~ticks:321 ~depth:4 ~pred:c32 ~src:operator2 in
  return (c1, c2, c31, c32, c311, c321)

(** This helper initializes a rollup and builds a commitment tree of the form:
    ---- c1 ---- c2 ---- c31 ---- c311
                  \
                   \---- c32 ---- c321
    Then, it calls the given scenario on it.
*)
let test_forking_scenario ~title ~scenario protocols =
  regression_test
    ~__FILE__
    ~tags:["l1"; "commitment"; "cement"; "fork"; "dispute"]
    title
    (fun protocol ->
      (* Choosing challenge_windows to be quite longer than commitment_period
         to avoid being in a situation where the first commitment in the result
         of [mk_forking_commitments] is cementable without further bakes. *)
      let commitment_period = 3 in
      let challenge_window = commitment_period * 7 in
      (* Completely arbitrary as we decide when to trigger timeouts in tests.
         Making it a lot smaller than the default value to speed up tests. *)
      let timeout = 10 in
      setup ~commitment_period ~challenge_window ~timeout ~protocol
      @@ fun node client _bootstrap1_key ->
      (* Originate a Sc rollup. *)
      let* sc_rollup = originate_sc_rollup client ~parameters_ty:"unit" in
      (* Building a forking commitments tree. *)
      let operator1 = Constant.bootstrap1 in
      let operator2 = Constant.bootstrap2 in
      let level0 = Node.get_level node in
      let* commits =
        mk_forking_commitments
          node
          client
          ~sc_rollup
          ~operator1:operator1.public_key_hash
          ~operator2:operator2.public_key_hash
      in
      let level1 = Node.get_level node in
      scenario
        client
        node
        ~sc_rollup
        ~operator1
        ~operator2
        commits
        level0
        level1)
    protocols

(* A more convenient wrapper around [cement_commitment]. *)
let cement_commitments client sc_rollup ?fail =
  Lwt_list.iter_s (fun hash -> cement_commitment client ~sc_rollup ~hash ?fail)

(** Given a commitment tree constructed by {test_forking_scenario}, this function:
    - tests different (failing and non-failing) cementation of commitments
      and checks the returned error for each situation (in case of failure);
    - resolves the dispute on top of c2, and checks that the defeated branch
      is removed, while the alive one can be cemented.
*)
let test_no_cementation_if_parent_not_lcc_or_if_disputed_commit protocols =
  test_forking_scenario
    ~title:
      "commitments: publish, and try to cement not on top of LCC or disputed"
    ~scenario:
      (fun client _node ~sc_rollup ~operator1 ~operator2 commits level0 level1 ->
      let c1, c2, c31, c32, c311, c321 = commits in
      let* constants = get_sc_rollup_constants client in
      let challenge_window = constants.challenge_window_in_blocks in

      let cement = cement_commitments client sc_rollup in
      let missing_blocks_to_cement = level0 + challenge_window - level1 in
      let* () =
        if missing_blocks_to_cement <= 0 then unit (* We can already cement *)
        else
          let* () =
            repeat (missing_blocks_to_cement - 1) (fun () ->
                Client.bake_for_and_wait client)
          in
          (* We cannot cement yet! *)
          let* () = cement [c1] ~fail:commit_too_recent in
          (* After these blocks, we should be able to cement all commitments
             (modulo cementation ordering & disputes resolution) *)
          repeat challenge_window (fun () -> Client.bake_for_and_wait client)
      in
      (* We cannot cement any of the commitments before cementing c1 *)
      let* () = cement [c2; c31; c32; c311; c321] ~fail:parent_not_lcc in
      (* But, we can cement c1 and then c2, in this order *)
      let* () = cement [c1; c2] in
      (* We cannot cement c31 or c32 on top of c2 because they are disputed *)
      let* () = cement [c31; c32] ~fail:disputed_commit in
      (* Of course, we cannot cement c311 or c321 because their parents are not
         cemented. *)
      let* () = cement ~fail:parent_not_lcc [c311; c321] in

      (* +++ dispute resolution +++
         Let's resolve the dispute between operator1 and operator2 on the fork
         c31 vs c32. [operator1] will make a bad initial dissection, so it
         loses the dispute, and the branch c32 --- c321 dies. *)

      (* [operator1] starts a dispute. *)
      let module M = Operation.Manager in
      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source:operator2
        @@ M.sc_rollup_refute ~sc_rollup ~opponent:operator1.public_key_hash ()
      in
      (* [operator1] makes a dissection. it will lose here because the dissection
         is ill-formed. *)
      let refutation = M.{choice_tick = 0; refutation_step = Dissection []} in
      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source:operator2
        @@ M.sc_rollup_refute
             ~sc_rollup
             ~opponent:operator1.public_key_hash
             ~refutation
             ()
      in
      (* Attempting to cement defeated branch will fail. *)
      let* () = cement ~fail:commit_doesnt_exit [c32; c321] in
      (* Now, we can cement c31 on top of c2 and c311 on top of c31. *)
      cement [c31; c311])
    protocols

(** Given a commitment tree constructed by {test_forking_scenario}, this test
    starts a dispute and makes a first valid dissection move.
*)
let test_valid_dispute_dissection protocols =
  test_forking_scenario
    ~title:"valid dispute dissection"
    ~scenario:
      (fun client _node ~sc_rollup ~operator1 ~operator2 commits _level0 _level1 ->
      let c1, c2, c31, c32, _c311, _c321 = commits in
      let cement = cement_commitments client sc_rollup in
      let* constants = get_sc_rollup_constants client in
      let challenge_window = constants.challenge_window_in_blocks in
      let commitment_period = constants.commitment_period_in_blocks in
      let number_of_sections_in_dissection =
        constants.number_of_sections_in_dissection
      in
      let* () =
        (* Be able to cement both c1 and c2 *)
        repeat (challenge_window + commitment_period) (fun () ->
            Client.bake_for_and_wait client)
      in
      let* () = cement [c1; c2] in
      let module M = Operation.Manager in
      (* The source initialises a dispute. *)
      let source = operator2 in
      let opponent = operator1.public_key_hash in
      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source
        @@ M.sc_rollup_refute ~sc_rollup ~opponent ()
      in
      (* Construct a valid dissection with valid initial hash of size
         [sc_rollup.number_of_sections_in_dissection]. The state hash below is
         the hash of the state computed after submitting the first commitment c1
         (which is also equal to states's hashes of subsequent commitments, as we
         didn't add any message in inboxes). If this hash needs to be recomputed,
         run this test with --verbose and grep for 'compressed_state' in the
         produced logs. *)
      let state_hash =
        "scs11VNjWyZw4Tgbvsom8epQbox86S2CKkE1UAZkXMM7Pj8MQMLzMf"
      in

      let rec aux i acc =
        if i = number_of_sections_in_dissection - 1 then
          List.rev ({M.state_hash = None; tick = i} :: acc)
        else aux (i + 1) ({M.state_hash = Some state_hash; tick = i} :: acc)
      in
      (* Inject a valid dissection move *)
      let refutation =
        M.{choice_tick = 0; refutation_step = Dissection (aux 0 [])}
      in

      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source
        @@ M.sc_rollup_refute ~sc_rollup ~opponent ~refutation ()
      in
      (* We cannot cement neither c31, nor c32 because refutation game hasn't
         ended. *)
      cement [c31; c32] ~fail:"Attempted to cement a disputed commitment")
    protocols

(* Testing rollup node catch up mechanism
   --------------------------------------

   The rollup node must be able to catch up from the genesis
   of the rollup when paired with a node in archive mode.
*)
let test_late_rollup_node =
  test_scenario
    {
      tags = ["node"];
      variant = "late";
      description = "a late rollup should catch up";
    }
  @@ fun _protocol sc_rollup_node _sc_rollup_address _node client ->
  let* () = bake_levels 65 client in
  let* () = Sc_rollup_node.run sc_rollup_node in
  let* () = bake_levels 30 client in
  let* _status = Sc_rollup_node.wait_for_level ~timeout:2. sc_rollup_node 95 in
  return ()

let timeout ?expect_failure ~sc_rollup ~staker client =
  let*! () =
    Client.Sc_rollup.timeout
      ~hooks
      ~dst:sc_rollup
      ~src:"bootstrap1"
      ~staker
      client
      ?expect_failure
  in
  Client.bake_for_and_wait client

(* Testing the timeout to record gas consumption in a regression trace and
   detect when the value changes.
   For functional tests on timing-out a dispute, see unit tests in
   [lib_protocol].

   For this test, we rely on [test_forking_scenario] to create a tree structure
   of commitments and we start a dispute.
   The first player is not even going to play, we'll simply bake enough blocks
   to get to the point where we can timeout. *)
let test_timeout protocols =
  test_forking_scenario
    ~title:"refutation game timeout"
    ~scenario:
      (fun client _node ~sc_rollup ~operator1 ~operator2 commits level0 level1 ->
      (* These are the commitments on the rollup. See [test_forking_scenario] to
         visualize the tree structure. *)
      let c1, c2, _c31, _c32, _c311, _c321 = commits in
      (* A helper function to cement a sequence of commitments. *)
      let cement = cement_commitments client sc_rollup in
      let* constants = get_sc_rollup_constants client in
      let challenge_window = constants.challenge_window_in_blocks in
      let timeout_period = constants.timeout_period_in_blocks in

      (* Bake enough blocks to cement the commitments up to the divergence. *)
      let* () =
        repeat
          (* There are [level0 - level1 - 1] blocks between [level1] and
             [level0], plus the challenge window for [c1] and the one for [c2].
          *)
          (level0 - level1 - 1 + (2 * challenge_window))
          (fun () -> Client.bake_for_and_wait client)
      in
      let* () = cement [c1; c2] in

      let module M = Operation.Manager in
      (* [operator2] starts a dispute, but won't be playing then. *)
      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source:operator2
        @@ M.sc_rollup_refute ~sc_rollup ~opponent:operator1.public_key_hash ()
      in
      (* Get exactly to the block where we are able to timeout. *)
      let* () =
        repeat (timeout_period + 1) (fun () -> Client.bake_for_and_wait client)
      in
      timeout ~sc_rollup ~staker:operator1.public_key_hash client)
    protocols

let test_refutation_reward_and_punishment protocols =
  regression_test
    ~__FILE__
    ~tags:["l1"; "refutation"; "stake"; "reward"; "punishment"]
    "refutation: check the punishment and reward"
    (fun protocol ->
      (* Timeout is the easiest way to end a game, we set timeout period
         low to produce an outcome quickly. *)
      let timeout_period = 3 in
      let commitment_period = 2 in
      setup ~commitment_period ~timeout:timeout_period ~protocol
      @@ fun node client _ ->
      let* {commitment_period_in_blocks; stake_amount; _} =
        get_sc_rollup_constants client
      in
      let punishment = Tez.to_mutez stake_amount in
      let reward = punishment / 2 in
      (* Originate a Sc rollup. *)
      let* sc_rollup = originate_sc_rollup client ~parameters_ty:"unit" in

      (* Pick the two players and their initial balances. *)
      let operator1 = Constant.bootstrap2 in
      let operator2 = Constant.bootstrap3 in

      let* operator1_balances =
        contract_balances ~pkh:operator1.public_key_hash client
      in
      let* operator2_balances =
        contract_balances ~pkh:operator2.public_key_hash client
      in

      (* Retrieve the origination commitment *)
      let* c0, _ = last_cemented_commitment_hash_with_level ~sc_rollup client in

      (* Compute the inbox level for which we'd like to commit *)
      let starting_level = Node.get_level node in
      let inbox_level = starting_level + commitment_period_in_blocks in
      (* d is the delta between the target inbox level and the current level *)
      let d = inbox_level - Node.get_level node in
      (* Bake sufficiently many blocks to be able to commit for the desired inbox
         level. We may actually bake no blocks if d <= 0 *)
      let* () = repeat d (fun () -> Client.bake_for_and_wait client) in

      (* [operator1] stakes on a commitment. *)
      let* _ =
        publish_dummy_commitment
          ~inbox_level
          ~predecessor:c0
          ~sc_rollup
          ~number_of_ticks:1
          ~src:operator1.public_key_hash
          client
      in
      let* new_operator1_balances =
        contract_balances ~pkh:operator1.public_key_hash client
      in

      Check.(
        (new_operator1_balances.frozen
        = operator1_balances.frozen + Tez.to_mutez stake_amount)
          int
          ~error_msg:"expecting frozen balance for operator1: %R, got %L") ;

      (* [operator2] stakes on a commitment. *)
      let* _ =
        publish_dummy_commitment
          ~inbox_level
          ~predecessor:c0
          ~sc_rollup
          ~number_of_ticks:2
          ~src:operator2.public_key_hash
          client
      in
      let* new_operator2_balances =
        contract_balances ~pkh:operator2.public_key_hash client
      in
      Check.(
        (new_operator2_balances.frozen
        = operator2_balances.frozen + Tez.to_mutez stake_amount)
          int
          ~error_msg:"expecting frozen balance for operator2: %R, got %L") ;

      let module M = Operation.Manager in
      (* [operator1] starts a dispute, but will never play. *)
      let* () =
        bake_operation_via_rpc client
        @@ M.make ~source:operator1
        @@ M.sc_rollup_refute ~sc_rollup ~opponent:operator2.public_key_hash ()
      in
      (* Get exactly to the block where we are able to timeout. *)
      let* () =
        repeat (timeout_period + 1) (fun () -> Client.bake_for_and_wait client)
      in
      let* () = timeout ~sc_rollup ~staker:operator2.public_key_hash client in

      (* The game should have now ended. *)

      (* [operator2] wins half of the opponent's stake. *)
      let* final_operator2_balances =
        contract_balances ~pkh:operator2.public_key_hash client
      in
      Check.(
        (final_operator2_balances.frozen = new_operator2_balances.frozen)
          int
          ~error_msg:"operator2 should keep its frozen deposit: %R, got %L") ;
      Check.(
        (final_operator2_balances.liquid
        = new_operator2_balances.liquid + reward)
          int
          ~error_msg:"operator2 should win a reward: %R, got %L") ;

      (* [operator1] loses all its stake. *)
      let* final_operator1_balances =
        contract_balances ~pkh:operator1.public_key_hash client
      in
      Check.(
        (final_operator1_balances.frozen
        = new_operator1_balances.frozen - punishment)
          int
          ~error_msg:"operator1 should lose its frozen deposit: %R, got %L") ;

      unit)
    protocols

let register ~kind ~protocols =
  test_origination ~kind protocols ;
  test_rollup_node_running ~kind protocols ;
  test_rollup_get_genesis_info ~kind protocols ;
  test_rollup_get_chain_block_context_sc_rollup_last_cemented_commitment_hash_with_level
    ~kind
    protocols ;
  test_rollup_inbox_size ~kind protocols ;
  test_rollup_inbox_current_messages_hash ~kind protocols ;
  test_rollup_inbox_of_rollup_node ~kind "basic" basic_scenario protocols ;
  (* See above at definition of sc_rollup_node_stops_scenario:

     test_rollup_inbox_of_rollup_node
      ~kind
      "stops"
      sc_rollup_node_stops_scenario
      protocols ;
  *)
  test_rollup_inbox_of_rollup_node
    ~kind
    "disconnects"
    sc_rollup_node_disconnects_scenario
    protocols ;
  test_rollup_inbox_of_rollup_node
    ~kind
    "handles_chain_reorg"
    sc_rollup_node_handles_chain_reorg
    protocols ;
  test_rollup_node_boots_into_initial_state protocols ~kind ;
  test_rollup_node_advances_pvm_state
    protocols
    ~kind
    ~boot_sector:None
    ~internal:false ;
  test_rollup_node_advances_pvm_state
    protocols
    ~kind
    ~boot_sector:None
    ~internal:true ;
  test_commitment_scenario
    "commitment_is_stored"
    commitment_stored
    protocols
    ~kind ;
  test_commitment_scenario
    "robust_to_failures"
    commitment_stored_robust_to_failures
    protocols
    ~kind ;
  test_commitment_scenario
    ~extra_tags:["modes"; "observer"]
    "observer_does_not_publish"
    (mode_publish Observer false)
    protocols
    ~kind ;
  test_commitment_scenario
    ~extra_tags:["modes"; "maintenance"]
    "maintenance_publishes"
    (mode_publish Maintenance true)
    protocols
    ~kind ;
  test_commitment_scenario
    ~extra_tags:["modes"; "batcher"]
    "batcher_does_not_publish"
    (mode_publish Batcher false)
    protocols
    ~kind ;
  test_commitment_scenario
    ~extra_tags:["modes"; "operator"]
    "operator_publishes"
    (mode_publish Operator true)
    protocols
    ~kind ;
  test_commitment_scenario
    ~commitment_period:15
    ~challenge_window:10080
    "node_use_proto_param"
    commitment_stored
    protocols
    ~kind ;
  test_commitment_scenario
    "non_final_level"
    commitment_not_stored_if_non_final
    protocols
    ~kind ;
  test_commitment_scenario
    "messages_reset"
    commitments_messages_reset
    protocols
    ~kind ;
  test_commitment_scenario
    "handles_chain_reorgs"
    commitments_reorgs
    protocols
    ~kind ;
  test_commitment_scenario
    ~challenge_window:1
    "no_commitment_publish_before_lcc"
    (* TODO: https://gitlab.com/tezos/tezos/-/issues/2976
       change tests so that we do not need to repeat custom parameters. *)
    commitment_before_lcc_not_published
    protocols
    ~kind ;
  test_commitment_scenario
    "first_published_at_level_global"
    first_published_level_is_global
    protocols
    ~kind ;
  test_consecutive_commitments protocols ~kind ;
  test_refutation protocols ~kind ;
  test_late_rollup_node protocols ~kind

let register ~protocols =
  (* PVM-independent tests. We still need to specify a PVM kind
     because the tezt will need to originate a rollup. However,
     the tezt will not test for PVM kind specific featued. *)
  test_rollup_client_gets_address protocols ~kind:"wasm_2_0_0" ;
  test_rollup_node_configuration protocols ~kind:"wasm_2_0_0" ;
  test_rollup_list protocols ~kind:"wasm_2_0_0" ;
  test_rollup_client_show_address protocols ~kind:"wasm_2_0_0" ;
  test_rollup_client_generate_keys protocols ~kind:"wasm_2_0_0" ;
  test_rollup_client_list_keys protocols ~kind:"wasm_2_0_0" ;
  (* Specific Arith PVM tezts *)
  test_rollup_arith_origination_boot_sector protocols ;
  test_rollup_node_uses_arith_boot_sector protocols ;
  (* Specific Wasm PVM tezts *)
  (* TODO: https://gitlab.com/tezos/tezos/-/issues/3772
     test_rollup_node_run_with_kernel
        protocols
        ~kind:"wasm_2_0_0"
        ~kernel_name:"computation"
        ~internal:false ;
     test_rollup_node_run_with_kernel
       protocols
       ~kind:"wasm_2_0_0"
       ~kernel_name:"no_parse_random"
       ~internal:false ;
     test_rollup_node_run_with_kernel
       protocols
       ~kind:"wasm_2_0_0"
       ~kernel_name:"no_parse_bad_fingerprint"
       ~internal:false ;
  *)
  (* Shared tezts - will be executed for both PVMs. *)
  register ~kind:"wasm_2_0_0" ~protocols ;
  register ~kind:"arith" ~protocols ;
  test_no_cementation_if_parent_not_lcc_or_if_disputed_commit protocols ;
  test_valid_dispute_dissection protocols ;
  test_timeout protocols ;
  test_refutation_reward_and_punishment protocols
back to top