• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MinaProtocol / mina / 767

04 Nov 2025 01:59PM UTC coverage: 32.374% (-4.5%) from 36.902%
767

push

buildkite

web-flow
Merge pull request #18063 from MinaProtocol/lyh/compat-into-dev-nov4-2025

Merge compatible into develop Nov. 4th 2025

87 of 228 new or added lines in 10 files covered. (38.16%)

3416 existing lines in 136 files now uncovered.

23591 of 72871 relevant lines covered (32.37%)

26590.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.88
/src/lib/syncable_ledger/syncable_ledger.ml
1
open Core_kernel
134✔
2
open Async_kernel
3
open Pipe_lib
4
open Network_peer
5

6
type Structured_log_events.t += Snarked_ledger_synced
UNCOV
7
  [@@deriving register_event { msg = "Snarked database sync'd. All done" }]
×
8

9
(** Run f recursively n times, starting with value r.
10
    e.g. funpow 3 f r = f (f (f r)) *)
UNCOV
11
let rec funpow n f r = if n > 0 then funpow (n - 1) f (f r) else r
×
12

13
module Query = struct
14
  [%%versioned
15
  module Stable = struct
16
    module V2 = struct
17
      type 'addr t =
134✔
18
        | What_child_hashes of 'addr * int
×
19
            (** What are the hashes of the children of this address? 
20
            If depth > 1 then we get the leaves of a subtree rooted
21
            at address and of the given depth. 
22
            For depth = 1 we have the simplest case with just the 2
23
            direct children.
24
            *)
UNCOV
25
        | What_contents of 'addr
×
26
            (** What accounts are at this address? addr must have depth
27
            tree_depth - account_subtree_height *)
UNCOV
28
        | Num_accounts
×
29
            (** How many accounts are there? Used to size data structure and
30
            figure out what part of the tree is filled in. *)
31
      [@@deriving sexp, yojson, hash, compare]
402✔
32
    end
33

34
    module V1 = struct
35
      type 'addr t =
134✔
36
        | What_child_hashes of 'addr
×
37
            (** What are the hashes of the children of this address? *)
38
        | What_contents of 'addr
×
39
            (** What accounts are at this address? addr must have depth
40
            tree_depth - account_subtree_height *)
41
        | Num_accounts
×
42
            (** How many accounts are there? Used to size data structure and
43
            figure out what part of the tree is filled in. *)
44
      [@@deriving sexp, yojson, hash, compare]
402✔
45

46
      let to_latest : 'a t -> 'a V2.t = function
47
        | What_child_hashes a ->
×
48
            What_child_hashes (a, 1)
49
        | What_contents a ->
×
50
            What_contents a
51
        | Num_accounts ->
×
52
            Num_accounts
53
    end
54
  end]
55
end
56

57
module Answer = struct
58
  [%%versioned
59
  module Stable = struct
60
    module V2 = struct
61
      type ('hash, 'account) t =
134✔
UNCOV
62
        | Child_hashes_are of
×
63
            'hash Mina_stdlib.Bounded_types.ArrayN4000.Stable.V1.t
×
64
            (** The requested addresses' children have these hashes.
65
            May be any power of 2 number of children, and not necessarily 
66
            immediate children  *)
67
        | Contents_are of 'account list
×
68
            (** The requested address has these accounts *)
69
        | Num_accounts of int * 'hash
×
70
            (** There are this many accounts and the smallest subtree that
71
                contains all non-empty nodes has this hash. *)
72
      [@@deriving sexp, yojson]
402✔
73
    end
74
  end]
75
end
76

77
type daemon_config = { max_subtree_depth : int; default_subtree_depth : int }
78

79
let create_config ~(compile_config : Mina_compile_config.t) ~max_subtree_depth
80
    ~default_subtree_depth () =
UNCOV
81
  { max_subtree_depth =
×
UNCOV
82
      Option.value ~default:compile_config.sync_ledger_max_subtree_depth
×
83
        max_subtree_depth
84
  ; default_subtree_depth =
UNCOV
85
      Option.value ~default:compile_config.sync_ledger_default_subtree_depth
×
86
        default_subtree_depth
87
  }
88

89
module type CONTEXT = sig
90
  val logger : Logger.t
91

92
  val ledger_sync_config : daemon_config
93
end
94

95
module type Inputs_intf = sig
96
  module Addr : module type of Merkle_address
97

98
  module Account : sig
99
    type t [@@deriving bin_io, sexp, yojson]
100
  end
101

102
  module Hash : Merkle_ledger.Intf.Hash with type account := Account.t
103

104
  module Root_hash : sig
105
    type t [@@deriving equal, sexp, yojson]
106

107
    val to_hash : t -> Hash.t
108
  end
109

110
  module MT :
111
    Merkle_ledger.Intf.SYNCABLE
112
      with type hash := Hash.t
