Select the types of activity you want to include in your feed.
Fix bottler: skip git pull on empty tap repos
git pull --ff-only fails when the remote tap repo has no commits (empty repo, no main branch). Check rev-parse origin/main first; if it fails, skip the pull.
···228228 - Inclusion proof: sibling hashes from leaf to root *)
229229230230module Vds_rfc9162 : sig
231231- val in_memory : ?hash:hash -> unit -> vds
232232- (** [in_memory ?hash ()] creates a fresh in-memory RFC 9162 VDS. Data is lost
233233- when the process exits. Suitable for testing. *)
231231+ val in_memory : ?hash:hash -> ?max_entries:int -> unit -> vds
232232+ (** [in_memory ?hash ?max_entries ()] creates a fresh in-memory RFC 9162 VDS.
233233+ All leaf hashes are held in RAM; [max_entries] caps the number of entries
234234+ to prevent unbounded memory growth (default: 1,000,000 ~= 32 MB of
235235+ hashes). Data is lost when the process exits. *)
234236235237 val sqlite : ?hash:hash -> Sqlite.t -> vds
236238 (** [sqlite ?hash db] creates a persistent RFC 9162 VDS backed by SQLite.
237237- Tables are created automatically. Suitable for production. *)
239239+ Tables are created automatically. Leaf hashes are read from the database
240240+ on demand — only O(log n) are loaded per inclusion proof. Startup streams
241241+ the hash table to rebuild the compact root in O(n) time and O(log n)
242242+ memory. *)
238243239244 val import :
240245 string -> create:(hash:hash -> unit -> vds) -> (vds, string) result
+98-46
lib/vds.ml
···6363(* -- Shared RFC 9162 algorithms -- *)
64646565module Growable = struct
6666- type t = { mutable data : string array; mutable len : int }
6666+ type t = { mutable data : string array; mutable len : int; max_entries : int }
67676868- let create cap = { data = Array.make (max cap 16) ""; len = 0 }
6868+ let create ?(max_entries = 1_000_000) cap =
6969+ { data = Array.make (max cap 16) ""; len = 0; max_entries }
7070+6971 let length t = t.len
70727173 let get t i =
···7375 t.data.(i)
74767577 let push t v =
7676- if t.len = Array.length t.data then begin
7777- let new_cap = min (Array.length t.data * 2) Sys.max_array_length in
7878- if new_cap <= Array.length t.data then
7979- failwith "Growable: maximum array capacity reached";
8080- let new_data = Array.make new_cap "" in
8181- Array.blit t.data 0 new_data 0 t.len;
8282- Array.fill t.data 0 t.len "";
8383- t.data <- new_data
8484- end;
8585- t.data.(t.len) <- v;
8686- t.len <- t.len + 1
7878+ if t.len >= t.max_entries then
7979+ Error
8080+ (Fmt.str "maximum entry count reached (%d); see max_entries"
8181+ t.max_entries)
8282+ else begin
8383+ if t.len = Array.length t.data then begin
8484+ let new_cap =
8585+ min (min (Array.length t.data * 2) Sys.max_array_length) t.max_entries
8686+ in
8787+ let new_data = Array.make new_cap "" in
8888+ Array.blit t.data 0 new_data 0 t.len;
8989+ Array.fill t.data 0 t.len "";
9090+ t.data <- new_data
9191+ end;
9292+ t.data.(t.len) <- v;
9393+ t.len <- t.len + 1;
9494+ Ok ()
9595+ end
8796end
88978998(** Internal node cache. In an append-only tree, compute_root(offset, len) is
9099 immutable once all [len] leaves at [offset] exist. Cache hits make
91100 inclusion_path O(log n) instead of O(n). *)
101101+(** Internal node cache with bounded size.
102102+103103+ In an append-only tree, subtree hashes are immutable once computed. The
104104+ cache maps [(offset, length)] to the subtree root hash. On overflow, only
105105+ power-of-2-length entries are kept — these are "complete" subtrees that are
106106+ maximally reused and cheapest to lose (they'd be recomputed in O(1) from
107107+ two cached children). *)
92108module Node_cache = struct
109109+ let default_max_entries = 100_000
110110+93111 type t = {
94112 tbl : (int * int, string) Hashtbl.t;
95113 hash : Hash.t;
96114 get : int -> string;
115115+ max_entries : int;
97116 }
981179999- let create hash get = { tbl = Hashtbl.create 256; hash; get }
118118+ let v ?(max_entries = default_max_entries) hash get =
119119+ { tbl = Hashtbl.create 256; hash; get; max_entries }
120120+121121+ let is_power_of_2 n = n > 0 && n land (n - 1) = 0
122122+123123+ let evict t =
124124+ let to_keep = Hashtbl.create (Hashtbl.length t.tbl / 2) in
125125+ Hashtbl.iter
126126+ (fun ((_, len) as key) v ->
127127+ if is_power_of_2 len then Hashtbl.replace to_keep key v)
128128+ t.tbl;
129129+ Hashtbl.reset t.tbl;
130130+ Hashtbl.iter (Hashtbl.replace t.tbl) to_keep
100131101132 let rec compute_root t off len =
102133 if len = 0 then t.hash.Hash.digest ""
···117148 node_hash (compute_root t off split)
118149 (compute_root t (off + split) (len - split))
119150 in
151151+ if Hashtbl.length t.tbl >= t.max_entries then evict t;
120152 Hashtbl.replace t.tbl key h;
121153 h
122154···245277 else
246278 let leaf_h = Hash.leaf_hash_with t.hash value in
247279 let idx = Growable.length t.hashes in
248248- Growable.push t.hashes leaf_h;
249249- Compact.append t.compact leaf_h;
250250- Hashtbl.replace t.leaves key value;
251251- t.leaves_order <- key :: t.leaves_order;
252252- let n = Growable.length t.hashes in
253253- let path = Node_cache.inclusion_path t.ncache 0 n idx in
254254- let root = Compact.root t.compact ~empty_hash:t.empty_hash in
255255- Ok
256256- {
257257- leaf_index = idx;
258258- tree_size = n;
259259- root;
260260- path;
261261- leaf_hash = leaf_h;
262262- })
280280+ match Growable.push t.hashes leaf_h with
281281+ | Error e -> Error e
282282+ | Ok () ->
283283+ Compact.append t.compact leaf_h;
284284+ Hashtbl.replace t.leaves key value;
285285+ t.leaves_order <- key :: t.leaves_order;
286286+ let n = Growable.length t.hashes in
287287+ let path = Node_cache.inclusion_path t.ncache 0 n idx in
288288+ let root = Compact.root t.compact ~empty_hash:t.empty_hash in
289289+ Ok
290290+ {
291291+ leaf_index = idx;
292292+ tree_size = n;
293293+ root;
294294+ path;
295295+ leaf_hash = leaf_h;
296296+ })
263297264298 let export t =
265299 with_lock t (fun () ->
···274308 export_cbor ~hash:t.hash ~root:r ~entries)
275309 end
276310277277- let v ?(hash = Hash.sha256) () =
311311+ let v ?(hash = Hash.sha256) ?(max_entries = 1_000_000) () =
278312 let node_hash = Hash.node_hash_with hash in
279279- let hashes = Growable.create 256 in
313313+ let hashes = Growable.create ~max_entries 256 in
280314 v
281315 (module Impl)
282316 Impl.
···284318 hash;
285319 hashes;
286320 compact = Compact.create node_hash;
287287- ncache = Node_cache.create hash (Growable.get hashes);
321321+ ncache = Node_cache.v hash (Growable.get hashes);
288322 leaves = Hashtbl.create 256;
289323 leaves_order = [];
290324 empty_hash = hash.Hash.digest "";
···301335 hash : Hash.t;
302336 entries : Sqlite.Table.t;
303337 empty_hash : string;
304304- hashes : Growable.t;
338338+ mutable entry_count : int;
305339 compact : Compact.t;
306340 ncache : Node_cache.t;
307341 mu : Mutex.t;
···313347314348 let algorithm_id t = Hash.id t.hash
315349 let proof_format _ = Hash.Rfc9162
316316- let size t = with_lock t (fun () -> Growable.length t.hashes)
350350+ let size t = with_lock t (fun () -> t.entry_count)
317351318352 let root t =
319353 with_lock t (fun () -> Compact.root t.compact ~empty_hash:t.empty_hash)
320354321355 let lookup t ~key = with_lock t (fun () -> Sqlite.Table.find t.entries key)
322356357357+ (** Read a leaf hash from the scitt_hashes table by index (0-based). Rowids
358358+ are 1-based, so rowid = index + 1. *)
359359+ let get_hash db idx =
360360+ let rowid = Int64.of_int (idx + 1) in
361361+ match
362362+ Sqlite.fold_table db "scitt_hashes" ~init:None ~f:(fun rid values acc ->
363363+ if rid = rowid then
364364+ match values with [ Sqlite.Vblob h ] -> Some h | _ -> acc
365365+ else acc)
366366+ with
367367+ | Some h -> h
368368+ | None -> Fmt.failwith "scitt_hashes: missing rowid %Ld" rowid
369369+323370 let append t ~key ~value =
324371 with_lock t (fun () ->
325372 if Sqlite.Table.mem t.entries key then Error ("duplicate key: " ^ key)
···331378 [ Sqlite.Vblob leaf_h ]
332379 in
333380 Sqlite.Table.put t.entries key value;
334334- Growable.push t.hashes leaf_h;
335335- Compact.append t.compact leaf_h);
336336- let n = Growable.length t.hashes in
381381+ Compact.append t.compact leaf_h;
382382+ t.entry_count <- t.entry_count + 1);
383383+ let n = t.entry_count in
337384 let idx = n - 1 in
338385 let path = Node_cache.inclusion_path t.ncache 0 n idx in
339386 let root = Compact.root t.compact ~empty_hash:t.empty_hash in
···363410 with Failure _ -> ());
364411 let entries = Sqlite.Table.create db ~name:"scitt_entry" in
365412 let node_hash = Hash.node_hash_with hash in
366366- let hashes = Growable.create 256 in
367367- Sqlite.fold_table db "scitt_hashes" ~init:() ~f:(fun _rowid values () ->
368368- match values with
369369- | [ Sqlite.Vblob h ] -> Growable.push hashes h
370370- | _ -> ());
413413+ (* Count existing hashes and rebuild Compact by streaming — O(n) time,
414414+ O(log n) memory. No Growable array needed. *)
415415+ let entry_count = ref 0 in
416416+ let read_hash idx = Impl.get_hash db idx in
371417 let compact =
372372- Compact.rebuild ~get:(Growable.get hashes) ~len:(Growable.length hashes)
373373- node_hash
418418+ let c = Compact.create node_hash in
419419+ Sqlite.fold_table db "scitt_hashes" ~init:() ~f:(fun _rowid values () ->
420420+ match values with
421421+ | [ Sqlite.Vblob h ] ->
422422+ Compact.append c h;
423423+ incr entry_count
424424+ | _ -> ());
425425+ c
374426 in
375427 v
376428 (module Impl)
···380432 hash;
381433 entries;
382434 empty_hash = hash.Hash.digest "";
383383- hashes;
435435+ entry_count = !entry_count;
384436 compact;
385385- ncache = Node_cache.create hash (Growable.get hashes);
437437+ ncache = Node_cache.v hash read_hash;
386438 mu = Mutex.create ();
387439 }
388440end
+9-3
lib/vds.mli
···8484(** {1 Backends} *)
85858686module In_memory : sig
8787- val v : ?hash:Hash.t -> unit -> t
8888- (** [v ()] is a fresh in-memory RFC 9162 Merkle tree. Suitable for testing.
8787+ val v : ?hash:Hash.t -> ?max_entries:int -> unit -> t
8888+ (** [v ?max_entries ()] is a fresh in-memory RFC 9162 Merkle tree. All leaf
8989+ hashes are held in memory; at 32 bytes per hash, [max_entries] entries use
9090+ ~32 * [max_entries] bytes. Default: 1,000,000 (~32 MB).
89919092 {b Concurrency}: multicore-safe. All operations are serialized with an
9193 internal mutex. *)
···93959496module Sqlite : sig
9597 val v : ?hash:Hash.t -> Sqlite.t -> t
9696- (** [v db] is a durable RFC 9162 Merkle tree backed by SQLite.
9898+ (** [v db] is a durable RFC 9162 Merkle tree backed by SQLite. Leaf hashes are
9999+ stored in the [scitt_hashes] table and read on demand — only O(log n)
100100+ hashes are loaded per inclusion proof. Startup streams the hash table to
101101+ rebuild the O(log n) compact root state without loading all hashes into
102102+ memory.
9710398104 {b Concurrency}: multicore-safe. All operations are serialized with an
99105 internal mutex. Disk writes use SQLite transactions for atomicity. *)