Raw File
persistence.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2020 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

include Persistence_intf

(** The regexp that tests if a protocol hash is the one of Alpha.

      This inelegant design is the best we could think of that is resilient
      in the face of protocol changes: we don't want to have to change code in
      each protocol every time the Alpha protocol changes.
  *)
let is_proto_alpha_regexp = Re.Str.regexp_case_fold ".*ProtoAlpha.*"

module Make (Registration : Registration.S) = struct
  include Persistence_intf

  let rpc_context_encoding :
      Tezos_protocol_environment.rpc_context Data_encoding.t =
    let open Data_encoding in
    let open Tezos_protocol_environment in
    conv
      (fun {block_hash; block_header; context} ->
        (block_hash, block_header, context))
      (fun (block_hash, block_header, context) ->
        {block_hash; block_header; context})
      (obj3
         (req "block_hash" Block_hash.encoding)
         (req "shell_header" Block_header.shell_header_encoding)
         (req "context" Memory_context.encoding))

  module Persistent_mockup_environment = struct
    type t = {
      protocol_hash : Protocol_hash.t;
      chain_id : Chain_id.t;
      rpc_context : Tezos_protocol_environment.rpc_context;
      protocol_data : bytes;
    }

    let encoding =
      let open Data_encoding in
      conv
        (fun {protocol_hash; chain_id; rpc_context; protocol_data} ->
          (protocol_hash, chain_id, rpc_context, protocol_data))
        (fun (protocol_hash, chain_id, rpc_context, protocol_data) ->
          {protocol_hash; chain_id; rpc_context; protocol_data})
        (obj4
           (req "protocol_hash" Protocol_hash.encoding)
           (req "chain_id" Chain_id.encoding)
           (req "context" rpc_context_encoding)
           (req "protocol_data" Variable.bytes))

    let to_json = Data_encoding.Json.construct encoding

    let of_json = Data_encoding.Json.destruct encoding
  end

  let get_registered_mockup (protocol_hash_opt : Protocol_hash.t option)
      (printer : #Tezos_client_base.Client_context.printer) :
      Registration.mockup_environment tzresult Lwt.t =
    let open Lwt_result_syntax in
    let mockup_environments = Registration.get_registered_environments () in
    let hash_is_of_mockup hash (module Mockup : Registration.MOCKUP) =
      match hash with
      | Some protocol_hash ->
          Protocol_hash.equal protocol_hash Mockup.protocol_hash
      | None ->
          Re.Str.string_match
            is_proto_alpha_regexp
            (Protocol_hash.to_b58check Mockup.protocol_hash)
            0
    in
    let*! () =
      if Option.is_none protocol_hash_opt then
        printer#warning
          "No protocol specified: using Alpha as default protocol."
      else Lwt.return_unit
    in
    match
      List.find (hash_is_of_mockup protocol_hash_opt) mockup_environments
    with
    | Some mockup -> return mockup
    | None ->
        let requested_protocol =
          match protocol_hash_opt with
          | Some requested ->
              Format.asprintf
                "Requested protocol with hash %a"
                Protocol_hash.pp
                requested
          | None -> "Default protocol Alpha (no requested protocol)"
        in
        let protocol_hashes =
          List.map
            (fun (module Mockup : Registration.MOCKUP) -> Mockup.protocol_hash)
            mockup_environments
        in
        failwith
          "%s not found in available mockup environments. Available protocol \
           hashes: [%a]"
          requested_protocol
          Format.(
            pp_print_list
              ~pp_sep:(fun fmt () -> fprintf fmt ", ")
              Protocol_hash.pp)
          protocol_hashes

  let default_mockup_context :
      Tezos_client_base.Client_context.printer ->
      (Registration.mockup_environment * Registration.mockup_context) tzresult
      Lwt.t =
   fun cctxt ->
    let open Lwt_result_syntax in
    let* mockup = get_registered_mockup None cctxt in
    let (module Mockup) = mockup in
    let* rpc_context =
      Mockup.init
        ~cctxt
        ~parameters:Mockup.default_parameters
        ~constants_overrides_json:None
        ~bootstrap_accounts_json:None
    in
    return (mockup, rpc_context)

  let init_mockup_context_by_protocol_hash :
      cctxt:Tezos_client_base.Client_context.printer ->
      protocol_hash:Protocol_hash.t ->
      constants_overrides_json:Data_encoding.json option ->
      bootstrap_accounts_json:Data_encoding.json option ->
      (Registration.mockup_environment * Registration.mockup_context) tzresult
      Lwt.t =
   fun ~cctxt ~protocol_hash ~constants_overrides_json ~bootstrap_accounts_json ->
    let open Lwt_result_syntax in
    let* mockup = get_registered_mockup (Some protocol_hash) cctxt in
    let (module Mockup) = mockup in
    let* menv =
      Mockup.init
        ~cctxt
        ~parameters:Mockup.default_parameters
        ~constants_overrides_json
        ~bootstrap_accounts_json
    in
    return (mockup, menv)

  let mockup_context_from_persisted
      ({protocol_hash; chain_id; rpc_context; protocol_data} :
        Persistent_mockup_environment.t)
      (printer : #Tezos_client_base.Client_context.printer) =
    let open Lwt_result_syntax in
    let* mockup = get_registered_mockup (Some protocol_hash) printer in
    return
      ( mockup,
        Tezos_mockup_registration.Registration_intf.
          {chain = chain_id; rpc_context; protocol_data} )

  let get_mockup_context_from_disk ~base_dir ~protocol_hash
      (printer : #Tezos_client_base.Client_context.printer) =
    let open Lwt_result_syntax in
    let file = (Files.Context.get ~dirname:base_dir :> string) in
    if not (Sys.file_exists file) then
      failwith "get_mockup_context_from_disk: file %s not found" file
    else
      let* context_json =
        Tezos_stdlib_unix.Lwt_utils_unix.Json.read_file file
      in
      match Persistent_mockup_environment.of_json context_json with
      | persisted_mockup ->
          let ({
                 timestamp = predecessor_timestamp;
                 level = predecessor_level;
                 fitness = predecessor_fitness;
                 _;
               }
                : Block_header.shell_header) =
            persisted_mockup.rpc_context.block_header
          in
          let timestamp =
            Time.System.to_protocol (Tezos_base.Time.System.now ())
          in
          let predecessor = persisted_mockup.rpc_context.block_hash in
          let* (module Mockup) =
            get_registered_mockup (Some persisted_mockup.protocol_hash) printer
          in
          (*
             In the mockup mode, reactivity is important and there are
             no constraints to be consistent with other nodes. For this
             reason, the mockup mode loads the cache lazily.
             See {!Environment_context.source_of_cache}.
          *)
          let* value_of_key =
            Mockup.Protocol.value_of_key
              ~chain_id:persisted_mockup.chain_id
              ~predecessor_context:persisted_mockup.rpc_context.context
              ~predecessor_timestamp
              ~predecessor_level
              ~predecessor_fitness
              ~predecessor
              ~timestamp
          in
          let* context =
            Tezos_protocol_environment.Context.load_cache
              predecessor
              persisted_mockup.rpc_context.context
              `Lazy
              value_of_key
          in
          let persisted_mockup =
            {
              persisted_mockup with
              rpc_context = {persisted_mockup.rpc_context with context};
            }
          in
          let* (((module Mockup_environment), _) as res) =
            mockup_context_from_persisted persisted_mockup printer
          in
          let* () =
            match protocol_hash with
            | None -> return_unit
            | Some desired_protocol
              when Protocol_hash.equal
                     Mockup_environment.protocol_hash
                     desired_protocol ->
                return_unit
            | Some desired_protocol ->
                failwith
                  "Protocol %a was requested via --protocol\n\
                   yet the mockup at %s was initialized with %a"
                  Protocol_hash.pp_short
                  desired_protocol
                  base_dir
                  Protocol_hash.pp_short
                  Mockup_environment.protocol_hash
          in
          return res
      | exception _e ->
          failwith "get_mockup_context_from_disk: could not read %s" file

  let overwrite_mockup ~protocol_hash ~chain_id ~rpc_context ~protocol_data
      ~base_dir =
    let context_file = (Files.Context.get ~dirname:base_dir :> string) in
    if not (Sys.file_exists context_file) then
      failwith "overwrite_mockup: file %s does not exist" context_file
    else
      Persistent_mockup_environment.(
        to_json {protocol_hash; chain_id; rpc_context; protocol_data})
      |> Tezos_stdlib_unix.Lwt_utils_unix.Json.write_file context_file

  type base_dir_class =
    | Base_dir_does_not_exist
    | Base_dir_is_file
    | Base_dir_is_mockup
    | Base_dir_is_nonempty
    | Base_dir_is_empty

  let pp_base_dir_class ppf bclass =
    Format.fprintf
      ppf
      "base_dir_%s"
      ((function
         | Base_dir_does_not_exist -> "does_not_exist"
         | Base_dir_is_file -> "is_file"
         | Base_dir_is_mockup -> "is_mockup"
         | Base_dir_is_empty -> "is_empty"
         | Base_dir_is_nonempty -> "is_non_empty")
         bclass)

  let is_directory_empty dir = Array.length (Sys.readdir dir) = 0

  let classify_base_dir base_dir =
    let open Lwt_syntax in
    if not (Sys.file_exists base_dir) then return_ok Base_dir_does_not_exist
    else if not (Sys.is_directory base_dir) then return_ok Base_dir_is_file
    else if is_directory_empty base_dir then return_ok Base_dir_is_empty
    else
      let dirname = base_dir in
      let* b = Files.exists_mockup_directory ~dirname in
      match b with
      | true -> (
          let* b = Files.Context.exists ~dirname in
          match b with
          | true -> return_ok Base_dir_is_mockup
          | false -> return_ok Base_dir_is_nonempty)
      | false -> return_ok Base_dir_is_nonempty

  let create_mockup ~(cctxt : Tezos_client_base.Client_context.full)
      ~protocol_hash ~constants_overrides_json ~bootstrap_accounts_json
      ~asynchronous =
    let open Lwt_result_syntax in
    let base_dir = cctxt#get_base_dir in
    let create_base_dir () =
      let*! () = Tezos_stdlib_unix.Lwt_utils_unix.create_dir base_dir in
      let*! () =
        cctxt#message "Created mockup client base dir in %s" base_dir
      in
      return_unit
    in
    let* () =
      let* bd = classify_base_dir base_dir in
      match bd with
      | Base_dir_does_not_exist | Base_dir_is_empty -> create_base_dir ()
      | Base_dir_is_file -> failwith "%s is a file" base_dir
      | Base_dir_is_mockup ->
          failwith "%s is already initialized as a mockup directory" base_dir
      | Base_dir_is_nonempty ->
          failwith
            "%s is not empty, please specify a fresh base directory"
            base_dir
    in
    let* _mockup_env, {chain = chain_id; rpc_context; protocol_data} =
      init_mockup_context_by_protocol_hash
        ~cctxt:(cctxt :> Tezos_client_base.Client_context.printer)
        ~protocol_hash
        ~constants_overrides_json
        ~bootstrap_accounts_json
    in
    let mockup_dir = (Files.get_mockup_directory ~dirname:base_dir :> string) in
    let*! () = Tezos_stdlib_unix.Lwt_utils_unix.create_dir mockup_dir in
    let context_file = (Files.Context.get ~dirname:base_dir :> string) in
    let* () =
      Persistent_mockup_environment.(
        to_json {protocol_hash; chain_id; rpc_context; protocol_data})
      |> Tezos_stdlib_unix.Lwt_utils_unix.Json.write_file context_file
    in
    if asynchronous then
      (* Setup a local persistent mempool *)
      let mempool_file = (Files.Mempool.get ~dirname:base_dir :> string) in
      let*! () =
        cctxt#message "creating persistent mempool file at %s" mempool_file
      in
      Lwt_io.with_file ~mode:Lwt_io.Output mempool_file (fun oc ->
          let*! () = Lwt_io.write oc "[]" in
          return_unit)
    else return_unit
end

module Internal_for_tests = struct
  module Make = Make
end

include Make (Registration)
back to top