Raw File
node_run_command.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com>     *)
(* Copyright (c) 2019-2021 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

type error += Non_private_sandbox of P2p_addr.t

type error += RPC_Port_already_in_use of P2p_point.Id.t list

type error += Invalid_sandbox_file of string

let () =
  register_error_kind
    `Permanent
    ~id:"main.run.non_private_sandbox"
    ~title:"Forbidden public sandbox"
    ~description:"A sandboxed node should not listen on a public address."
    ~pp:(fun ppf addr ->
      Format.fprintf
        ppf
        "The node is configured to listen on a public address (%a), while only \
         'private' networks are authorised with `--sandbox`.\n\
        \           See `%s run --help` on how to change the listening address."
        Ipaddr.V6.pp
        addr
        Sys.argv.(0))
    Data_encoding.(obj1 (req "addr" P2p_addr.encoding))
    (function Non_private_sandbox addr -> Some addr | _ -> None)
    (fun addr -> Non_private_sandbox addr) ;
  register_error_kind
    `Permanent
    ~id:"main.run.port_already_in_use"
    ~title:"Cannot start node: RPC port already in use"
    ~description:"Another tezos node is probably running on the same RPC port."
    ~pp:(fun ppf addrlist ->
      Format.fprintf
        ppf
        "Another tezos node is probably running on one of these addresses \
         (%a). Please choose another RPC port."
        (Format.pp_print_list P2p_point.Id.pp)
        addrlist)
    Data_encoding.(obj1 (req "addrlist" (list P2p_point.Id.encoding)))
    (function RPC_Port_already_in_use addrlist -> Some addrlist | _ -> None)
    (fun addrlist -> RPC_Port_already_in_use addrlist) ;
  register_error_kind
    `Permanent
    ~id:"main.run.invalid_sandbox_file"
    ~title:"Invalid sandbox file"
    ~description:"The provided sandbox file is not a valid sandbox JSON file."
    ~pp:(fun ppf s ->
      Format.fprintf ppf "The file '%s' is not a valid JSON sandbox file" s)
    Data_encoding.(obj1 (req "sandbox_file" string))
    (function Invalid_sandbox_file s -> Some s | _ -> None)
    (fun s -> Invalid_sandbox_file s)

module Event = struct
  include Internal_event.Simple

  let section = ["node"; "main"]

  let disabled_discovery_addr =
    declare_0
      ~section
      ~name:"disabled_discovery_addr"
      ~msg:"disabled local peer discovery"
      ~level:Notice
      ()

  let disabled_listen_addr =
    declare_0
      ~section
      ~name:"disabled_listen_addr"
      ~msg:"disabled P2P server"
      ~level:Notice
      ()

  let disabled_config_validation =
    declare_0
      ~section
      ~name:"disabled_config_validation"
      ~msg:"disabled node configuration validation"
      ~level:Warning
      ()

  let starting_local_rpc_server =
    declare_3
      ~section
      ~name:"starting_local_rpc_server"
      ~msg:"starting local RPC server on {host}:{port} (acl = {acl_policy})"
      ~level:Notice
      ("host", Data_encoding.string)
      ("port", Data_encoding.uint16)
      ("acl_policy", Data_encoding.string)

  let starting_internal_rpc_server =
    declare_0
      ~section
      ~name:"starting_internal_rpc_server"
      ~msg:"starting internal RPC server"
      ~level:Info
      ()

  let starting_metrics_server =
    declare_2
      ~section
      ~name:"starting_metrics_server"
      ~msg:"starting metrics server on {host}:{port}"
      ~level:Notice
      ("host", Data_encoding.string)
      ("port", Data_encoding.uint16)

  let starting_node =
    declare_3
      ~section
      ~name:"starting_node"
      ~msg:"starting the Tezos node v{version} ({git_info})"
      ~level:Notice
      ("chain", Distributed_db_version.Name.encoding)
      ~pp2:Tezos_version.Version.pp
      ("version", Tezos_version.Node_version.version_encoding)
      ("git_info", Data_encoding.string)

  let node_is_ready =
    declare_0
      ~section
      ~name:"node_is_ready"
      ~msg:"the Tezos node is now running"
      ~level:Notice
      ()

  let shutting_down_node =
    declare_0
      ~section
      ~name:"shutting_down_node"
      ~msg:"shutting down the Tezos node"
      ~level:Notice
      ()

  let shutting_down_local_rpc_server =
    declare_0
      ~section
      ~name:"shutting_down_local_rpc_server"
      ~msg:"shutting down the local RPC server"
      ~level:Info
      ()

  let bye =
    (* Note that "exit_code" may be negative in case of signals. *)
    declare_1
      ~section
      ~name:"bye"
      ~msg:"bye"
      ~level:Notice
      (* may be negative in case of signals *)
      ("exit_code", Data_encoding.int31)

  let metrics_ended =
    declare_1
      ~section
      ~name:"metrics_ended"
      ~level:Error
      ~msg:"metrics server ended with error {stacktrace}"
      ("stacktrace", Data_encoding.string)

  let incorrect_history_mode =
    declare_2
      ~section
      ~name:"incorrect_history_mode"
      ~msg:
        "The given history mode {given_history_mode} does not correspond to \
         the stored history mode {stored_history_mode}. If you wish to force \
         the switch, use the flag '--force-history-mode-switch'."
      ~level:Error
      ~pp1:History_mode.pp
      ("given_history_mode", History_mode.encoding)
      ~pp2:History_mode.pp
      ("stored_history_mode", History_mode.encoding)
end

open Filename.Infix

let init_identity_file (config : Config_file.t) =
  let check_data_dir ~data_dir =
    let dummy_genesis =
      {
        Genesis.time = Time.Protocol.epoch;
        block = Block_hash.zero;
        protocol = Protocol_hash.zero;
      }
    in
    Data_version.ensure_data_dir ~mode:Exists dummy_genesis data_dir
  in
  let identity_file =
    config.data_dir // Data_version.default_identity_file_name
  in
  Identity_file.init
    ~check_data_dir
    ~identity_file
    ~expected_pow:config.p2p.expected_pow

let init_node ?sandbox ?target ~identity ~singleprocess ~internal_events
    ~force_history_mode_switch (config : Config_file.t) =
  let open Lwt_result_syntax in
  (* TODO "WARN" when pow is below our expectation. *)
  let*! () =
    if config.disable_config_validation then
      Event.(emit disabled_config_validation) ()
    else Lwt.return_unit
  in
  let* discovery_addr, discovery_port =
    match config.p2p.discovery_addr with
    | None ->
        let*! () = Event.(emit disabled_discovery_addr) () in
        return (None, None)
    | Some addr -> (
        let* addrs = Config_file.resolve_discovery_addrs addr in
        match addrs with
        | [] -> failwith "Cannot resolve P2P discovery address: %S" addr
        | (addr, port) :: _ -> return (Some addr, Some port))
  in
  let* listening_addr, listening_port =
    match config.p2p.listen_addr with
    | None ->
        let*! () = Event.(emit disabled_listen_addr) () in
        return (None, None)
    | Some addr -> (
        let* addrs = Config_file.resolve_listening_addrs addr in
        match addrs with
        | [] -> failwith "Cannot resolve P2P listening address: %S" addr
        | (addr, port) :: _ -> return (Some addr, Some port))
  in
  let* p2p_config =
    match (listening_addr, sandbox) with
    | Some addr, Some _ when Ipaddr.V6.(compare addr unspecified) = 0 ->
        return_none
    | Some addr, Some _ when not (Ipaddr.V6.is_private addr) ->
        tzfail (Non_private_sandbox addr)
    | None, Some _ -> return_none
    | _ ->
        let* trusted_points =
          Config_file.resolve_bootstrap_addrs
            (Config_file.bootstrap_peers config)
        in
        let advertised_port : P2p_addr.port option =
          Option.either config.p2p.advertised_net_port listening_port
        in
        let p2p_config : P2p.config =
          {
            listening_addr;
            listening_port;
            advertised_port;
            discovery_addr;
            discovery_port;
            trusted_points;
            peers_file = config.data_dir // Data_version.default_peers_file_name;
            private_mode = config.p2p.private_mode;
            reconnection_config = config.p2p.reconnection_config;
            identity;
            proof_of_work_target =
              Tezos_crypto.Crypto_box.make_pow_target config.p2p.expected_pow;
            trust_discovered_peers = sandbox <> None;
            disable_peer_discovery = config.p2p.disable_peer_discovery;
          }
        in
        return_some (p2p_config, config.p2p.limits)
  in
  let* sandbox_param =
    match (config.blockchain_network.genesis_parameters, sandbox) with
    | None, None -> return_none
    | Some parameters, None ->
        return_some (parameters.context_key, parameters.values)
    | _, Some filename ->
        let* json =
          trace (Invalid_sandbox_file filename)
          @@ Lwt_utils_unix.Json.read_file filename
        in
        return_some ("sandbox_parameter", json)
  in
  let genesis = config.blockchain_network.genesis in
  let patch_context =
    Some (Patch_context.patch_context genesis sandbox_param)
  in
  let node_config : Node.config =
    {
      genesis;
      chain_name = config.blockchain_network.chain_name;
      sandboxed_chain_name = config.blockchain_network.sandboxed_chain_name;
      user_activated_upgrades =
        config.blockchain_network.user_activated_upgrades;
      user_activated_protocol_overrides =
        config.blockchain_network.user_activated_protocol_overrides;
      operation_metadata_size_limit =
        config.shell.block_validator_limits.operation_metadata_size_limit;
      patch_context;
      data_dir = config.data_dir;
      internal_events;
      store_root = Data_version.store_dir config.data_dir;
      context_root = Data_version.context_dir config.data_dir;
      protocol_root = Data_version.protocol_dir config.data_dir;
      p2p = p2p_config;
      target;
      enable_testchain = config.p2p.enable_testchain;
      disable_mempool = config.p2p.disable_mempool;
      dal_config = config.blockchain_network.dal_config;
    }
  in
  let* () =
    match config.shell.history_mode with
    | Some history_mode when force_history_mode_switch ->
        Store.may_switch_history_mode
          ~store_dir:node_config.store_root
          ~context_dir:node_config.context_root
          genesis
          ~new_history_mode:history_mode
    | _ -> return_unit
  in
  let version =
    Tezos_version.Version.to_string Tezos_version_value.Current_git_info.version
  in
  let commit_info =
    ({
       commit_hash = Tezos_version_value.Current_git_info.commit_hash;
       commit_date = Tezos_version_value.Current_git_info.committer_date;
     }
      : Tezos_version.Node_version.commit_info)
  in
  Node.create
    ~sandboxed:(sandbox <> None)
    ?sandbox_parameters:(Option.map snd sandbox_param)
    ~singleprocess
    ~version
    ~commit_info
    node_config
    config.shell.peer_validator_limits
    config.shell.block_validator_limits
    config.shell.prevalidator_limits
    config.shell.chain_validator_limits
    config.shell.history_mode

let rpc_metrics =
  Prometheus.Summary.v_labels
    ~label_names:["endpoint"; "method"]
    ~help:"RPC endpoint call counts and sum of execution times."
    ~namespace:Tezos_version.Node_version.namespace
    ~subsystem:"rpc"
    "calls"

module Metrics_server = Prometheus_app.Cohttp (Cohttp_lwt_unix.Server)

type port = int

type socket_file = string

type single_server_kind =
  | Process of socket_file
  | Local of Conduit_lwt_unix.server * port

let extract_mode = function
  | Process socket_file -> `Unix_domain_socket (`File socket_file)
  | Local (mode, _) -> mode

(* Add default accepted CORS headers *)
let sanitize_cors_headers ~default headers =
  List.map String.lowercase_ascii headers
  |> String.Set.of_list
  |> String.Set.(union (of_list default))
  |> String.Set.elements

(* Launches an RPC server depending on the given server kind *)
let launch_rpc_server (config : Config_file.t) dir rpc_server_kind addr =
  let open Lwt_result_syntax in
  let rpc_config = config.rpc in
  let media_types = rpc_config.media_type in
  let host = Ipaddr.V6.to_string addr in
  let* acl =
    (* Also emits events depending on server kind *)
    match rpc_server_kind with
    | Process _ ->
        let*! () = Event.(emit starting_internal_rpc_server) () in
        return_none
    | Local (mode, port) ->
        let*! acl_policy = RPC_server.Acl.resolve_domain_names rpc_config.acl in
        let acl =
          let open RPC_server.Acl in
          find_policy acl_policy (Ipaddr.V6.to_string addr, Some port)
          |> Option.value_f ~default:(fun () -> default addr)
        in
        let*! () =
          match (mode : Conduit_lwt_unix.server) with
          | `TCP _ | `TLS _ | `Unix_domain_socket _ ->
              Event.(emit starting_local_rpc_server)
                (host, port, RPC_server.Acl.policy_type acl)
          | _ -> Lwt.return_unit
        in
        return_some acl
  in
  let cors =
    let cors_headers =
      sanitize_cors_headers ~default:["Content-Type"] rpc_config.cors_headers
    in
    Resto_cohttp.Cors.
      {
        allowed_origins = rpc_config.cors_origins;
        allowed_headers = cors_headers;
      }
  in
  let server =
    RPC_server.init_server
      ~cors
      ?acl
      ~media_types:(Media_type.Command_line.of_command_line media_types)
      dir
  in
  let callback (conn : Cohttp_lwt_unix.Server.conn) req body =
    let path = Cohttp.Request.uri req |> Uri.path in
    if path = "/metrics" then
      let*! response = Metrics_server.callback conn req body in
      Lwt.return (`Response response)
    else Tezos_rpc_http_server.RPC_server.resto_callback server conn req body
  in
  let update_metrics uri meth =
    Prometheus.Summary.(time (labels rpc_metrics [uri; meth]) Sys.time)
  in
  let callback =
    RPC_middleware.rpc_metrics_transform_callback ~update_metrics dir callback
  in
  let mode = extract_mode rpc_server_kind in
  Lwt.catch
    (fun () ->
      let*! () =
        RPC_server.launch
          ~host
          server
          ~callback
          ~max_active_connections:config.rpc.max_active_rpc_connections
          mode
      in
      return server)
    (function
      (* FIXME: https://gitlab.com/tezos/tezos/-/issues/1312
         This exception seems to be unreachable.
      *)
      | Unix.Unix_error (Unix.EADDRINUSE, "bind", "") as exn -> (
          match rpc_server_kind with
          | Process _ -> fail_with_exn exn
          | Local (_, port) -> tzfail (RPC_Port_already_in_use [(addr, port)]))
      | exn -> fail_with_exn exn)

(* Describes the kind of servers that can be handled by the node.
   - Local_rpc_server: RPC server is run by the node itself
     (this may block the node in case of heavy RPC load),
   - External_rpc_server: RPC server is spawned as an external
     process,
   - No_server: the node is not responding to any RPC. *)
type rpc_server_kind =
  | Local_rpc_server of RPC_server.server list
  | External_rpc_server of (RPC_server.server * Rpc_process_worker.t) list
  | No_server

(* Initializes an RPC server handled by the node main process. *)
let init_local_rpc_server (config : Config_file.t) dir =
  let open Lwt_result_syntax in
  let* servers =
    List.concat_map_es
      (fun addr ->
        let* addrs = Config_file.resolve_rpc_listening_addrs addr in
        match addrs with
        | [] -> failwith "Cannot resolve listening address: %S" addr
        | addrs ->
            List.map_es
              (fun (addr, port) ->
                let mode =
                  match config.rpc.tls with
                  | None -> `TCP (`Port port)
                  | Some {cert; key} ->
                      `TLS
                        ( `Crt_file_path cert,
                          `Key_file_path key,
                          `No_password,
                          `Port port )
                in
                launch_rpc_server config dir (Local (mode, port)) addr)
              addrs)
        (* For backward compatibility, and as the default behaviour is
           the local RPC server, we merge the listen_addrs and
           local_listen_addrs arguments. *)
      (config.rpc.listen_addrs @ config.rpc.local_listen_addrs)
  in
  return (Local_rpc_server servers)

let rpc_socket_path ~socket_dir ~id ~pid =
  let filename = Format.sprintf "octez-external-rpc-socket-%d-%d" pid id in
  Filename.concat socket_dir filename

(* Initializes an RPC server handled by the node process. It will be
   used by an external RPC process, identified by [id], to forward
   RPCs to the node through a Unix socket. *)
let init_local_rpc_server_for_external_process id (config : Config_file.t) dir
    addr =
  let open Lwt_result_syntax in
  let socket_dir = Tezos_base_unix.Socket.get_temporary_socket_dir () in
  let pid = Unix.getpid () in
  let comm_socket_path = rpc_socket_path ~id ~socket_dir ~pid in
  (* Register a clean up callback to clean the comm_socket_path when
     shutting down. Indeed, the socket file is created by the
     Conduit-lwt-unix.Conduit_lwt_server.listen function, but the
     resource is not cleaned. *)
  let _ =
    Lwt_exit.register_clean_up_callback ~loc:__LOC__ (fun _ ->
        Lwt_unix.unlink comm_socket_path)
  in
  let* rpc_server =
    launch_rpc_server config dir (Process comm_socket_path) addr
  in
  return (rpc_server, comm_socket_path)

let init_external_rpc_server config node_version dir internal_events =
  let open Lwt_result_syntax in
  (* Start one rpc_process for each rpc endpoint. *)
  let id = ref 0 in
  let* rpc_servers =
    List.concat_map_ep
      (fun addr ->
        let* addrs = Config_file.resolve_rpc_listening_addrs addr in
        match addrs with
        | [] -> failwith "Cannot resolve listening address: %S" addr
        | addrs ->
            List.map_ep
              (fun (p2p_point : P2p_point.Id.t) ->
                let id =
                  let curid = !id in
                  incr id ;
                  curid
                in
                let* local_rpc_server, comm_socket_path =
                  init_local_rpc_server_for_external_process
                    id
                    config
                    dir
                    (fst p2p_point)
                in
                let addr = P2p_point.Id.to_string p2p_point in
                (* Update the config sent to the rpc_process to
                   start so that it contains a single listen
                   address. *)
                let config =
                  {
                    config with
                    rpc = {config.rpc with external_listen_addrs = [addr]};
                  }
                in
                let rpc_process =
                  Octez_rpc_process.Rpc_process_worker.create
                    ~comm_socket_path
                    config
                    node_version
                    internal_events
                in
                let* () =
                  Octez_rpc_process.Rpc_process_worker.start rpc_process
                in
                return (local_rpc_server, rpc_process))
              addrs)
      config.rpc.external_listen_addrs
  in
  return (External_rpc_server rpc_servers)

let metrics_serve metrics_addrs =
  let open Lwt_result_syntax in
  let* addrs = List.map_ep Config_file.resolve_metrics_addrs metrics_addrs in
  let*! servers =
    List.map_p
      (fun (addr, port) ->
        let host = Ipaddr.V6.to_string addr in
        let*! () = Event.(emit starting_metrics_server) (host, port) in
        let*! ctx = Conduit_lwt_unix.init ~src:host () in
        let ctx = Cohttp_lwt_unix.Net.init ~ctx () in
        let mode = `TCP (`Port port) in
        let callback = Metrics_server.callback in
        Cohttp_lwt_unix.Server.create
          ~ctx
          ~mode
          (Cohttp_lwt_unix.Server.make ~callback ()))
      (List.flatten addrs)
  in
  return servers

(* This call is not strictly necessary as the parameters are initialized
   lazily the first time a Sapling operation (validation or forging) is
   done. This is what the client does.
   For a long running binary however it is important to make sure that the
   parameters files are there at the start and avoid failing much later while
   validating an operation. Plus paying this cost upfront means that the first
   validation will not be more expensive. *)
let init_zcash () =
  try
    Tezos_sapling.Core.Validator.init_params () ;
    Lwt.return_unit
  with exn ->
    Lwt.fail_with
      (Printf.sprintf
         "Failed to initialize Zcash parameters: %s"
         (Printexc.to_string exn))

let init_rpc (config : Config_file.t) (node : Node.t) internal_events =
  let open Lwt_result_syntax in
  (* Start local RPC server (handled by the node main process) only
     when at least one local listen addr is given. *)
  let commit_info =
    ({
       commit_hash = Tezos_version_value.Current_git_info.commit_hash;
       commit_date = Tezos_version_value.Current_git_info.committer_date;
     }
      : Tezos_version.Node_version.commit_info)
  in
  let node_version = Node.get_version node in

  let dir = Node.build_rpc_directory ~node_version ~commit_info node in
  let dir = Node_directory.build_node_directory config dir in
  let dir =
    Tezos_rpc.Directory.register_describe_directory_service
      dir
      Tezos_rpc.Service.description_service
  in
  let* local_rpc_server =
    (* For backward compatibility, and as the default behaviour is the
       local RPC server, we merge the listen_addrs and
       local_listen_addrs arguments. *)
    if config.rpc.listen_addrs @ config.rpc.local_listen_addrs = [] then
      return No_server
    else init_local_rpc_server config dir
  in
  (* Start RPC process only when at least one listen addr is given. *)
  let* rpc_server =
    if config.rpc.external_listen_addrs = [] then return No_server
    else
      (* Starts the node's local RPC server that aims to handle the
         RPCs forwarded by the rpc_process, if they cannot be
         processed by the rpc_process itself. *)
      init_external_rpc_server config node_version dir internal_events
  in
  return (local_rpc_server :: [rpc_server])

let run ?verbosity ?sandbox ?target ?(cli_warnings = [])
    ?ignore_testchain_warning ~singleprocess ~force_history_mode_switch
    (config : Config_file.t) =
  let open Lwt_result_syntax in
  (* Main loop *)
  let internal_events =
    match config.internal_events with
    | Some ie -> ie
    | None ->
        Tezos_base_unix.Internal_event_unix.make_with_defaults
          ~enable_default_daily_logs_at:
            Filename.Infix.(config.data_dir // "daily_logs")
          ?verbosity
          ~log_cfg:config.log
          ()
  in
  let*! () =
    Tezos_base_unix.Internal_event_unix.init ~config:internal_events ()
  in
  let*! () =
    Lwt_list.iter_s (fun evt -> Internal_event.Simple.emit evt ()) cli_warnings
  in
  let* () =
    Data_version.ensure_data_dir
      ~mode:Is_compatible
      config.blockchain_network.genesis
      config.data_dir
  in
  let* () = Config_validation.check ?ignore_testchain_warning config in
  let* identity = init_identity_file config in
  Updater.init (Data_version.protocol_dir config.data_dir) ;
  let*! () =
    Event.(emit starting_node)
      ( config.blockchain_network.chain_name,
        Tezos_version_value.Current_git_info.version,
        Tezos_version_value.Current_git_info.abbreviated_commit_hash )
  in
  let*! () = init_zcash () in
  let* () =
    Tezos_crypto_dal.Cryptobox.Config.init_verifier_dal
      config.blockchain_network.dal_config
  in
  let*! node =
    init_node
      ?sandbox
      ?target
      ~internal_events
      ~identity
      ~singleprocess
      ~force_history_mode_switch
      config
  in
  let*! () =
    Result.iter_error_s
      (function
        | Store_errors.Cannot_switch_history_mode {previous_mode; next_mode}
          :: _ ->
            Event.(emit incorrect_history_mode) (previous_mode, next_mode)
        | _ -> Lwt.return_unit)
      node
  in
  let*? node in
  let log_node_downer =
    Lwt_exit.register_clean_up_callback ~loc:__LOC__ (fun _ ->
        Event.(emit shutting_down_node) ())
  in
  let* rpc_servers = init_rpc config node internal_events in
  let rpc_downer =
    Lwt_exit.register_clean_up_callback
      ~loc:__LOC__
      ~after:[log_node_downer]
      (fun _ ->
        let*! () = Event.(emit shutting_down_local_rpc_server) () in
        List.iter_s
          (function
            | No_server -> Lwt.return_unit
            | External_rpc_server rpc_servers ->
                List.iter_p
                  (fun (local_server, rpc_process) ->
                    (* Stop the RPC_process first to avoid requests to
                       be forwarded to the note with a RPC_server that
                       is down. *)
                    let*! () =
                      Octez_rpc_process.Rpc_process_worker.stop rpc_process
                    in
                    let*! () = RPC_server.shutdown local_server in
                    Lwt.return_unit)
                  rpc_servers
            | Local_rpc_server rpc_server ->
                List.iter_p RPC_server.shutdown rpc_server)
          rpc_servers)
  in
  let node_downer =
    Lwt_exit.register_clean_up_callback
      ~loc:__LOC__
      ~after:[rpc_downer]
      (fun _ -> Node.shutdown node)
  in
  let*! () = Event.(emit node_is_ready) () in
  let _ =
    Lwt_exit.register_clean_up_callback
      ~loc:__LOC__
      ~after:[node_downer]
      (fun exit_status ->
        let*! () = Event.(emit bye) exit_status in
        Tezos_base_unix.Internal_event_unix.close ())
  in
  Lwt.dont_wait
    (fun () ->
      let*! r = metrics_serve config.metrics_addr in
      match r with
      | Ok _ -> Lwt.return_unit
      | Error err ->
          Event.(emit metrics_ended (Format.asprintf "%a" pp_print_trace err)))
    (fun exn ->
      Event.(
        emit__dont_wait__use_with_care metrics_ended (Printexc.to_string exn))) ;
  Lwt_utils.never_ending ()

let process sandbox verbosity target singleprocess force_history_mode_switch
    args =
  let open Lwt_result_syntax in
  let verbosity =
    let open Internal_event in
    match verbosity with [] -> None | [_] -> Some Info | _ -> Some Debug
  in
  let main_promise =
    let cli_warnings = ref [] in
    let* config =
      Shared_arg.read_and_patch_config_file
        ~ignore_bootstrap_peers:
          (match sandbox with Some _ -> true | None -> false)
        ~emit:(fun event () ->
          cli_warnings := event :: !cli_warnings ;
          Lwt.return_unit)
        args
    in
    let* () =
      match sandbox with
      | Some _ when config.data_dir = Config_file.default_data_dir ->
          failwith "Cannot use default data directory while in sandbox mode"
      | _ -> return_unit
    in
    let* target =
      match target with
      | None -> return_none
      | Some s ->
          let l = String.split_on_char ',' s in
          Lwt.catch
            (fun () ->
              assert (Compare.List_length_with.(l = 2)) ;
              let target =
                match l with
                | [block_hash; level] ->
                    ( Block_hash.of_b58check_exn block_hash,
                      Int32.of_string level )
                | _ -> assert false
              in
              return_some target)
            (fun _ ->
              failwith
                "Failed to parse the provided target. A '<block_hash>,<level>' \
                 value was expected.")
    in
    Lwt_lock_file.try_with_lock
      ~when_locked:(fun () ->
        failwith "Data directory is locked by another process")
      ~filename:(Data_version.lock_file config.data_dir)
    @@ fun () ->
    Lwt.catch
      (fun () ->
        run
          ?sandbox
          ?verbosity
          ?target
          ~singleprocess
          ~force_history_mode_switch
          ~cli_warnings:!cli_warnings
          ~ignore_testchain_warning:args.enable_testchain
          config)
      (function exn -> fail_with_exn exn)
  in
  Lwt.Exception_filter.(set handle_all_except_runtime) ;
  Lwt_main.run
    (let*! r = Lwt_exit.wrap_and_exit main_promise in
     match r with
     | Ok () ->
         let*! _ = Lwt_exit.exit_and_wait 0 in
         Lwt.return (`Ok ())
     | Error err ->
         let*! _ = Lwt_exit.exit_and_wait 1 in
         Lwt.return @@ `Error (false, Format.asprintf "%a" pp_print_trace err))

module Term = struct
  let verbosity =
    let open Cmdliner in
    let doc =
      "Increase log level. Using $(b,-v) is equivalent to using \
       $(b,TEZOS_LOG='* -> info'), and $(b,-vv) is equivalent to using \
       $(b,TEZOS_LOG='* -> debug')."
    in
    Arg.(
      value & flag_all & info ~docs:Shared_arg.Manpage.misc_section ~doc ["v"])

  let sandbox =
    let open Cmdliner in
    let doc =
      "Run the daemon in sandbox mode. P2P to non-localhost addresses are \
       disabled, and constants of the economic protocol can be altered with a \
       JSON file which overrides the $(b,genesis_parameters) field of the \
       network configuration (e.g. scripts/sandbox.json). $(b,IMPORTANT): \
       Using sandbox mode affects the node state and subsequent runs of Tezos \
       node must also use sandbox mode. In order to run the node in normal \
       mode afterwards, a full reset must be performed (by removing the node's \
       data directory)."
    in
    Arg.(
      value
      & opt (some non_dir_file) None
      & info
          ~docs:Shared_arg.Manpage.misc_section
          ~doc
          ~docv:"FILE.json"
          ["sandbox"])

  let target =
    let open Cmdliner in
    let doc =
      "When asked to take a block as a target, the daemon will only accept the \
       chains that contains that block and those that might reach it."
    in
    Arg.(
      value
      & opt (some string) None
      & info
          ~docs:Shared_arg.Manpage.misc_section
          ~doc
          ~docv:"<block_hash>,<level>"
          ["target"])

  let singleprocess =
    let open Cmdliner in
    let doc =
      "When enabled, it deactivates block validation using an external \
       process. Thus, the validation procedure is done in the same process as \
       the node and might not be responding when doing extensive I/Os."
    in
    Arg.(
      value & flag
      & info ~docs:Shared_arg.Manpage.misc_section ~doc ["singleprocess"])

  let force_history_mode_switch =
    let open Cmdliner in
    let doc =
      Format.sprintf
        "Forces the switch of history modes when a different history mode is \
         found between the written configuration and the given history mode.  \
         Warning: this option will modify the storage irremediably. Please \
         refer to the Tezos node documentation for more details."
    in
    Arg.(
      value & flag
      & info
          ~docs:Shared_arg.Manpage.misc_section
          ~doc
          ["force-history-mode-switch"])

  let term =
    Cmdliner.Term.(
      ret
        (const process $ sandbox $ verbosity $ target $ singleprocess
       $ force_history_mode_switch $ Shared_arg.Term.args))
end

module Manpage = struct
  let command_description =
    "The $(b,run) command is meant to run the Tezos node. Most of its command \
     line arguments corresponds to config file entries, and will have priority \
     over the latter if used."

  let description = [`S "DESCRIPTION"; `P command_description]

  let debug =
    let log_sections =
      String.concat
        " "
        (List.of_seq (Internal_event.get_registered_sections ()))
    in
    [
      `S "DEBUG";
      `P
        ("The environment variable $(b,TEZOS_LOG) is used to fine-tune what is \
          going to be logged. The syntax is \
          $(b,TEZOS_LOG='<section> -> <level> [ ; ...]') where section is one \
          of $(i," ^ log_sections
       ^ ") and level is one of $(i,fatal), $(i,error), $(i,warn), \
          $(i,notice), $(i,info) or $(i,debug). A $(b,*) can be used as a \
          wildcard in sections, i.e. $(b, node* -> debug). The rules are \
          matched left to right, therefore the leftmost rule is highest \
          priority .");
    ]

  let examples =
    [
      `S "EXAMPLES";
      `I
        ( "$(b,Run in sandbox mode listening to RPC commands at localhost port \
           8732)",
          "$(mname) run \
           --sandbox=src/proto_alpha/parameters/sandbox-parameters.json \
           --data-dir /custom/data/dir --rpc-addr localhost:8732" );
      `I ("$(b,Run a node that accepts network connections)", "$(mname) run");
    ]

  let man =
    description @ Shared_arg.Manpage.args @ debug @ examples
    @ Shared_arg.Manpage.bugs

  let info = Cmdliner.Cmd.info ~doc:"Run the Tezos node" ~man "run"
end

let cmd = Cmdliner.Cmd.v Manpage.info Term.term
back to top