113
       and type root_hash := Root_hash.t
114
       and type addr := Addr.t
115
       and type account := Account.t
116

117
  val account_subtree_height : int
118
end
119

120
module type S = sig
121
  type 'a t [@@deriving sexp]
122

123
  type merkle_tree
124

125
  type merkle_path
126

127
  type hash
128

129
  type root_hash
130

131
  type addr
132

133
  type diff
134

135
  type account
136

137
  type index = int
138

139
  type query
140

141
  type answer
142

143
  module Responder : sig
144
    type t
145

146
    val create :
147
         merkle_tree
148
      -> (query -> unit)
149
      -> context:(module CONTEXT)
150
      -> trust_system:Trust_system.t
151
      -> t
152

153
    val answer_query :
154
      t -> query Envelope.Incoming.t -> answer Or_error.t Deferred.t
155
  end
156

157
  val create :
158
       merkle_tree
159
    -> context:(module CONTEXT)
160
    -> trust_system:Trust_system.t
161
    -> 'a t
162

163
  val answer_writer :
164
       'a t
165
    -> (root_hash * query * answer Envelope.Incoming.t) Linear_pipe.Writer.t
166

167
  val query_reader : 'a t -> (root_hash * query) Linear_pipe.Reader.t
168

169
  val destroy : 'a t -> unit
170

171
  val new_goal :
172
       'a t
173
    -> root_hash
174
    -> data:'a
175
    -> equal:('a -> 'a -> bool)
