aboutsummaryrefslogblamecommitdiffstats
path: root/lib/kernel/Merkle.ml
blob: 266a5602f8c22a16e9c924b88069478c91bc6e82 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                       

        

                                                                          

   


                     
                                              

   

                                                                          
           
 



                                                            
 
                                                                                           
                                                                                       
                  
                                                                                   
                               
 


                                                                                          


                                                                                        

                                                                                                    
 






                                             
                                                                                                

   






                                     
 

                             
 



                                                                                               
 










































































                                                                                                   


                                                                      

























                                                                           
          

















































































                                                                                                  









                                                                                                 



































































                                                                                         









                                        
















                                                                                     














































                                                                                                
     
 
                                                                                           

                                           
                                   

           



                                                            
 
                                                                                           

                              
                  
                                                                                   
                               
                                                                                                 
                                                                                                

                                                                                                
                                                                                       
 





                                             
 

                                                                                              
 
                                                                                                
   
 

































                                                                       




                                                                                       




















                                                                                               


                                                    
                                                                 
                                                                       
                                                                              
   
module type VALUE = sig
  type t

  val encode : t -> Concepts.Blob.t
  val decode : Concepts.Blob.t -> (t, Concepts.Condition.condition) result
end

module type KEY = sig
  include VALUE

  val compare : t -> t -> Utilities.Ordering.t
end

