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

module type NAME = sig
  val name : string
end

module type KEY = sig
  type t

  val compare : t -> t -> int

  val pp : Format.formatter -> t -> unit

  val encoding : t Data_encoding.t
end

module type VALUE = sig
  type t

  val equal : t -> t -> bool

  val pp : Format.formatter -> t -> unit

  val encoding : t Data_encoding.t
end

module type S = sig
  type t

  type key

  type value

  val empty : capacity:int64 -> t

  val encoding : t Data_encoding.t

  val pp : Format.formatter -> t -> unit

  val find : key -> t -> value option

  type error +=
    | Key_bound_to_different_value of {
        key : key;
        existing_value : value;
        given_value : value;
      }

  val remember : key -> value -> t -> t tzresult

  module Internal_for_tests : sig
    val empty : capacity:int64 -> next_index:int64 -> t

    val keys : t -> key list
  end
end

module Make (Name : NAME) (Key : KEY) (Value : VALUE) :
  S with type key = Key.t and type value = Value.t = struct
  type key = Key.t

  type value = Value.t

  module Int64_map = Map.Make (Int64)
  module Map = Map.Make (Key)

  type t = {
    events : value Map.t;
        (** Values stored in the structure, indexes with the keys. *)
    sequence : key Int64_map.t;
        (** An additional map from int64 indexes to keys, to be able
            to remove old entries when the structure is full.  *)
    capacity : int64;
        (** The max number of the entries in the structure. Once the maximum size
            is reached, older entries are deleted to free space for new ones. *)
    next_index : int64;
        (** The index to use for the next entry to add in the structure. *)
    oldest_index : int64;
        (** The oldest index of the (oldest) entry that has been added to the
            data structure. If the structure is empty, [oldest_index] is
            equal to [next_index]. *)
    size : int64;
        (** Counts the number of entries that are stored in history. It
            satisfies the invariant: `0 <= size <= capacity` *)
  }

  let encoding : t Data_encoding.t =
    let open Data_encoding in
    let events_encoding =
      Data_encoding.conv
        Map.bindings
        (fun l -> Map.add_seq (List.to_seq l) Map.empty)
        Data_encoding.(list (tup2 Key.encoding Value.encoding))
    in
    let sequence_encoding =
      conv
        Int64_map.bindings
        (List.fold_left (fun m (k, v) -> Int64_map.add k v m) Int64_map.empty)
        (list (tup2 int64 Key.encoding))
    in
    conv
      (fun {events; sequence; capacity; next_index; oldest_index; size} ->
        (events, sequence, capacity, next_index, oldest_index, size))
      (fun (events, sequence, capacity, next_index, oldest_index, size) ->
        {events; sequence; capacity; next_index; oldest_index; size})
      (obj6
         (req "events" events_encoding)
         (req "sequence" sequence_encoding)
         (req "capacity" int64)
         (req "next_index" int64)
         (req "oldest_index" int64)
         (req "size" int64))

  let pp fmt {events; sequence; capacity; size; oldest_index; next_index} =
    Map.bindings events |> fun bindings ->
    Int64_map.bindings sequence |> fun sequence_bindings ->
    let pp_binding fmt (hash, history_proof) =
      Format.fprintf fmt "@[%a -> %a@;@]" Key.pp hash Value.pp history_proof
    in
    let pp_sequence_binding fmt (counter, hash) =
      Format.fprintf fmt "@[%s -> %a@;@]" (Int64.to_string counter) Key.pp hash
    in
    Format.fprintf
      fmt
      "@[<hov 2>History:@;\
      \ { capacity: %Ld;@;\
      \ current size: %Ld;@;\
      \ oldest index: %Ld;@;\
      \ next_index : %Ld;@;\
      \ bindings: %a;@;\
      \ sequence: %a; }@]"
      capacity
      size
      oldest_index
      next_index
      (Format.pp_print_list pp_binding)
      bindings
      (Format.pp_print_list pp_sequence_binding)
      sequence_bindings

  let empty ~capacity =
    let next_index = 0L in
    {
      events = Map.empty;
      sequence = Int64_map.empty;
      capacity;
      next_index;
      oldest_index = next_index;
      size = 0L;
    }

  type error +=
    | Key_bound_to_different_value of {
        key : key;
        existing_value : value;
        given_value : value;
      }

  let () =
    assert (not (String.equal Name.name "")) ;
    register_error_kind
      `Temporary
      ~id:
        (Format.sprintf
           "Bounded_history_repr.%s.key_bound_to_different_value"
           Name.name)
      ~title:(Name.name ^ ": Key already bound to a different value.")
      ~description:
        (Name.name
       ^ ": Remember called with a key that is already bound to a different\n\
         \        value.")
      Data_encoding.(
        obj3
          (req "key" Key.encoding)
          (req "existing_value" Value.encoding)
          (req "given_value" Value.encoding))
      (function
        | Key_bound_to_different_value {key; existing_value; given_value} ->
            Some (key, existing_value, given_value)
        | _ -> None)
      (fun (key, existing_value, given_value) ->
        Key_bound_to_different_value {key; existing_value; given_value})

  let remember key value t =
    let open Tzresult_syntax in
    if Compare.Int64.(t.capacity <= 0L) then return t
    else
      match Map.find key t.events with
      | Some value' when not (Value.equal value value') ->
          error
          @@ Key_bound_to_different_value
               {key; existing_value = value'; given_value = value}
      | _ -> (
          let events = Map.add key value t.events in
          let current_index = t.next_index in
          let next_index = Int64.succ current_index in
          let t =
            {
              events;
              sequence = Int64_map.add current_index key t.sequence;
              capacity = t.capacity;
              next_index;
              oldest_index = t.oldest_index;
              size = Int64.succ t.size;
            }
          in
          (* A negative size means that [t.capacity] is set to [Int64.max_int]
             and that the structure is full, so adding a new entry makes the size
             overflows. In this case, we remove an element in the else branch to
             keep the size of the structure equal to [Int64.max_int] at most. *)
          if Compare.Int64.(t.size > 0L && t.size <= t.capacity) then return t
          else
            let l = t.oldest_index in
            match Int64_map.find l t.sequence with
            | None ->
                (* If t.size > t.capacity > 0, there is necessarily
                   an entry whose index is t.oldest_index in [sequence]. *)
                assert false
            | Some h ->
                let sequence = Int64_map.remove l t.sequence in
                let events = Map.remove h events in
                return
                  {
                    next_index = t.next_index;
                    capacity = t.capacity;
                    size = t.capacity;
                    oldest_index = Int64.succ t.oldest_index;
                    sequence;
                    events;
                  })

  let find key t = Map.find_opt key t.events

  module Internal_for_tests = struct
    let empty ~capacity ~next_index =
      {(empty ~capacity) with next_index; oldest_index = next_index}

    let keys {sequence; oldest_index; _} =
      let l = Int64_map.bindings sequence in
      (* All entries with an index greater than oldest_index are well ordered.
         There are put in the [lp] list. Entries with an index smaller than
         oldest_index are also well ordered, but they should come after
         elements in [lp]. This happens in theory when the index reaches
         max_int and then overflows. *)
      let ln, lp =
        List.partition_map
          (fun (n, h) ->
            if Compare.Int64.(n < oldest_index) then Left h else Right h)
          l
      in
      (* do a tail recursive concatenation lp @ ln *)
      List.rev_append (List.rev lp) ln
  end
end
back to top