176
    -> [ `Repeat | `New | `Update_data ]
177

178
  val peek_valid_tree : 'a t -> merkle_tree option
179

180
  val valid_tree : 'a t -> (merkle_tree * 'a) Deferred.t
181

182
  val wait_until_valid :
183
       'a t
184
    -> root_hash
185
    -> [ `Ok of merkle_tree | `Target_changed of root_hash option * root_hash ]
186
       Deferred.t
187

188
  val fetch :
189
       'a t
190
    -> root_hash
191
    -> data:'a
192
    -> equal:('a -> 'a -> bool)
193
    -> [ `Ok of merkle_tree | `Target_changed of root_hash option * root_hash ]
194
       Deferred.t
195

196
  val apply_or_queue_diff : 'a t -> diff -> unit
197

198
  val merkle_path_at_addr : 'a t -> addr -> merkle_path Or_error.t
199

200
  val get_account_at_addr : 'a t -> addr -> account Or_error.t
201
end
202

203
(*
204

205
Every node of the merkle tree is always in one of three states:
206

207
- Fresh.
208
  The current contents for this node in the MT match what we
209
  expect.
210
- Stale
211
  The current contents for this node in the MT do _not_ match
212
  what we expect.
213
- Unknown.
214
  We don't know what to expect yet.
215

216

217
Although every node conceptually has one of these states, and can
218
make a transition at any time, the syncer operates only along a
219
"frontier" of the tree, which consists of the deepest Stale nodes.
220

221
The goal of the ledger syncer is to make the root node be fresh,
222
starting from it being stale.
223

224
The syncer usually operates exclusively on these frontier nodes
225
and their direct children. However, the goal hash can change
226
while the syncer is running, and at that point every non-root node
227
conceptually becomes Unknown, and we need to restart. However, we
228
don't need to restart completely: in practice, only small portions
229
of the merkle tree change between goals, and we can re-use the "Stale"
230
nodes we already have if the expected hash doesn't change.
231

232
*)
233
(*
234
Note: while syncing, the underlying ledger is in an
235
indeterminate state. We're mutating hashes at internal
236
nodes without updating their children. In fact, we
237
don't even set all the hashes for the internal nodes!
238
(When we hit a height=N subtree, we don't do anything
239
with the hashes in the bottomost N-1 internal nodes).
240
*)
241

242
module Make (Inputs : Inputs_intf) : sig
243
  open Inputs
244

245
  include
246
    S
247
      with type merkle_tree := MT.t
248
       and type hash := Hash.t
249
       and type root_hash := Root_hash.t
250
       and type addr := Addr.t
251
       and type merkle_path := MT.path
252
       and type account := Account.t
253
       and type query := Addr.t Query.t
254
       and type answer := (Hash.t, Account.t) Answer.t
255
end = struct
256
  open Inputs
257

258
  type diff = unit
259

260
  type index = int
261

262
  type answer = (Hash.t, Account.t) Answer.t
263

264
  type query = Addr.t Query.t
265

266
  (* Provides addresses at an specific depth from this address *)
267
  let intermediate_range ledger_depth addr i =
UNCOV
268
    Array.init (1 lsl i) ~f:(fun idx ->
×
UNCOV
269
        Addr.extend_exn ~ledger_depth addr ~num_bits:i (Int64.of_int idx) )
×
270

271
  module Responder = struct
272
    type t =
273
      { mt : MT.t
274
      ; f : query -> unit
275
      ; context : (module CONTEXT)
276
      ; trust_system : Trust_system.t
277
      }
278

279
    let create :
280
           MT.t
281
        -> (query -> unit)
282
        -> context:(module CONTEXT)
283
        -> trust_system:Trust_system.t
284
        -> t =
UNCOV
285
     fun mt f ~context ~trust_system -> { mt; f; context; trust_system }
×
286

287
    let answer_query :
288
        t -> query Envelope.Incoming.t -> answer Or_error.t Deferred.t =
289
     fun { mt; f; context; trust_system } query_envelope ->
UNCOV
290
      let open (val context) in
×
291
      let open Trust_system in
292
      let ledger_depth = MT.depth mt in
UNCOV
293
      let sender = Envelope.Incoming.sender query_envelope in
×
UNCOV
294
      let query = Envelope.Incoming.data query_envelope in
×
UNCOV
295
      f query ;
×
UNCOV
296
      let response_or_punish =
×
297
        match query with
UNCOV
298
        | What_contents a ->
×
UNCOV
299
            if Addr.height ~ledger_depth a > account_subtree_height then
×
300
              Either.Second
×
301
                ( Actions.Violated_protocol
302
                , Some
303
                    ( "Requested too big of a subtree at once"
304
                    , [ ("addr", Addr.to_yojson a) ] ) )
×
305
            else
UNCOV
306
              let addresses_and_accounts =
×
307
                List.sort ~compare:(fun (addr1, _) (addr2, _) ->
UNCOV
308
                    Addr.compare addr1 addr2 )
×
UNCOV
309
                @@ MT.get_all_accounts_rooted_at_exn mt a
×
310
                (* can't actually throw *)
311
              in
UNCOV
312
              let addresses, accounts = List.unzip addresses_and_accounts in
×
UNCOV
313
              if List.is_empty addresses then
×
314
                (* Peer should know what portions of the tree are full from the
315
                   Num_accounts query. *)
316
                Either.Second
×
317
                  ( Actions.Violated_protocol
318
                  , Some
319
                      ("Requested empty subtree", [ ("addr", Addr.to_yojson a) ])
×
320
                  )
321
              else
UNCOV
322
                let first_address, rest_address =
×
UNCOV
323
                  (List.hd_exn addresses, List.tl_exn addresses)
×
324
                in
325
                let missing_address, is_compact =
326
                  List.fold rest_address
UNCOV
327
                    ~init:(Addr.next first_address, true)
×
328
                    ~f:(fun (expected_address, is_compact) actual_address ->
UNCOV
329
                      if
×
330
                        is_compact
UNCOV
331
                        && [%equal: Addr.t option] expected_address
×
332
                             (Some actual_address)
UNCOV
333
                      then (Addr.next actual_address, true)
×
334
                      else (expected_address, false) )
×
335
                in
336
                if not is_compact then (
×
337
                  (* indicates our ledger is invalid somehow. *)
338
                  [%log fatal]
×
339
                    ~metadata:
340
                      [ ( "missing_address"
341
                        , Addr.to_yojson (Option.value_exn missing_address) )
×
342
                      ; ( "addresses_and_accounts"
343
                        , `List
344
                            (List.map addresses_and_accounts
×
345
                               ~f:(fun (addr, account) ->
346
                                 `Tuple
×
347
                                   [ Addr.to_yojson addr
×
348
                                   ; Account.to_yojson account
×
349
                                   ] ) ) )
350
                      ]
351
                    "Missing an account at address: $missing_address inside \
352
                     the list: $addresses_and_accounts" ;
353
                  assert false )
×
UNCOV
354
                else Either.First (Answer.Contents_are accounts)
×
UNCOV
355
        | Num_accounts ->
×
356
            let len = MT.num_accounts mt in
UNCOV
357
            let height = Int.ceil_log2 len in
×
358
            (* FIXME: bug when height=0 https://github.com/o1-labs/nanobit/issues/365 *)
UNCOV
359
            let content_root_addr =
×
360
              funpow
UNCOV
361
                (MT.depth mt - height)
×
362
                (fun a ->
UNCOV
363
                  Addr.child_exn ~ledger_depth a Mina_stdlib.Direction.Left )
×
UNCOV
364
                (Addr.root ())
×
365
            in
UNCOV
366
            Either.First
×
367
              (Num_accounts
UNCOV
368
                 (len, MT.get_inner_hash_at_addr_exn mt content_root_addr) )
×
UNCOV
369
        | What_child_hashes (a, subtree_depth) -> (
×
370
            match subtree_depth with
UNCOV
371
            | n when n >= 1 -> (
×
372
                let subtree_depth =
373
                  min n ledger_sync_config.max_subtree_depth
374
                in
UNCOV
375
                let ledger_depth = MT.depth mt in
×
UNCOV
376
                let addresses =
×
377
                  intermediate_range ledger_depth a subtree_depth
378
                in
UNCOV
379
                match
×
380
                  Or_error.try_with (fun () ->
UNCOV
381
                      let get_hash a = MT.get_inner_hash_at_addr_exn mt a in
×
382
                      let hashes = Array.map addresses ~f:get_hash in
UNCOV
383
                      Answer.Child_hashes_are hashes )
×
384
                with
UNCOV
385
                | Ok answer ->
×
386
                    Either.First answer
387
                | Error e ->
×
388
                    [%log error]
×
389
                      ~metadata:[ ("error", Error_json.error_to_yojson e) ]
×
390
                      "When handling What_child_hashes request, the following \
391
                       error happended: $error" ;
392
                    Either.Second
×
393
                      ( Actions.Violated_protocol
394
                      , Some
395
                          ( "Invalid address in What_child_hashes request"
396
                          , [ ("addr", Addr.to_yojson a) ] ) ) )
×
397
            | _ ->
×
398
                [%log error]
×
399
                  "When handling What_child_hashes request, the depth was \
400
                   outside the valid range" ;
401
                Either.Second
×
402
                  ( Actions.Violated_protocol
403
                  , Some
404
                      ( "Invalid depth requested in What_child_hashes request"
405
                      , [ ("addr", Addr.to_yojson a) ] ) ) )
×
406
      in
407

408
      match response_or_punish with
UNCOV
409
      | Either.First answer ->
×
410
          Deferred.return @@ Ok answer
411
      | Either.Second action ->
×
412
          let%map _ =
413
            record_envelope_sender trust_system logger sender action
×
414
          in
415
          let err =
×
416
            Option.value_map ~default:"Violated protocol" (snd action) ~f:fst
×
417
          in
418
          Or_error.error_string err
×
419
  end
420

421
  type 'a t =
422
    { mutable desired_root : Root_hash.t option
423
    ; mutable auxiliary_data : 'a option
424
    ; tree : MT.t
425
    ; trust_system : Trust_system.t
426
    ; answers :
427
        (Root_hash.t * query * answer Envelope.Incoming.t) Linear_pipe.Reader.t
428
    ; answer_writer :
429
        (Root_hash.t * query * answer Envelope.Incoming.t) Linear_pipe.Writer.t
430
    ; queries : (Root_hash.t * query) Linear_pipe.Writer.t
431
    ; query_reader : (Root_hash.t * query) Linear_pipe.Reader.t
432
    ; waiting_parents : Hash.t Addr.Table.t
433
          (** Addresses we are waiting for the children of, and the expected
434
              hash of the node with the address. *)
435
    ; waiting_content : Hash.t Addr.Table.t
436
    ; mutable validity_listener :
437
        [ `Ok | `Target_changed of Root_hash.t option * Root_hash.t ] Ivar.t
438
    ; context : (module CONTEXT)
439
    }
440

441
  let t_of_sexp _ = failwith "t_of_sexp: not implemented"
×
442

443
  let sexp_of_t _ = failwith "sexp_of_t: not implemented"
×
444

UNCOV
445
  let desired_root_exn { desired_root; _ } = desired_root |> Option.value_exn
×
446

447
  let destroy t =
448
    Linear_pipe.close_read t.answers ;
×
449
    Linear_pipe.close_read t.query_reader
×
450

UNCOV
451
  let answer_writer t = t.answer_writer
×
452

UNCOV
453
  let query_reader t = t.query_reader
×
454

455
  let expect_children : 'a t -> Addr.t -> Hash.t -> unit =
456
   fun t parent_addr expected ->
UNCOV
457
    let open (val t.context) in
×
UNCOV
458
    [%log trace]
×
459
      ~metadata:
UNCOV
460
        [ ("parent_address", Addr.to_yojson parent_addr)
×
UNCOV
461
        ; ("hash", Hash.to_yojson expected)
×
462
        ]
463
      "Expecting children parent $parent_address, expected: $hash" ;
UNCOV
464
    Addr.Table.add_exn t.waiting_parents ~key:parent_addr ~data:expected
×
465

466
  let expect_content : 'a t -> Addr.t -> Hash.t -> unit =
467
   fun t addr expected ->
UNCOV
468
    let open (val t.context) in
×
UNCOV
469
    [%log trace]
×
470
      ~metadata:
UNCOV
471
        [ ("address", Addr.to_yojson addr); ("hash", Hash.to_yojson expected) ]
×
472
      "Expecting content addr $address, expected: $hash" ;
UNCOV
473
    Addr.Table.add_exn t.waiting_content ~key:addr ~data:expected
×
474

475
  (** Given an address and the accounts below that address, fill in the tree
476
      with them. *)
477
  let add_content :
478
         'a t
479
      -> Addr.t
480
      -> Account.t list
481
      -> [ `Success
482
         | `Hash_mismatch of Hash.t * Hash.t  (** expected hash, actual *) ] =
483
   fun t addr content ->
UNCOV
484
    let open (val t.context) in
×
485
    let expected = Addr.Table.find_exn t.waiting_content addr in
486
    (* TODO #444 should we batch all the updates and do them at the end? *)
487
    (* We might write the wrong data to the underlying ledger here, but if so
488
       we'll requeue the address and it'll be overwritten. *)
UNCOV
489
    MT.set_all_accounts_rooted_at_exn t.tree addr content ;
×
UNCOV
490
    Addr.Table.remove t.waiting_content addr ;
×
UNCOV
491
    [%log trace]
×
492
      ~metadata:
UNCOV
493
        [ ("address", Addr.to_yojson addr); ("hash", Hash.to_yojson expected) ]
×
494
      "Found content addr $address, with hash $hash, removing from waiting \
495
       content" ;
UNCOV
496
    let actual = MT.get_inner_hash_at_addr_exn t.tree addr in
×
UNCOV
497
    if Hash.equal actual expected then `Success
×
498
    else `Hash_mismatch (expected, actual)
×
499

500
  (* Merges each 2 contigous nodes, halving the size of the array *)
501
  let merge_siblings : Hash.t array -> index -> Hash.t array =
502
   fun nodes height ->
UNCOV
503
    let len = Array.length nodes in
×
504
    if len mod 2 <> 0 then failwith "length must be even" ;
×
UNCOV
505
    let half_len = len / 2 in
×
UNCOV
506
    let f i = Hash.merge ~height nodes.(2 * i) nodes.((2 * i) + 1) in
×
507
    Array.init half_len ~f
508

509
  (* Assumes nodes to be a power of 2 and merges them into their common root *)
510
  let rec merge_many : Hash.t array -> index -> Hash.t =
511
   fun nodes height ->
UNCOV
512
    let len = Array.length nodes in
×
UNCOV
513
    match len with
×
UNCOV
514
    | 1 ->
×
515
        nodes.(0)
UNCOV
516
    | _ ->
×
517
        let half = merge_siblings nodes height in
UNCOV
518
        merge_many half (height + 1)
×
519

520
  let merge_many : Hash.t array -> index -> index -> Hash.t =
521
   fun nodes height subtree_depth ->
UNCOV
522
    let bottom_height = height - subtree_depth in
×
523
    let hash = merge_many nodes bottom_height in
UNCOV
524
    hash
×
525

526
  (* Adds the subtree given as the 2^k subtree leaves with the given prefix address *)
527
  (* Returns next nodes to be checked *)
528
  let add_subtree :
529
         'a t
530
      -> Addr.t
531
      -> Hash.t array
532
      -> int
533
      -> [ `Good of (Addr.t * Hash.t) array
534
         | `Hash_mismatch of Hash.t * Hash.t
535
         | `Invalid_length ] =
536
   fun t addr nodes requested_depth ->
UNCOV
537
    let open (val t.context) in
×
538
    let len = Array.length nodes in
UNCOV
539
    let is_power = Int.is_pow2 len in
×
UNCOV
540
    let is_more_than_two = len >= 2 in
×
541
    let subtree_depth = Int.ceil_log2 len in
UNCOV
542
    let less_than_requested = subtree_depth <= requested_depth in
×
UNCOV
543
    let valid_length = is_power && is_more_than_two && less_than_requested in
×
544
    if valid_length then
UNCOV
545
      let ledger_depth = MT.depth t.tree in
×
UNCOV
546
      let expected =
×
547
        Option.value_exn ~message:"Forgot to wait for a node"
UNCOV
548
          (Addr.Table.find t.waiting_parents addr)
×
549
      in
UNCOV
550
      let merged =
×
UNCOV
551
        merge_many nodes (ledger_depth - Addr.depth addr) subtree_depth
×
552
      in
UNCOV
553
      if Hash.equal expected merged then (
×
554
        Addr.Table.remove t.waiting_parents addr ;
UNCOV
555
        let addresses = intermediate_range ledger_depth addr subtree_depth in
×
UNCOV
556
        let addresses_and_hashes = Array.zip_exn addresses nodes in
×
557

558
        (* Filter to fetch only those that differ *)
UNCOV
559
        let should_fetch_children addr hash =
×
UNCOV
560
          not @@ Hash.equal (MT.get_inner_hash_at_addr_exn t.tree addr) hash
×
561
        in
562
        let subtrees_to_fetch =
563
          addresses_and_hashes
UNCOV
564
          |> Array.filter ~f:(Tuple2.uncurry should_fetch_children)
×
565
        in
UNCOV
566
        `Good subtrees_to_fetch )
×
567
      else `Hash_mismatch (expected, merged)
×
568
    else `Invalid_length
×
569

570
  let all_done t =
UNCOV
571
    let open (val t.context) in
×
UNCOV
572
    if not (Root_hash.equal (MT.merkle_root t.tree) (desired_root_exn t)) then
×
573
      failwith "We finished syncing, but made a mistake somewhere :("
×
UNCOV
574
    else (
×
575
      if Ivar.is_full t.validity_listener then
576
        [%log error] "Ivar.fill bug is here!" ;
×
UNCOV
577
      Ivar.fill t.validity_listener `Ok )
×
578

579
  (** Compute the hash of an empty tree of the specified height. *)
580
  let empty_hash_at_height h =
UNCOV
581
    let rec go prev ctr =
×
UNCOV
582
      if ctr = h then prev else go (Hash.merge ~height:ctr prev prev) (ctr + 1)
×
583
    in
584
    go Hash.empty_account 0
585

586
  (** Given the hash of the smallest subtree that contains all accounts, the
587
      height of that hash in the tree and the height of the whole tree, compute
588
      the hash of the whole tree. *)
589
  let complete_with_empties hash start_height result_height =
UNCOV
590
    let rec go cur_empty prev_hash height =
×
UNCOV
591
      if height = result_height then prev_hash
×
592
      else
UNCOV
593
        let cur = Hash.merge ~height prev_hash cur_empty in
×
UNCOV
594
        let next_empty = Hash.merge ~height cur_empty cur_empty in
×
UNCOV
595
        go next_empty cur (height + 1)
×
596
    in
UNCOV
597
    go (empty_hash_at_height start_height) hash start_height
×
598

599
  (** Given an address and the hash of the corresponding subtree, start getting
600
      the children.
601
  *)
602
  let handle_node t addr exp_hash =
UNCOV
603
    let open (val t.context) in
×
UNCOV
604
    if Addr.depth addr >= MT.depth t.tree - account_subtree_height then (
×
605
      expect_content t addr exp_hash ;
UNCOV
606
      Linear_pipe.write_without_pushback_if_open t.queries
×
UNCOV
607
        (desired_root_exn t, What_contents addr) )
×
UNCOV
608
    else (
×
609
      expect_children t addr exp_hash ;
UNCOV
610
      Linear_pipe.write_without_pushback_if_open t.queries
×
UNCOV
611
        ( desired_root_exn t
×
612
        , What_child_hashes (addr, ledger_sync_config.default_subtree_depth) ) )
613

614
  (** Handle the initial Num_accounts message, starting the main syncing
615
      process. *)
616
  let handle_num_accounts :
617
      'a t -> int -> Hash.t -> [ `Success | `Hash_mismatch of Hash.t * Hash.t ]
618
      =
619
   fun t n content_hash ->
UNCOV
620
    let rh = Root_hash.to_hash (desired_root_exn t) in
×
UNCOV
621
    let height = Int.ceil_log2 n in
×
622
    (* FIXME: bug when height=0 https://github.com/o1-labs/nanobit/issues/365 *)
UNCOV
623
    let actual = complete_with_empties content_hash height (MT.depth t.tree) in
×
UNCOV
624
    if Hash.equal actual rh then (
×
625
      Addr.Table.clear t.waiting_parents ;
626
      (* We should use this information to set the empty account slots empty and
627
         start syncing at the content root. See #1972. *)
UNCOV
628
      Addr.Table.clear t.waiting_content ;
×
UNCOV
629
      handle_node t (Addr.root ()) rh ;
×
UNCOV
630
      `Success )
×
631
    else `Hash_mismatch (rh, actual)
×
632

633
  let main_loop t =
UNCOV
634
    let open (val t.context) in
×
635
    let handle_answer :
636
           Root_hash.t
637
           * Addr.t Query.t
638
           * (Hash.t, Account.t) Answer.t Envelope.Incoming.t
639
        -> unit Deferred.t =
640
     fun (root_hash, query, env) ->
641
      (* NOTE: think about synchronization here. This is deferred now, so
642
         the t and the underlying ledger can change while processing is
643
         happening. *)
UNCOV
644
      let already_done =
×
645
        match Ivar.peek t.validity_listener with Some `Ok -> true | _ -> false
×
646
      in
647
      let sender = Envelope.Incoming.sender env in
UNCOV
648
      let answer = Envelope.Incoming.data env in
×
UNCOV
649
      [%log trace]
×
650
        ~metadata:
UNCOV
651
          [ ("root_hash", Root_hash.to_yojson root_hash)
×
UNCOV
652
          ; ("query", Query.to_yojson Addr.to_yojson query)
×
653
          ]
654
        "Handle answer for $root_hash" ;
655
      if not (Root_hash.equal root_hash (desired_root_exn t)) then (
×
656
        [%log trace]
×
657
          ~metadata:
658
            [ ("desired_hash", Root_hash.to_yojson (desired_root_exn t))
×
659
            ; ("ignored_hash", Root_hash.to_yojson root_hash)
×
660
            ]
661
          "My desired root was $desired_hash, so I'm ignoring $ignored_hash" ;
662
        Deferred.unit )
×
663
      else if already_done then (
×
664
        (* This can happen if we asked for hashes that turn out to be equal in
665
           underlying ledger and the target. *)
666
        [%log debug] "Got sync response when we're already finished syncing" ;
×
667
        Deferred.unit )
×
668
      else
UNCOV
669
        let open Trust_system in
×
670
        (* If a peer misbehaves we still need the information we asked them for,
671
           so requeue in that case. *)
672
        let requeue_query () =
673
          Linear_pipe.write_without_pushback_if_open t.queries (root_hash, query)
×
674
        in
675
        let credit_fulfilled_request () =
UNCOV
676
          record_envelope_sender t.trust_system logger sender
×
677
            ( Actions.Fulfilled_request
678
            , Some
679
                ( "sync ledger query $query"
UNCOV
680
                , [ ("query", Query.to_yojson Addr.to_yojson query) ] ) )
×
681
        in
682
        let%bind _ =
683
          match (query, answer) with
UNCOV
684
          | Query.What_contents addr, Answer.Contents_are leaves -> (
×
685
              match add_content t addr leaves with
UNCOV
686
              | `Success ->
×
UNCOV
687
                  credit_fulfilled_request ()
×
688
              | `Hash_mismatch (expected, actual) ->
×
689
                  let%map () =
690
                    record_envelope_sender t.trust_system logger sender
×
691
                      ( Actions.Sent_bad_hash
692
                      , Some
693
                          ( "sent accounts $accounts for address $addr, they \
694
                             hash to $actual but we expected $expected"
695
                          , [ ( "accounts"
696
                              , `List (List.map ~f:Account.to_yojson leaves) )
×
697
                            ; ("addr", Addr.to_yojson addr)
×
698
                            ; ("actual", Hash.to_yojson actual)
×
699
                            ; ("expected", Hash.to_yojson expected)
×
700
                            ] ) )
701
                  in
702
                  requeue_query () )
