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

let originate_new_rollup ?(alias = "rollup")
    ?(boot_sector = Constant.wasm_echo_kernel_boot_sector)
    ?(parameters_ty = "bytes") ~src client =
  let* rollup =
    Client.Sc_rollup.originate
      client
      ~wait:"2"
      ~alias
      ~src
      ~kind:"wasm_2_0_0"
      ~parameters_ty
      ~boot_sector
      ~burn_cap:(Tez.of_int 2)
  in
  Log.info "Rollup %s originated" rollup ;
  return rollup

let setup_l2_node ?preimages_dir ?(mode = Sc_rollup_node.Operator) ?runner ?name
    ?loser_mode ?(log_kernel_debug = false) ?operator ?metrics_port client node
    rollup =
  let rollup_node =
    Sc_rollup_node.create
      ?runner
      ?name
      ?loser_mode
      ~base_dir:(Client.base_dir client)
      ?default_operator:operator
      ?metrics_port
      mode
      node
  in
  let* () =
    match preimages_dir with
    | None -> unit
    | Some dir ->
        let* _ =
          Lwt_unix.system
            ("cp -r " ^ dir ^ " "
            ^ (Sc_rollup_node.data_dir rollup_node // "wasm_2_0_0"))
        in
        unit
  in
  let* _ = Sc_rollup_node.config_init rollup_node rollup in
  Log.info "Starting a smart rollup node to track %s" rollup ;
  let* () =
    Sc_rollup_node.run
      rollup_node
      rollup
      (if log_kernel_debug then Sc_rollup_node.[Log_kernel_debug] else [])
  in
  let* () = Sc_rollup_node.wait_for_ready rollup_node in
  Log.info "Smart rollup node started." ;
  return rollup_node

let game_in_progress ~staker rollup_address client =
  let* game =
    Client.RPC.call client
    @@ RPC.get_chain_block_context_smart_rollups_smart_rollup_staker_games
         ~staker
         rollup_address
         ()
  in
  return (not ([] = JSON.as_list game))

let rec wait_for_game ~staker rollup_address client node =
  let* in_progress = game_in_progress ~staker rollup_address client in
  if not in_progress then (
    let* current_level = Node.get_level node in
    Log.info "Still no game at level %d" current_level ;
    let* _ = Node.wait_for_level node (current_level + 1) in
    wait_for_game ~staker rollup_address client node)
  else unit

let rec wait_for_end_of_game ~staker rollup_address client node =
  let* in_progress = game_in_progress ~staker rollup_address client in
  if in_progress then (
    let* current_level = Node.get_level node in
    Log.info "Game still in progress at level %d" current_level ;
    let* _ = Node.wait_for_level node (current_level + 1) in
    wait_for_end_of_game ~staker rollup_address client node)
  else unit

let rejection_with_proof ~(testnet : unit -> Testnet.t) () =
  (* We expect each player to have at least 11,000 xtz. This is enough
     to originate a rollup (1.68 xtz for one of the player), commit
     (10,000 xtz for both player), and play the game (each
     [Smart_rollup_refute] operation should be relatively cheap). *)
  let testnet = testnet () in
  let min_balance = Tez.(of_mutez_int 11_000_000_000) in
  let* client, node = Helpers.setup_octez_node ~testnet () in
  let* honest_operator = Client.gen_and_show_keys client in
  let* dishonest_operator = Client.gen_and_show_keys client in
  let* () =
    Lwt.join
      [
        Helpers.wait_for_funded_key node client min_balance honest_operator;
        Helpers.wait_for_funded_key node client min_balance dishonest_operator;
      ]
  in
  let* rollup_address =
    originate_new_rollup ~src:honest_operator.alias client
  in
  let* level = Node.get_level node in
  let fault_level = level + 5 in
  Log.info
    "Dishonest operator expected to inject an error at level %d"
    fault_level ;
  let* _rollup_nodes =
    Lwt.all
      [
        setup_l2_node
          ~name:"honest-node"
          ~operator:honest_operator.alias
          client
          node
          rollup_address;
        setup_l2_node
          ~name:"dishonest-node"
          ~loser_mode:Format.(sprintf "%d 0 0" fault_level)
          ~operator:dishonest_operator.alias
          client
          node
          rollup_address;
      ]
  in
  let* () =
    wait_for_game
      ~staker:honest_operator.public_key_hash
      rollup_address
      client
      node
  in
  let* () =
    wait_for_end_of_game
      ~staker:honest_operator.public_key_hash
      rollup_address
      client
      node
  in
  (* TODO: https://gitlab.com/tezos/tezos/-/issues/4929
     Should the scenario checks if the game ended with the expected
     result? *)
  unit

let send_message_client ?hooks ?(src = Constant.bootstrap2.alias) client msg =
  Client.Sc_rollup.send_message ?hooks ~src ~msg client

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

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

let send_text_messages ?(format = `Raw) ?hooks ?src client msgs =
  match format with
  | `Raw -> send_message_client ?hooks ?src client (to_text_messages_arg msgs)
  | `Hex -> send_message_client ?hooks ?src client (to_hex_messages_arg msgs)

(** Wait for the [sc_rollup_node_publish_execute_whitelist_update]
    event from the rollup node. *)
let wait_for_publish_execute_whitelist_update node =
  Sc_rollup_node.wait_for
    node
    "smart_rollup_node_publish_execute_whitelist_update.v0"
  @@ fun json ->
  let hash = JSON.(json |-> "hash" |> as_string) in
  let outbox_level = JSON.(json |-> "outbox_level" |> as_int) in
  let index = JSON.(json |-> "message_index" |> as_int) in
  Some (hash, outbox_level, index)

let get_or_gen_keys ~alias client =
  let process = Client.spawn_show_address ~alias client in
  let* status = Process.wait process in
  if status = Unix.WEXITED 0 then
    let* client_output = Process.check_and_read_stdout process in
    return @@ Account.parse_client_output ~alias ~client_output
  else Client.gen_and_show_keys ~alias client

let gen_tx_cmd cmd =
  Process.spawn
    ~name:"tx_gen"
    "node"
    ("etherlink/kernel_evm/benchmarks/transaction_generator.js" :: cmd)

let gen_accounts n ~output =
  let value = gen_tx_cmd ["gen_accounts"; string_of_int n; output] in
  Runnable.{value; run = Process.check}

let gen_config ~accounts_file ~output =
  let value = gen_tx_cmd ["gen_config"; accounts_file; output] in
  Runnable.{value; run = Process.check}

let gen_transactions rollup_address nonce ~accounts_file =
  let value =
    gen_tx_cmd
      ["gen_transactions"; accounts_file; rollup_address; string_of_int nonce]
  in
  Runnable.{value; run = Process.check_and_read_stdout}
  |> Runnable.map @@ fun output ->
     JSON.(
       parse
         ~origin:"gen_transactions"
         (String.trim output
         |> String.map (function '\n' -> ' ' | '\'' -> '"' | c -> c))
       |> as_list |> List.map as_string)

(** A smart rollup test, using latest evm kernel. It starts 1 operator
    rollup node, 2 batcher node, 1 accuser and 1 observer. It creates
    transactions sent to either batcher or directly to the chain at
    each block.

    This test depends a javascript script that has the following
    dependencies: `@warren-bank/ethereumjs-tx-sign ethers js-yaml`

*)
let simple_use_case_rollup ~(testnet : unit -> Testnet.t) () =
  let nb_of_l2_accounts = 10 in
  let accounts_file = Temp.file "accounts.yaml" in
  let*! () = gen_accounts nb_of_l2_accounts ~output:accounts_file in
  let config_file = Temp.file "config.yaml" in
  let*! () = gen_config ~accounts_file ~output:config_file in
  let preimages_dir = Temp.dir "preimages" in
  let* {boot_sector; _} =
    Sc_rollup_helpers.prepare_installer_kernel
      ~preimages_dir
      ~config:(`Path config_file)
      Constant.WASM.evm_kernel
  in
  let testnet = testnet () in
  let min_balance_operating = Tez.(of_mutez_int 11_000_000_000) in
  let min_balance_batching = Tez.(of_mutez_int 1_000_000) in
  let* client, node = Helpers.setup_octez_node ~testnet () in
  let* msg_sender = get_or_gen_keys ~alias:"msg_sender" client in
  let* operator1 = get_or_gen_keys ~alias:"operator1" client in
  let* operator2 = get_or_gen_keys ~alias:"operator2" client in
  let* batcher1 = get_or_gen_keys ~alias:"batcher1" client in
  let* batcher2 = get_or_gen_keys ~alias:"batcher2" client in
  let* accuser = get_or_gen_keys ~alias:"accuser" client in
  let* () =
    Lwt.join
      [
        Helpers.wait_for_funded_key node client min_balance_operating operator1;
        Helpers.wait_for_funded_key node client min_balance_operating operator2;
        Helpers.wait_for_funded_key node client min_balance_operating accuser;
        Helpers.wait_for_funded_key node client min_balance_batching batcher1;
        Helpers.wait_for_funded_key node client min_balance_batching batcher2;
        Helpers.wait_for_funded_key node client min_balance_batching msg_sender;
      ]
  in
  let rollup_alias = "evm_rollup_simple_case" in
  let* rollup_address =
    originate_new_rollup
      ~boot_sector
      ~alias:rollup_alias
      ~src:operator2.alias
      client
  in
  let* operator_node1 =
    setup_l2_node
      ~name:"rollup-operator1"
      ~mode:Sc_rollup_node.Operator
      ~operator:operator1.alias
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let* operator_node2 =
    setup_l2_node
      ~name:"rollup-operator2"
      ~mode:Sc_rollup_node.Operator
      ~operator:operator2.alias
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let* batcher_node1 =
    setup_l2_node
      ~name:"rollup-batcher1"
      ~mode:Sc_rollup_node.Batcher
      ~operator:batcher1.alias
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let* batcher_node2 =
    setup_l2_node
      ~name:"rollup-batcher2"
      ~mode:Sc_rollup_node.Batcher
      ~operator:batcher2.alias
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let* accuser_node =
    setup_l2_node
      ~name:"rollup-accuser"
      ~mode:Sc_rollup_node.Accuser
      ~operator:accuser.alias
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let* observer_node =
    setup_l2_node
      ~name:"rollup-observer"
      ~mode:Sc_rollup_node.Observer
      ~preimages_dir
      client
      node
      rollup_alias
  in
  let wait_sync node =
    let* _level = Sc_rollup_node.wait_sync ~timeout:30. node in
    unit
  in
  let wait_and_sync_all level =
    let* level = Node.wait_for_level node (level + 1)
    and* () =
      Lwt.join
        [
          wait_sync operator_node1;
          wait_sync operator_node2;
          wait_sync batcher_node1;
          wait_sync batcher_node2;
          wait_sync observer_node;
          wait_sync accuser_node;
        ]
    in
    return level
  in
  let level = Node.get_last_seen_level node in
  let* level = wait_and_sync_all level in
  let send_cmd send_where messages =
    match send_where with
    | `Batcher1 ->
        let* _hashes =
          Sc_rollup_node.RPC.call batcher_node1
          @@ Sc_rollup_rpc.post_local_batcher_injection ~messages
        in
        unit
    | `Batcher2 ->
        let* _hashes =
          Sc_rollup_node.RPC.call batcher_node2
          @@ Sc_rollup_rpc.post_local_batcher_injection ~messages
        in
        unit
    | `Node ->
        let* () =
          send_text_messages ~format:`Hex ~src:msg_sender.alias client messages
        in
        unit
  in
  (* number of blocks per week with 7 sec block time ~= 86_400 *)
  let number_of_blocks = 86_400 in
  let* _level =
    fold number_of_blocks (level, `Batcher1) @@ fun nonce (level, send_where) ->
    let* level = wait_and_sync_all level in
    let*! transactions = gen_transactions rollup_address nonce ~accounts_file in
    let* () = send_cmd send_where transactions in
    let next_send_where =
      match send_where with
      | `Batcher1 -> `Batcher2
      | `Batcher2 -> `Node
      | `Node -> `Batcher1
    in
    return (level, next_send_where)
  in
  unit

let register ~testnet =
  Test.register
    ~__FILE__
    ~title:"Rejection with proof"
    ~tags:["rejection"]
    (rejection_with_proof ~testnet) ;
  Test.register
    ~__FILE__
    ~title:"Simple rollup use case"
    ~tags:["rollup"; "accuser"; "node"; "batcher"]
    (simple_use_case_rollup ~testnet)
back to top