module type TREE = functor (S : Abstract.Storage.STORAGE) (K : KEY) -> sig
  type address = Concepts.Hash.hash
  type node

  val with_batch :
    S.transaction ->
    (unit -> (node, Concepts.Condition.condition) result) ->
    (node, Concepts.Condition.condition) result

  val find : S.transaction -> address -> (node option, Concepts.Condition.condition) result
  val persist : S.transaction -> node -> (address, Concepts.Condition.condition) result
  val empty : node
  val empty_under : S.transaction -> (address, Concepts.Condition.condition) result
  val hash_of : node -> address

  val insert :
    S.transaction -> K.t -> address -> node -> (node, Concepts.Condition.condition) result

  val of_list :
    S.transaction -> (K.t * address) list -> (node, Concepts.Condition.condition) result

  val remove : S.transaction -> K.t -> node -> (node, Concepts.Condition.condition) result
  val lookup : S.transaction -> K.t -> node -> (address option, Concepts.Condition.condition) result

  val fold_left :
    S.transaction ->
    ('c -> K.t -> address -> 'c) ->
    'c ->
    node ->
    ('c, Concepts.Condition.condition) result

  val keys : S.transaction -> node -> (K.t BatFingerTree.t, Concepts.Condition.condition) result
end

module Make : TREE =
functor
  (S : Abstract.Storage.STORAGE)
  (K : KEY)
  ->
  struct
    type address = Concepts.Hash.hash

    module Error = struct
      open Concepts.Condition

      let malformed_node () =
        condition "malformed-node" "A node representation did not conform to what was expected"
          empty
    end

    type node =
      | Leaf of {keys: K.t BatFingerTree.t; values: address BatFingerTree.t}
      | Trunk of {keys: K.t BatFingerTree.t; children: address BatFingerTree.t}

    let empty = Leaf {keys= BatFingerTree.empty; values= BatFingerTree.empty}

    let rec lookup1' keys key bottom top =
      let open Utilities.Ordering in
      if bottom >= top then bottom, false
      else
        let mid = bottom + ((top - bottom) / 2) in
        let node = BatFingerTree.get keys mid in
        match K.compare node key with
        | Equal -> mid, true
        | Greater -> lookup1' keys key bottom mid
        | Smaller -> lookup1' keys key (mid + 1) top

    let lookup1 keys key = lookup1' keys key 0 (BatFingerTree.size keys)
    let keys_of = function Leaf {keys; _} -> keys | Trunk {keys; _} -> keys

    let of_bencode =
      let open Concepts.Encoding.Bencode in
      let open Utilities.Result in
      (* This is terrible *)
      let decode_key t =
        as_string t
        |> Result.map String.to_bytes
        |> Result.map Concepts.Blob.blob_of_bytes
        |> fmap K.decode
      in
      let decode_value t = as_string t |> Result.map Concepts.Hash.of_raw_string in
      let decode_list f data =
        data
        |> fmap as_list
        |> Result.map (List.map f)
        |> fmap Utilities.List.sequence
        |> Result.map BatFingerTree.of_list
      in
      function
      | Tagged ('!', (Dict _ as data)) ->
          let* keys = field "keys" data |> decode_list decode_key in
          let* values = field "values" data |> decode_list decode_value in
          Ok (Leaf {keys; values})
      | Tagged ('#', (Dict _ as data)) ->
          let* keys = field "keys" data |> decode_list decode_key in
          let* children = field "children" data |> decode_list decode_value in
          Ok (Trunk {keys; children})
      | _ -> Error (Error.malformed_node ())

    let from_blob blob = Concepts.Encoding.Bencode.of_blob blob |> Utilities.Result.fmap of_bencode

    let to_bencode =
      let open Concepts.Encoding.Bencode in
      (* Ditto. *)
      let bencode_key k =
        K.encode k |> Concepts.Blob.bytes_of_blob |> String.of_bytes |> fun s -> String s
      in
      let bencode_hash h = String (Concepts.Hash.to_raw_string h) in
      function
      | Leaf {keys; values} ->
          Tagged
            ( '!',
              Dict
                [ "keys", List (BatFingerTree.to_list keys |> List.map bencode_key);
                  "values", List (BatFingerTree.to_list values |> List.map bencode_hash) ] )
      | Trunk {keys; children} ->
          Tagged
            ( '#',
              Dict
                [ "keys", List (BatFingerTree.to_list keys |> List.map bencode_key);
                  "children", List (BatFingerTree.to_list children |> List.map bencode_hash) ] )

    let to_blob node = to_bencode node |> Concepts.Encoding.Bencode.to_blob

    (* TODO: we should probably cache this inside the node itself rather
     than recalculating it every time. We could also allow the user to
     create "uninterned" nodes that are not storage-backed, that could
     be used for intermediates and the like. *)
    let hash_of node = to_blob node |> Concepts.Hash.hash_of_blob

    type _ Effect.t +=
      | Intern : (address * node * Concepts.Blob.t) -> unit Effect.t
      | Retrieve : address -> (node * Concepts.Blob.t) option Effect.t

    let rec flush tx buffer addr =
      let open Utilities.Result in
      match BatMap.find_opt addr buffer with
      | None -> Ok ()
      | Some (node, data) -> (
          let* _ = S.put tx (S.Hash addr) data in
          match node with
          | Leaf _ -> Ok ()
          | Trunk {children; _} ->
              BatFingerTree.map (fun addr -> flush tx buffer addr) children
              |> Utilities.FingerTree.sequence
              |> Result.map ignore )

    let with_batch tx f =
      let open Utilities.Result in
      let buffer = ref BatMap.empty in
      try
        let* node = f () in
        let* _ = flush tx !buffer (hash_of node) in
        Ok node
      with
      | effect Intern (addr, node, data), k ->
          buffer := BatMap.add addr (node, data) !buffer;
          Effect.Deep.continue k ()
      | effect Retrieve addr, k -> Effect.Deep.continue k (BatMap.find_opt addr !buffer)

    let find tx addr =
      let open Utilities.Result in
      try Effect.perform (Retrieve addr) |> Option.map fst |> Result.ok
      with Effect.Unhandled _ -> (
        let* data = S.get tx (S.Hash addr) in
        match data with
        | None -> Ok None
        | Some data ->
            let* node = from_blob data in
            Ok (Some node) )

    let persist tx node =
      let data = to_blob node in
      let addr = Concepts.Hash.hash_of_blob data in
      begin try Ok (Effect.perform (Intern (addr, node, data)))
      with Effect.Unhandled _ -> S.put tx (S.Hash addr) data
      end
      |> Result.map (fun () -> addr)

    let find' tx addr =
      let open Utilities.Result in
      let* child = find tx addr in
      match child with
      | Some child -> Ok child
      | None ->
          failwith
            "A child node was not found on the underlying storage. Either your database is \
             corrupted, or this is a bug on RNT!"

    let rec lookup tx key node =
      let open Utilities.Result in
      let i, found = lookup1 (keys_of node) key in
      match node with
      | Leaf {values; _} -> if found then Ok (Some (BatFingerTree.get values i)) else Ok None
      | Trunk {children; _} ->
          let* child = BatFingerTree.get children (if found then i + 1 else i) |> find' tx in
          lookup tx key child

    let empty_under tx = persist tx empty

    type op = Update of address * node | Split of address * K.t * address

    let emplace i v ft =
      let size = BatFingerTree.size ft in
      if i < 0 || i > size then invalid_arg "Merkle.emplace: index out of bounds"
      else if size = 0 then BatFingerTree.singleton v
      else if i = size then BatFingerTree.snoc ft v
      else
        let fl, fr = BatFingerTree.split_at ft i in
        BatFingerTree.singleton v |> BatFingerTree.append fl |> (Fun.flip BatFingerTree.append) fr

    let insert1 key value node =
      let i, present = lookup1 (keys_of node) key in
      match node with
      | Leaf ({keys; values} as leaf) ->
          if present then Leaf {leaf with values= BatFingerTree.set values i value}
          else Leaf {keys= emplace i key keys; values= emplace i value values}
      (* When in danger or in doubt: run in circles, scream and shout. *)
      | Trunk _ -> failwith "Cannot insert a value on a trunk node. This is a bug in RNT!"

    let pivot_at ft i =
      let l, r' = BatFingerTree.split_at ft i in
      let r, p = BatFingerTree.front_exn r' in
      l, p, r

    let split_at pivot = function
      | Leaf {keys; values} ->
          let kl, kr = BatFingerTree.split_at keys pivot in
          let vl, vr = BatFingerTree.split_at values pivot in
          let p = BatFingerTree.get kr 0 in
          Leaf {keys= kl; values= vl}, p, Leaf {keys= kr; values= vr}
      | Trunk {keys; children} ->
          let kl, kp, kr = pivot_at keys pivot in
          let cl, cr = BatFingerTree.split_at children (pivot + 1) in
          Trunk {keys= kl; children= cl}, kp, Trunk {keys= kr; children= cr}

    (* FIXME: this works out to a geometric distribution with roughly 64
     keys per node. Some of the consequences of this are that:
     - most of our nodes will end up with one or a few elements, which
       would make our trees taller than they would normally need to be
     - some nodes will end up much larger than usual, which is bad for
       lookup performance

     What we want is a normal distribution centered around our desired
     node size. See
     https://www.dolthub.com/docs/architecture/storage-engine/prolly-tree/#controlling-chunk-size
   *)
    let should_split = Concepts.Hash.pick 6

    let hash_of_keys ks =
      let buffer = Buffer.create 1024 in
      BatFingerTree.iter
        (fun k -> K.encode k |> Concepts.Blob.bytes_of_blob |> Buffer.add_bytes buffer)
        ks;
      Concepts.Hash.hash_of_bytes (Buffer.to_bytes buffer)

    let split_position keys =
      let rec loop left right =
        match BatFingerTree.front right with
        | None -> None
        | Some (tail, head) ->
            (* TODO: rolling hash *)
            let left' = BatFingerTree.snoc left head in
            let hash = hash_of_keys left' in
            if should_split hash then Some (BatFingerTree.size left) else loop left' tail
      in
      loop BatFingerTree.empty keys

    let commit_node tx node =
      let open Utilities.Result in
      match split_position (keys_of node) with
      | Some i ->
          let l, p, r = split_at i node in
          let* l_addr = persist tx l in
          let* r_addr = persist tx r in
          Ok (Split (l_addr, p, r_addr))
      | None ->
          let* addr = persist tx node in
          Ok (Update (addr, node))

    let rec insert' tx key value node =
      let open Utilities.Result in
      match node with
      | Leaf _ -> insert1 key value node |> commit_node tx
      | Trunk {keys; children} -> (
          let i, found = lookup1 keys key in
          let i = if found then i + 1 else i in
          let* child = BatFingerTree.get children i |> find' tx in
          let* r = insert' tx key value child in
          match r with
          | Update (addr, _) ->
              Trunk {keys; children= BatFingerTree.set children i addr} |> commit_node tx
          | Split (l, p, r) ->
              Trunk
                { keys= emplace i p keys;
                  children= BatFingerTree.set children i l |> emplace (i + 1) r }
              |> commit_node tx )

    let insert tx key value node =
      let open Utilities.Result in
      let* op = insert' tx key value node in
      match op with
      | Update (_, node) -> Ok node
      | Split (l, p, r) ->
          let new_root =
            Trunk
              { keys= BatFingerTree.singleton p;
                children=
                  BatFingerTree.empty
                  |> (Fun.flip BatFingerTree.snoc) l
                  |> (Fun.flip BatFingerTree.snoc) r }
          in
          let* _ = persist tx new_root in
          Ok new_root

    let of_list tx bindings =
      let open Utilities.Result in
      with_batch tx (fun () ->
          let* _ = empty_under tx in
          List.fold_left
            (fun node (key, value) ->
              let* node = node in
              insert tx key value node )
            (Ok empty) bindings )

    let rec fold_left tx f acc =
      let open Utilities.Result in
      function
      | Leaf {keys; values} ->
          Utilities.FingerTree.zip keys values
          |> BatFingerTree.fold_left (fun acc (k, v) -> f acc k v) acc
          |> Result.ok
      | Trunk {children; _} ->
          BatFingerTree.fold_left
            (fun acc addr ->
              let* acc = acc in
              let* node = find' tx addr in
              fold_left tx f acc node )
            (Ok acc) children

    let keys tx node =
      fold_left tx (fun acc k _ -> BatFingerTree.snoc acc k) BatFingerTree.empty node

    let excise i ft =
      let fl, fr = BatFingerTree.split_at ft i in
      BatFingerTree.append fl (BatFingerTree.tail_exn fr)

    (* Like an insert, produces a new tree where the path to the key
       is copied without it, and a node it leaves empty goes from its
       parent with the key that separated it. *)
    let rec remove' tx key node =
      let open Utilities.Result in
      match node with
      | Leaf {keys; values} ->
          let i, _ = lookup1 keys key in
          if BatFingerTree.size keys = 1 then Ok None
          else
            let leaf = Leaf {keys= excise i keys; values= excise i values} in
            let* _ = persist tx leaf in
            Ok (Some leaf)
      | Trunk {keys; children} ->
          let i, found = lookup1 keys key in
          let i = if found then i + 1 else i in
          let* child = BatFingerTree.get children i |> find' tx in
          let* child = remove' tx key child in
          let trunk =
            match child with
            | Some child -> Trunk {keys; children= BatFingerTree.set children i (hash_of child)}
            | None -> Trunk {keys= excise (max 0 (i - 1)) keys; children= excise i children}
          in
          let* _ = persist tx trunk in
          Ok (Some trunk)

    let rec shrink tx = function
      | Trunk {children; _} when BatFingerTree.size children = 1 ->
          Utilities.Result.fmap (shrink tx) (find' tx (BatFingerTree.get children 0))
      | node -> Ok node

    let remove tx key node =
      let open Utilities.Result in
      let* present = lookup tx key node in
      if Option.is_none present then Ok node
      else
        let* node = remove' tx key node in
        match node with
        | None ->
            let* _ = persist tx empty in
            Ok empty
        | Some node -> shrink tx node
  end

module type INTERFACE = functor (S : Abstract.Storage.STORAGE) (K : KEY) (V : VALUE) -> sig
  module Tree : module type of Make (S) (K)

  type address = Concepts.Hash.hash
  type node

  val with_batch :
    S.transaction ->
    (unit -> (node, Concepts.Condition.condition) result) ->
    (node, Concepts.Condition.condition) result

  val find : S.transaction -> address -> (node option, Concepts.Condition.condition) result
  val from : Tree.node -> node
  val into : node -> Tree.node
  val empty : node
  val empty_under : S.transaction -> (address, Concepts.Condition.condition) result
  val hash_of : node -> address
  val insert : S.transaction -> K.t -> V.t -> node -> (node, Concepts.Condition.condition) result
  val of_list : S.transaction -> (K.t * V.t) list -> (node, Concepts.Condition.condition) result
  val remove : S.transaction -> K.t -> node -> (node, Concepts.Condition.condition) result
  val lookup : S.transaction -> K.t -> node -> (V.t option, Concepts.Condition.condition) result
  val mem : S.transaction -> K.t -> node -> (bool, Concepts.Condition.condition) result

  val fold_left :
    S.transaction ->
    ('c -> K.t -> V.t -> 'c) ->
    'c ->
    node ->
    ('c, Concepts.Condition.condition) result

  val iter :
    S.transaction -> (K.t -> V.t -> 'a) -> node -> (unit, Concepts.Condition.condition) result

  val keys : S.transaction -> node -> (K.t BatFingerTree.t, Concepts.Condition.condition) result
end

module Interface : INTERFACE =
functor
  (S : Abstract.Storage.STORAGE)
  (K : KEY)
  (V : VALUE)
  ->
  struct
    module Tree = Make (S) (K)
    module SI = Storage.Make (S)

    type address = Tree.address
    type node = Tree.node

    open Utilities.Result

    let intern tx v =
      let data = V.encode v in
      let addr = Concepts.Hash.hash_of_blob data in
      let* () = S.put tx (S.Hash addr) data in
      Ok addr

    let retrieve tx addr = SI.get_req tx (S.Hash addr) |> fmap V.decode
    let find = Tree.find
    let from x = x
    let into x = x
    let empty = Tree.empty
    let empty_under = Tree.empty_under
    let hash_of = Tree.hash_of
    let with_batch = Tree.with_batch

    let insert tx k v node =
      let* addr = intern tx v in
      Tree.insert tx k addr node

    let of_list tx bindings =
      List.map (fun (k, v) -> intern tx v |> Result.map (fun addr -> k, addr)) bindings
      |> Utilities.List.sequence
      |> fmap (Tree.of_list tx)

    let remove = Tree.remove

    let lookup tx k node =
      let* addr = Tree.lookup tx k node in
      match addr with None -> Ok None | Some addr -> retrieve tx addr |> Result.map Option.some

    let mem tx k node = Tree.lookup tx k node |> Result.map Option.is_some

    let fold_left tx f acc node =
      Tree.fold_left tx
        (fun acc k addr ->
          let* acc = acc in
          let* v = retrieve tx addr in
          Ok (f acc k v) )
        (Ok acc) node
      |> Result.join

    let iter tx f node = fold_left tx (fun _ k v -> f k v |> ignore) () node
    let keys = Tree.keys
  end

module StringKey : KEY with type t = string = struct
  type t = string

  let encode s = String.to_bytes s |> Concepts.Blob.blob_of_bytes
  let compare s1 s2 = String.compare s1 s2 |> Utilities.Ordering.of_int
  let decode b = Concepts.Blob.bytes_of_blob b |> String.of_bytes |> Result.ok
end