×
UNCOV
703
          | Query.Num_accounts, Answer.Num_accounts (count, content_root) -> (
×
704
              match handle_num_accounts t count content_root with
UNCOV
705
              | `Success ->
×
UNCOV
706
                  credit_fulfilled_request ()
×
707
              | `Hash_mismatch (expected, actual) ->
×
708
                  let%map () =
709
                    record_envelope_sender t.trust_system logger sender
×
710
                      ( Actions.Sent_bad_hash
711
                      , Some
712
                          ( "Claimed num_accounts $count, content root hash \
713
                             $content_root_hash, that implies a root hash of \
714
                             $actual, we expected $expected"
715
                          , [ ("count", `Int count)
716
                            ; ("content_root_hash", Hash.to_yojson content_root)
×
717
                            ; ("actual", Hash.to_yojson actual)
×
718
                            ; ("expected", Hash.to_yojson expected)
×
719
                            ] ) )
720
                  in
721
                  requeue_query () )
×
UNCOV
722
          | ( Query.What_child_hashes (address, requested_depth)
×
723
            , Answer.Child_hashes_are hashes ) -> (
724
              match add_subtree t address hashes requested_depth with
725
              | `Hash_mismatch (expected, actual) ->
×
726
                  let%map () =
727
                    record_envelope_sender t.trust_system logger sender
×
728
                      ( Actions.Sent_bad_hash
729
                      , Some
730
                          ( "hashes sent for subtree on address $address merge \
731
                             to $actual_merge but we expected $expected_merge"
732
                          , [ ("actual_merge", Hash.to_yojson actual)
×
733
                            ; ("expected_merge", Hash.to_yojson expected)
×
734
                            ] ) )
735
                  in
736
                  requeue_query ()
×
737
              | `Invalid_length ->
×
738
                  let%map () =
739
                    record_envelope_sender t.trust_system logger sender
×
740
                      ( Actions.Sent_bad_hash
741
                      , Some
742
                          ( "hashes sent for subtree on address $address must \
743
                             be a power of 2 in the range 2-2^$depth"
744
                          , [ ( "depth"
745
                              , `Int ledger_sync_config.max_subtree_depth )
746
                            ] ) )
747
                  in
748
                  requeue_query ()
×
UNCOV
749
              | `Good children_to_verify ->
×
750
                  Array.iter children_to_verify ~f:(fun (addr, hash) ->
UNCOV
751
                      handle_node t addr hash ) ;
×
UNCOV
752
                  credit_fulfilled_request () )
×
753
          | query, answer ->
×
754
              let%map () =
755
                record_envelope_sender t.trust_system logger sender
×
756
                  ( Actions.Violated_protocol
757
                  , Some
758
                      ( "Answered question we didn't ask! Query was $query \
759
                         answer was $answer"
760
                      , [ ("query", Query.to_yojson Addr.to_yojson query)
×
761
                        ; ( "answer"
762
                          , Answer.to_yojson Hash.to_yojson Account.to_yojson
×
763
                              answer )
764
                        ] ) )
765
              in
766
              requeue_query ()
×
767
        in
UNCOV
768
        if
×
769
          Root_hash.equal
UNCOV
770
            (Option.value_exn t.desired_root)
×
UNCOV
771
            (MT.merkle_root t.tree)
×
UNCOV
772
        then (
×
UNCOV
773
          [%str_log trace] Snarked_ledger_synced ;
×
UNCOV
774
          all_done t ) ;
×
UNCOV
775
        Deferred.unit
×
776
    in
777
    Linear_pipe.iter t.answers ~f:handle_answer
778

779
  let new_goal t h ~data ~equal =
UNCOV
780
    let open (val t.context) in
×
781
    let should_skip =
782
      match t.desired_root with
UNCOV
783
      | None ->
×
784
          false
785
      | Some h' ->
×
786
          Root_hash.equal h h'
×
787
    in
UNCOV
788
    if not should_skip then (
×
789
      Option.iter t.desired_root ~f:(fun root_hash ->
790
          [%log debug]
×
791
            ~metadata:
792
              [ ("old_root_hash", Root_hash.to_yojson root_hash)
×
793
              ; ("new_root_hash", Root_hash.to_yojson h)
×
794
              ]
795
            "New_goal: changing target from $old_root_hash to $new_root_hash" ) ;
UNCOV
796
      Ivar.fill_if_empty t.validity_listener
×
797
        (`Target_changed (t.desired_root, h)) ;
UNCOV
798
      t.validity_listener <- Ivar.create () ;
×
799
      t.desired_root <- Some h ;
800
      t.auxiliary_data <- Some data ;
801
      Linear_pipe.write_without_pushback_if_open t.queries (h, Num_accounts) ;
UNCOV
802
      `New )
×
803
    else if
×
804
      Option.fold t.auxiliary_data ~init:false ~f:(fun _ saved_data ->
805
          equal data saved_data )
×
806
    then (
×
807
      [%log debug] "New_goal to same hash, not doing anything" ;
×
808
      `Repeat )
×
809
    else (
×
810
      t.auxiliary_data <- Some data ;
811
      `Update_data )
812

813
  let rec valid_tree t =
814
    match%bind Ivar.read t.validity_listener with
×
815
    | `Ok ->
×
816
        return (t.tree, Option.value_exn t.auxiliary_data)
×
817
    | `Target_changed _ ->
×
818
        valid_tree t
819

820
  let peek_valid_tree t =
821
    Option.bind (Ivar.peek t.validity_listener) ~f:(function
×
822
      | `Ok ->
×
823
          Some t.tree
824
      | `Target_changed _ ->
×
825
          None )
826

827
  let wait_until_valid t h =
UNCOV
828
    if not (Root_hash.equal h (desired_root_exn t)) then
×
829
      return (`Target_changed (t.desired_root, h))
×
830
    else
UNCOV
831
      Deferred.map (Ivar.read t.validity_listener) ~f:(function
×
832
        | `Target_changed payload ->
×
833
            `Target_changed payload
UNCOV
834
        | `Ok ->
×
835
            `Ok t.tree )
836

837
  let fetch t rh ~data ~equal =
UNCOV
838
    ignore (new_goal t rh ~data ~equal : [ `New | `Repeat | `Update_data ]) ;
×
839
    wait_until_valid t rh
840

841
  let create mt ~context ~trust_system =
UNCOV
842
    let qr, qw = Linear_pipe.create () in
×
UNCOV
843
    let ar, aw = Linear_pipe.create () in
×
UNCOV
844
    let t =
×
845
      { desired_root = None
846
      ; auxiliary_data = None
847
      ; tree = mt
848
      ; trust_system
849
      ; answers = ar
850
      ; answer_writer = aw
851
      ; queries = qw
852
      ; query_reader = qr
UNCOV
853
      ; waiting_parents = Addr.Table.create ()
×
UNCOV
854
      ; waiting_content = Addr.Table.create ()
×
UNCOV
855
      ; validity_listener = Ivar.create ()
×
856
      ; context
857
      }
858
    in
UNCOV
859
    don't_wait_for (main_loop t) ;
×
UNCOV
860
    t
×
861

862
  let apply_or_queue_diff _ _ =
863
    (* Need some interface for the diffs, not sure the layering is right here. *)
864
    failwith "todo"
×
865

866
  let merkle_path_at_addr _ = failwith "no"
×
867

868
  let get_account_at_addr _ = failwith "no"
×
869
end
268✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc