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

MinaProtocol / mina / 1239

31 Mar 2026 12:24PM UTC coverage: 38.104% (-3.7%) from 41.817%
1239

push

buildkite

web-flow
Merge pull request #18591 from MinaProtocol/copilot/fix-error-message-blockchain-verification

0 of 1 new or added line in 1 file covered. (0.0%)

2995 existing lines in 128 files now uncovered.

28619 of 75108 relevant lines covered (38.1%)

51610.03 hits per line

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

0.56
/src/lib/bootstrap_controller/bootstrap_controller.ml
1
(* Only show stdout for failed inline tests. *)
142✔
2
open Core
3
open Async
4
open Mina_base
5
open Mina_state
6
open Pipe_lib.Strict_pipe
7
open Network_peer
8
open Mina_stdlib
9
module Ledger = Mina_ledger.Ledger
10
module Root_ledger = Mina_ledger.Root
11
module Sync_ledger = Mina_ledger.Sync_ledger
12
module Transition_cache = Transition_cache
13

14
module type CONTEXT = sig
15
  val logger : Logger.t
16

17
  val precomputed_values : Precomputed_values.t
18

19
  val constraint_constants : Genesis_constants.Constraint_constants.t
20

21
  val consensus_constants : Consensus.Constants.t
22

23
  val ledger_sync_config : Syncable_ledger.daemon_config
24

25
  val proof_cache_db : Proof_cache_tag.cache_db
26

27
  val signature_kind : Mina_signature_kind.t
28
end
29

30
type Structured_log_events.t += Bootstrap_complete
UNCOV
31
  [@@deriving register_event { msg = "Bootstrap state: complete." }]
×
32

33
type t =
34
  { context : (module CONTEXT)
35
  ; trust_system : Trust_system.t
36
  ; verifier : Verifier.t
37
  ; mutable best_seen_transition : Mina_block.initial_valid_block
38
  ; mutable current_root : Mina_block.initial_valid_block
39
  ; network : Mina_networking.t
40
  ; mutable num_of_root_snarked_ledger_retargeted : int
41
  }
42

43
(** An auxiliary data structure for collecting various metrics for bootstrap controller. *)
44
type bootstrap_cycle_stats =
×
45
  { cycle_result : string
×
46
  ; sync_ledger_time : Time.Span.t
×
47
  ; staged_ledger_data_download_time : Time.Span.t
×
48
  ; staged_ledger_construction_time : Time.Span.t option
×
49
  ; local_state_sync_required : bool
×
50
  ; local_state_sync_time : Time.Span.t option
×
51
  }
52
[@@deriving to_yojson]
53

54
let time_deferred deferred =
55
  let start_time = Time.now () in
×
56
  let%map result = deferred in
57
  let end_time = Time.now () in
×
58
  (Time.diff end_time start_time, result)
×
59

60
let worth_getting_root ({ context = (module Context); _ } as t) candidate =
61
  let module Consensus_context = struct
×
62
    include Context
63

64
    let logger =
65
      Logger.extend logger
×
66
        [ ( "selection_context"
67
          , `String "Bootstrap_controller.worth_getting_root" )
68
        ]
69
  end in
70
  Consensus.Hooks.equal_select_status `Take
71
  @@ Consensus.Hooks.select
72
       ~context:(module Consensus_context)
73
       ~existing:
74
         ( t.best_seen_transition |> Mina_block.Validation.block_with_hash
×
75
         |> With_hash.map ~f:Mina_block.consensus_state )
×
76
       ~candidate
77

78
let received_bad_proof ({ context = (module Context); _ } as t) host e =
79
  let open Context in
×
80
  Trust_system.(
81
    record t.trust_system logger host
82
      Actions.
83
        ( Violated_protocol
84
        , Some
85
            ( "Bad ancestor proof: $error"
86
            , [ ("error", Error_json.error_to_yojson e) ] ) ))
×
87

88
let done_syncing_root root_sync_ledger =
89
  Option.is_some (Sync_ledger.Root.peek_valid_tree root_sync_ledger)
×
90

91
let should_sync ~root_sync_ledger t candidate_state =
92
  (not @@ done_syncing_root root_sync_ledger)
×
93
  && worth_getting_root t candidate_state
×
94

95
(** Update [Synced_ledger]'s target and [best_seen_transition] and [current_root] accordingly. *)
96
let start_sync_job_with_peer ~sender ~root_sync_ledger
97
    ({ context = (module Context); _ } as t) peer_best_tip peer_root =
98
  let open Context in
×
99
  let%bind () =
100
    Trust_system.(
101
      record t.trust_system logger sender
×
102
        Actions.
103
          ( Fulfilled_request
104
          , Some ("Received verified peer root and best tip", []) ))
105
  in
106
  t.best_seen_transition <- peer_best_tip ;
×
107
  t.current_root <- peer_root ;
108
  let blockchain_state =
109
    t.current_root |> Mina_block.Validation.block |> Mina_block.header
×
110
    |> Mina_block.Header.protocol_state |> Protocol_state.blockchain_state
×
111
  in
112
  let expected_staged_ledger_hash =
×
113
    blockchain_state |> Blockchain_state.staged_ledger_hash
114
  in
115
  let snarked_ledger_hash =
×
116
    blockchain_state |> Blockchain_state.snarked_ledger_hash
117
  in
118
  return
×
119
  @@
120
  match
121
    Sync_ledger.Root.new_goal root_sync_ledger
122
      (Frozen_ledger_hash.to_ledger_hash snarked_ledger_hash)
×
123
      ~data:
124
        ( State_hash.With_state_hashes.state_hash
×
125
          @@ Mina_block.Validation.block_with_hash t.current_root
×
126
        , sender
127
        , expected_staged_ledger_hash )
128
      ~equal:(fun (hash1, _, _) (hash2, _, _) -> State_hash.equal hash1 hash2)
×
129
  with
130
  | `New ->
×
131
      t.num_of_root_snarked_ledger_retargeted <-
132
        t.num_of_root_snarked_ledger_retargeted + 1 ;
133
      `Syncing_new_snarked_ledger
134
  | `Update_data ->
×
135
      `Updating_root_transition
136
  | `Repeat ->
×
137
      `Ignored
138

139
let to_consensus_state h =
140
  Mina_block.Header.protocol_state h |> Protocol_state.consensus_state
×
141

142
(** For each transition, this function would compare it with the existing one.
143
    If the incoming transition is better, then download the merkle list from
144
    that transition to its root and verify it. If we get a better root than
145
    the existing one, then reset the Sync_ledger's target by calling
146
    [start_sync_job_with_peer] function. *)
147
let on_transition ({ context = (module Context); _ } as t) ~sender
148
    ~root_sync_ledger candidate_header =
149
  let open Context in
×
150
  let candidate_consensus_state =
151
    With_hash.map ~f:to_consensus_state candidate_header
152
  in
153
  if not @@ should_sync ~root_sync_ledger t candidate_consensus_state then
×
154
    Deferred.return `Ignored
×
155
  else
156
    match%bind
157
      Mina_networking.get_ancestry t.network sender.Peer.peer_id
×
158
        (With_hash.map_hash candidate_consensus_state
×
159
           ~f:State_hash.State_hashes.state_hash )
160
    with
161
    | Error e ->
×
162
        [%log error]
×
163
          ~metadata:[ ("error", Error_json.error_to_yojson e) ]
×
164
          !"Could not get the proof of the root transition from the network: \
165
            $error" ;
166
        Deferred.return `Ignored
×
167
    | Ok peer_root_with_proof -> (
×
168
        let pcd =
169
          peer_root_with_proof.data
170
          |> Proof_carrying_data.map
×
171
               ~f:
172
                 (Mina_block.write_all_proofs_to_disk ~signature_kind
173
                    ~proof_cache_db )
174
          |> Proof_carrying_data.map_proof
175
               ~f:
176
                 (Tuple2.map_snd
177
                    ~f:
178
                      (Mina_block.write_all_proofs_to_disk ~signature_kind
179
                         ~proof_cache_db ) )
180
        in
181
        match%bind
182
          Mina_block.verify_on_header
×
183
            ~verify:
184
              (Sync_handler.Root.verify
×
185
                 ~context:(module Context)
186
                 ~verifier:t.verifier candidate_consensus_state )
187
            pcd
188
        with
189
        | Ok (`Root root, `Best_tip best_tip) ->
×
190
            if done_syncing_root root_sync_ledger then return `Ignored
×
191
            else
192
              start_sync_job_with_peer ~sender ~root_sync_ledger t best_tip root
×
193
        | Error e ->
×
194
            return (received_bad_proof t sender e |> Fn.const `Ignored) )
×
195

196
(** A helper function that wraps the calls to Sync_ledger and iterate through
197
    incoming transitions, add those to the transition_cache and calls
198
    [on_transition] function. *)
199
let sync_ledger ({ context = (module Context); _ } as t) ~preferred
200
    ~root_sync_ledger ~transition_graph ~sync_ledger_reader =
201
  let open Context in
×
202
  let query_reader = Sync_ledger.Root.query_reader root_sync_ledger in
203
  let response_writer = Sync_ledger.Root.answer_writer root_sync_ledger in
×
204
  Mina_networking.glue_sync_ledger ~preferred t.network query_reader
×
205
    response_writer ;
206
  Pipe_lib.Choosable_synchronous_pipe.iter sync_ledger_reader
×
207
    ~f:(fun (b_or_h, `Valid_cb vc) ->
208
      let header_with_hash, sender, transition_cache_element =
×
209
        match b_or_h with
210
        | `Block b_env ->
×
211
            ( Envelope.Incoming.data b_env
×
212
              |> Mina_block.Validation.block_with_hash
×
213
              |> With_hash.map ~f:Mina_block.header
×
214
            , Envelope.Incoming.remote_sender_exn b_env
×
215
            , Envelope.Incoming.map ~f:(fun x -> `Block x) b_env )
×
216
        | `Header h_env ->
×
217
            ( Envelope.Incoming.data h_env
×
218
              |> Mina_block.Validation.header_with_hash
×
219
            , Envelope.Incoming.remote_sender_exn h_env
×
220
            , Envelope.Incoming.map ~f:(fun x -> `Header x) h_env )
×
221
      in
222
      let previous_state_hash =
223
        With_hash.data header_with_hash
×
224
        |> Mina_block.Header.protocol_state
×
225
        |> Protocol_state.previous_state_hash
226
      in
227
      Transition_cache.add transition_graph ~parent:previous_state_hash
×
228
        (transition_cache_element, vc) ;
229
      (* TODO: Efficiently limiting the number of green threads in #1337 *)
230
      if
×
231
        worth_getting_root t
232
          (With_hash.map ~f:to_consensus_state header_with_hash)
×
233
      then (
×
234
        [%log trace] "Added the transition from sync_ledger_reader into cache"
×
235
          ~metadata:
236
            [ ( "state_hash"
237
              , State_hash.to_yojson
×
238
                  (State_hash.With_state_hashes.state_hash header_with_hash) )
×
239
            ; ( "header"
240
              , Mina_block.Header.to_yojson (With_hash.data header_with_hash) )
×
241
            ] ;
242

243
        Deferred.ignore_m
×
244
        @@ on_transition t ~sender ~root_sync_ledger header_with_hash )
×
245
      else Deferred.unit )
×
246

247
let external_transition_compare ~context:(module Context : CONTEXT) =
248
  let get_consensus_state =
×
249
    Fn.compose Protocol_state.consensus_state Mina_block.Header.protocol_state
250
  in
251
  Comparable.lift
×
252
    (fun existing candidate ->
253
      (* To prevent the logger to spam a lot of messages, the logger input is set to null *)
254
      if
×
255
        State_hash.equal
256
          (State_hash.With_state_hashes.state_hash existing)
×
257
          (State_hash.With_state_hashes.state_hash candidate)
×
258
      then 0
×
259
      else if
×
260
        Consensus.Hooks.equal_select_status `Keep
261
        @@ Consensus.Hooks.select ~context:(module Context) ~existing ~candidate
262
      then -1
×
263
      else 1 )
×
264
    ~f:(With_hash.map ~f:get_consensus_state)
265

266
let download_snarked_ledger ~trust_system ~preferred_peers ~transition_graph
267
    ~sync_ledger_reader ~context t temp_snarked_ledger =
268
  time_deferred
×
269
    (let root_sync_ledger =
270
       Sync_ledger.Root.create temp_snarked_ledger ~context ~trust_system
271
     in
272
     don't_wait_for
×
273
       (sync_ledger t ~preferred:preferred_peers ~root_sync_ledger
×
274
          ~transition_graph ~sync_ledger_reader ) ;
275
     (* We ignore the resulting ledger returned here since it will always
276
        * be the same as the ledger we started with because we are syncing
277
        * a db ledger. *)
278
     let%map _, data = Sync_ledger.Root.valid_tree root_sync_ledger in
×
279
     Sync_ledger.Root.destroy root_sync_ledger ;
×
280
     data )
×
281

282
let handle_scan_state_and_aux ~logger ~expected_staged_ledger_hash
283
    ~temp_snarked_ledger ~verifier ~constraint_constants ~signature_kind
284
    ~proof_cache_db t
285
    ( scan_state_uncached
286
    , expected_merkle_root
287
    , pending_coinbases
288
    , protocol_states ) =
289
  let%map staged_ledger_construction_result =
290
    O1trace.thread "construct_root_staged_ledger" (fun () ->
×
291
        let open Deferred.Or_error.Let_syntax in
×
292
        let received_staged_ledger_hash =
293
          Staged_ledger_hash.of_aux_ledger_and_coinbase_hash
294
            (Staged_ledger.Scan_state.Stable.Latest.hash scan_state_uncached)
×
295
            expected_merkle_root pending_coinbases
296
        in
297
        [%log debug]
×
298
          ~metadata:
299
            [ ( "expected_staged_ledger_hash"
300
              , Staged_ledger_hash.to_yojson expected_staged_ledger_hash )
×
301
            ; ( "received_staged_ledger_hash"
302
              , Staged_ledger_hash.to_yojson received_staged_ledger_hash )
×
303
            ]
304
          "Comparing $expected_staged_ledger_hash to \
305
           $received_staged_ledger_hash" ;
306
        let%bind new_root =
307
          t.current_root
308
          |> Mina_block.Validation.skip_frontier_dependencies_validation
×
309
               `This_block_belongs_to_a_detached_subtree
310
          |> Mina_block.Validation.validate_staged_ledger_hash
×
311
               (`Staged_ledger_already_materialized received_staged_ledger_hash)
312
          |> Result.map_error ~f:(fun _ ->
×
313
                 Error.of_string "received faulty scan state from peer" )
×
314
          |> Deferred.return
×
315
        in
316
        let protocol_states =
×
317
          List.map protocol_states
318
            ~f:(With_hash.of_data ~hash_data:Protocol_state.hashes)
319
        in
320
        let scan_state =
×
321
          Staged_ledger.Scan_state.write_all_proofs_to_disk ~signature_kind
322
            ~proof_cache_db scan_state_uncached
323
        in
324
        let%bind protocol_states =
325
          Staged_ledger.Scan_state.check_required_protocol_states scan_state
×
326
            ~protocol_states
327
          |> Deferred.return
×
328
        in
329
        let protocol_states_map =
×
330
          protocol_states
331
          |> List.map ~f:(fun ps ->
×
332
                 (State_hash.With_state_hashes.state_hash ps, ps) )
×
333
          |> State_hash.Map.of_alist_exn
334
        in
335
        let get_state hash =
×
336
          match Map.find protocol_states_map hash with
×
337
          | None ->
×
338
              let new_state_hash =
339
                State_hash.With_state_hashes.state_hash (fst new_root)
×
340
              in
341
              [%log error]
×
342
                ~metadata:
343
                  [ ("new_root", State_hash.to_yojson new_state_hash)
×
344
                  ; ("state_hash", State_hash.to_yojson hash)
×
345
                  ]
346
                "Protocol state (for scan state transactions) for $state_hash \
347
                 not found when bootstrapping to the new root $new_root" ;
348
              Or_error.errorf
×
349
                !"Protocol state (for scan state transactions) for \
×
350
                  %{sexp:State_hash.t} not found when bootstrapping to the new \
351
                  root %{sexp:State_hash.t}"
352
                hash new_state_hash
353
          | Some protocol_state ->
×
354
              Ok (With_hash.data protocol_state)
×
355
        in
356
        (* step 3. Construct staged ledger from snarked ledger, scan state
357
           and pending coinbases. *)
358
        (* Construct the staged ledger before constructing the transition
359
         * frontier in order to verify the scan state we received.
360
         * TODO: reorganize the code to avoid doing this twice (#3480) *)
361
        let open Deferred.Let_syntax in
362
        let%map staged_ledger_construction_time, construction_result =
363
          time_deferred
×
364
            (let open Deferred.Let_syntax in
365
            let temp_mask = Root_ledger.as_masked temp_snarked_ledger in
366
            (* [of_scan_state_pending_coinbases_and_snarked_ledger] is called here
367
             * to verify the scan state we received. We ignore the resulting staged
368
             * ledger (from [temp_mask]). Later we call [Transition_frontier.load]
369
             * which will indirectly invoke
370
             * [of_scan_state_pending_coinbases_and_snarked_ledger_unchecked]
371
             * repeating the same computation (except for proof verification
372
             * which is skipped). Chain of calls:
373
             * [Transition_frontier.load] -> .. -> [load_from_persistence_and_start]
374
             * -> [load_full_frontier] -> [construct_staged_ledger_at_root] ->
375
             * -> [of_scan_state_pending_coinbases_and_snarked_ledger_unchecked]
376
             *)
377
            let%map result =
378
              Staged_ledger.of_scan_state_pending_coinbases_and_snarked_ledger
379
                ~logger
380
                ~snarked_local_state:
381
                  Mina_block.(
382
                    t.current_root |> Validation.block |> header
×
383
                    |> Header.protocol_state |> Protocol_state.blockchain_state
×
384
                    |> Blockchain_state.snarked_local_state)
×
385
                ~verifier ~constraint_constants ~scan_state
386
                ~snarked_ledger:temp_mask ~expected_merkle_root
387
                ~pending_coinbases ~get_state ~signature_kind
388
            in
389
            ignore
×
390
              ( Ledger.Maskable.unregister_mask_exn ~loc:__LOC__ temp_mask
×
391
                : Ledger.unattached_mask ) ;
392
            Result.map result
393
              ~f:
394
                (const
×
395
                   (scan_state, pending_coinbases, new_root, protocol_states) ))
396
        in
397
        Ok (staged_ledger_construction_time, construction_result) )
×
398
  in
399
  match staged_ledger_construction_result with
×
400
  | Error err ->
×
401
      (None, Error err)
402
  | Ok (staged_ledger_construction_time, result) ->
×
403
      (Some staged_ledger_construction_time, result)
404

405
(** Run one bootstrap cycle *)
406
let run_cycle ~context:(module Context : CONTEXT) ~trust_system ~verifier
407
    ~network ~consensus_local_state ~network_transition_pipe ~preferred_peers
408
    ~persistent_root ~persistent_frontier ~initial_root_transition ~catchup_mode
409
    previous_cycles =
410
  let open Context in
×
411
  (* The short-lived pipe allocated here will be closed
412
     when a follow-up pipe is allocated: in the next cycle of bootstrap
413
     or when controll is passed to the transition frontier controller.staged_ledger_construction_time
414
     Because [Choosable_synchronous_pipe.t] is used, no data will be lost: any
415
     successful read is followed by mom-blocking handling, and if the read/write
416
     pair is not consumed, it will continue in a follow-up pipe allocated in the
417
     next call to [Swappable.swap_reader].
418
  *)
419
  let%bind sync_ledger_reader = Swappable.swap_reader network_transition_pipe in
×
420
  let initial_root_transition =
×
421
    initial_root_transition |> Mina_block.Validated.remember
×
422
    |> Mina_block.Validation.reset_frontier_dependencies_validation
×
423
    |> Mina_block.Validation.reset_staged_ledger_diff_validation
424
  in
425
  let t =
×
426
    { network
427
    ; context = (module Context)
428
    ; trust_system
429
    ; verifier
430
    ; best_seen_transition = initial_root_transition
431
    ; current_root = initial_root_transition
432
    ; num_of_root_snarked_ledger_retargeted = 0
433
    }
434
  in
435
  let transition_graph = Transition_cache.create () in
436
  let temp_persistent_root_instance =
×
437
    Transition_frontier.Persistent_root.create_instance_exn persistent_root
438
  in
439
  let temp_snarked_ledger =
×
440
    Transition_frontier.Persistent_root.Instance.snarked_ledger
441
      temp_persistent_root_instance
442
  in
443
  (* step 1. download snarked_ledger *)
444
  let%bind sync_ledger_time, (hash, sender, expected_staged_ledger_hash) =
445
    download_snarked_ledger
×
446
      ~context:(module Context)
447
      ~trust_system ~preferred_peers ~transition_graph ~sync_ledger_reader t
448
      temp_snarked_ledger
449
  in
450
  Mina_metrics.(
×
451
    Counter.inc Bootstrap.root_snarked_ledger_sync_ms
×
452
      Time.Span.(to_ms sync_ledger_time)) ;
×
453
  Mina_metrics.(
454
    Gauge.set Bootstrap.num_of_root_snarked_ledger_retargeted
×
455
      (Float.of_int t.num_of_root_snarked_ledger_retargeted)) ;
×
456
  (* step 2. Download scan state and pending coinbases. *)
457
  let%bind ( staged_ledger_data_download_time
458
           , (staged_ledger_construction_time, staged_ledger_aux_result) ) =
459
    let%bind ( staged_ledger_data_download_time
460
             , staged_ledger_data_download_result ) =
461
      time_deferred
×
462
        (Mina_networking.get_staged_ledger_aux_and_pending_coinbases_at_hash
×
463
           t.network sender.peer_id hash )
464
    in
465
    match staged_ledger_data_download_result with
×
466
    | Error err ->
×
467
        Deferred.return (staged_ledger_data_download_time, (None, Error err))
468
    | Ok result ->
×
469
        let%map res =
470
          handle_scan_state_and_aux ~logger ~expected_staged_ledger_hash
×
471
            ~temp_snarked_ledger ~verifier ~constraint_constants ~signature_kind
472
            ~proof_cache_db t result
473
        in
474
        (staged_ledger_data_download_time, res)
×
475
  in
476
  Transition_frontier.Persistent_root.Instance.close
×
477
    temp_persistent_root_instance ;
478
  match staged_ledger_aux_result with
×
479
  | Error e ->
×
480
      let%map () =
481
        Trust_system.(
482
          record t.trust_system logger sender
×
483
            Actions.
484
              ( Outgoing_connection_error
485
              , Some
486
                  ( "Can't find scan state from the peer or received faulty \
487
                     scan state from the peer."
488
                  , [] ) ))
489
      in
490
      [%log error]
×
491
        ~metadata:
492
          [ ("error", Error_json.error_to_yojson e)
×
493
          ; ("state_hash", State_hash.to_yojson hash)
×
494
          ; ( "expected_staged_ledger_hash"
495
            , Staged_ledger_hash.to_yojson expected_staged_ledger_hash )
×
496
          ]
497
        "Failed to find scan state for the transition with hash $state_hash \
498
         from the peer or received faulty scan state: $error. Retry bootstrap" ;
499
      let this_cycle =
×
500
        { cycle_result = "failed to download and construct scan state"
501
        ; sync_ledger_time
502
        ; staged_ledger_data_download_time
503
        ; staged_ledger_construction_time
504
        ; local_state_sync_required = false
505
        ; local_state_sync_time = None
506
        }
507
      in
508
      `Repeat (this_cycle :: previous_cycles)
509
  | Ok (scan_state, pending_coinbase, new_root, protocol_states) -> (
×
510
      let%bind () =
511
        Trust_system.(
512
          record t.trust_system logger sender
×
513
            Actions.
514
              ( Fulfilled_request
515
              , Some ("Received valid scan state from peer", []) ))
516
      in
517
      let best_seen_block_with_hash, _ = t.best_seen_transition in
×
518
      let consensus_state =
519
        With_hash.data best_seen_block_with_hash
×
520
        |> Mina_block.header |> Mina_block.Header.protocol_state
×
521
        |> Protocol_state.consensus_state
522
      in
523
      (* step 4. Synchronize consensus local state if necessary *)
524
      let%bind ( local_state_sync_time
525
               , (local_state_sync_required, local_state_sync_result) ) =
526
        time_deferred
×
527
          ( match
528
              Consensus.Hooks.required_local_state_sync
529
                ~constants:precomputed_values.consensus_constants
530
                ~consensus_state ~local_state:consensus_local_state
531
            with
532
          | None ->
×
533
              [%log debug]
×
534
                ~metadata:
535
                  [ ( "local_state"
536
                    , Consensus.Data.Local_state.to_yojson consensus_local_state
×
537
                    )
538
                  ; ( "consensus_state"
539
                    , Consensus.Data.Consensus_state.Value.to_yojson
×
540
                        consensus_state )
541
                  ]
542
                "Not synchronizing consensus local state" ;
543
              Deferred.return (false, Or_error.return ())
×
544
          | Some sync_jobs ->
×
545
              [%log info] "Synchronizing consensus local state" ;
×
546
              let%map result =
547
                Consensus.Hooks.sync_local_state
×
548
                  ~context:(module Context)
549
                  ~local_state:consensus_local_state ~trust_system
550
                  ~glue_sync_ledger:(Mina_networking.glue_sync_ledger t.network)
×
551
                  sync_jobs
552
              in
553
              (true, result) )
×
554
      in
555
      match local_state_sync_result with
×
556
      | Error e ->
×
557
          [%log error]
×
558
            ~metadata:[ ("error", Error_json.error_to_yojson e) ]
×
559
            "Local state sync failed: $error. Retry bootstrap" ;
560
          let this_cycle =
×
561
            { cycle_result = "failed to synchronize local state"
562
            ; sync_ledger_time
563
            ; staged_ledger_data_download_time
564
            ; staged_ledger_construction_time
565
            ; local_state_sync_required
566
            ; local_state_sync_time = Some local_state_sync_time
567
            }
568
          in
569
          Deferred.return (`Repeat (this_cycle :: previous_cycles))
570
      | Ok () ->
×
571
          (* step 5. Close the old frontier and reload a new one from disk. *)
572
          let new_root_data : Transition_frontier.Root_data.Limited.t =
573
            Transition_frontier.Root_data.Limited.create
574
              ~transition:(Mina_block.Validated.lift new_root)
×
575
              ~scan_state ~pending_coinbase ~protocol_states
576
          in
577
          let%bind () =
578
            Transition_frontier.Persistent_frontier.reset_database_exn
×
579
              persistent_frontier ~root_data:new_root_data
580
              ~genesis_state_hash:
581
                (State_hash.With_state_hashes.state_hash
×
582
                   precomputed_values.protocol_state_with_hashes )
583
          in
584
          (* TODO: lazy load db in persistent root to avoid unnecessary opens like this *)
585
          Transition_frontier.Persistent_root.(
×
586
            set_root_state_hash persistent_root
×
587
            @@ Mina_block.Validated.state_hash
×
588
            @@ Mina_block.Validated.lift new_root) ;
×
589
          let%map new_frontier =
590
            let fail msg =
591
              failwith
×
592
                ( "failed to initialize transition frontier after \
593
                   bootstrapping: " ^ msg )
594
            in
595
            Transition_frontier.load
×
596
              ~context:(module Context)
597
              ~retry_with_fresh_db:false ~verifier ~consensus_local_state
598
              ~persistent_root ~persistent_frontier ~catchup_mode ()
599
            >>| function
×
600
            | Ok frontier ->
×
601
                frontier
602
            | Error (`Failure msg) ->
×
603
                fail msg
604
            | Error `Bootstrap_required ->
×
605
                fail
606
                  "bootstrap still required (indicates logical error in code)"
607
            | Error `Persistent_frontier_malformed ->
×
608
                fail "persistent frontier was malformed"
609
            | Error `Snarked_ledger_mismatch ->
×
610
                fail
611
                  "this should not happen, because we just reset the \
612
                   snarked_ledger"
613
          in
614
          [%str_log info] Bootstrap_complete ;
×
615
          let collected_transitions = Transition_cache.data transition_graph in
×
616
          let logger =
×
617
            Logger.extend logger
618
              [ ("context", `String "Filter collected transitions in bootstrap")
619
              ]
620
          in
621
          let root_consensus_state =
×
622
            Transition_frontier.(
623
              Breadcrumb.consensus_state_with_hashes (root new_frontier))
×
624
          in
625
          let filtered_collected_transitions =
626
            List.filter collected_transitions
627
              ~f:(fun (incoming_transition, _) ->
628
                let transition =
×
629
                  Envelope.Incoming.data incoming_transition
×
630
                  |> Transition_cache.header_with_hash
631
                in
632
                Consensus.Hooks.equal_select_status `Take
×
633
                @@ Consensus.Hooks.select
634
                     ~context:(module Context)
635
                     ~existing:root_consensus_state
636
                     ~candidate:
637
                       (With_hash.map
×
638
                          ~f:
639
                            (Fn.compose Protocol_state.consensus_state
×
640
                               Mina_block.Header.protocol_state )
641
                          transition ) )
642
          in
643
          [%log debug] "Sorting filtered transitions by consensus state"
×
644
            ~metadata:[] ;
645
          let sorted_filtered_collected_transitions =
×
646
            O1trace.sync_thread "sorting_collected_transitions" (fun () ->
647
                List.sort filtered_collected_transitions
×
648
                  ~compare:
649
                    (Comparable.lift
×
650
                       ~f:(fun (x, _) ->
651
                         Transition_cache.header_with_hash
×
652
                         @@ Envelope.Incoming.data x )
×
653
                       (external_transition_compare ~context:(module Context)) ) )
654
          in
655
          let this_cycle =
×
656
            { cycle_result = "success"
657
            ; sync_ledger_time
658
            ; staged_ledger_data_download_time
659
            ; staged_ledger_construction_time
660
            ; local_state_sync_required
661
            ; local_state_sync_time = Some local_state_sync_time
662
            }
663
          in
664
          `Finished
665
            ( this_cycle :: previous_cycles
666
            , (new_frontier, sorted_filtered_collected_transitions) ) )
667

668
(** The entry point function for bootstrap controller. When bootstrap finished
669
    it would return a transition frontier with the root breadcrumb and a list
670
    of transitions collected during bootstrap.
671

672
    Bootstrap controller would do the following steps to contrust the
673
    transition frontier:
674
    1. Download the root snarked_ledger.
675
    2. Download the scan state and pending coinbases.
676
    3. Construct the staged ledger from the snarked ledger, scan state and
677
       pending coinbases.
678
    4. Synchronize the consensus local state if necessary.
679
    5. Close the old frontier and reload a new one from disk.
680
 *)
681
let run ~context:(module Context : CONTEXT) ~trust_system ~verifier ~network
682
    ~consensus_local_state ~network_transition_pipe ~preferred_peers
683
    ~persistent_root ~persistent_frontier ~initial_root_transition ~catchup_mode
684
    =
685
  let open Context in
×
686
  let run_cycle =
687
    run_cycle
688
      ~context:(module Context : CONTEXT)
689
      ~trust_system ~verifier ~network ~consensus_local_state
690
      ~network_transition_pipe ~preferred_peers ~persistent_root
691
      ~persistent_frontier ~initial_root_transition ~catchup_mode
692
  in
693
  O1trace.thread "bootstrap"
694
  @@ fun () ->
695
  let%map time_elapsed, (cycles, result) =
696
    time_deferred @@ Deferred.repeat_until_finished [] run_cycle
×
697
  in
698
  [%log info] "Bootstrap completed in $time_elapsed: $bootstrap_stats"
×
699
    ~metadata:
700
      [ ("time_elapsed", Time.Span.to_yojson_hum time_elapsed)
×
701
      ; ( "bootstrap_stats"
702
        , `List (List.map ~f:bootstrap_cycle_stats_to_yojson cycles) )
×
703
      ] ;
704
  Mina_metrics.(
×
705
    Gauge.set Bootstrap.bootstrap_time_ms Core.Time.(Span.to_ms @@ time_elapsed)) ;
×
706
  result
707

708
let%test_module "Bootstrap_controller tests" =
709
  ( module struct
710
    let max_frontier_length =
711
      Transition_frontier.global_max_length Genesis_constants.For_unit_tests.t
×
712

713
    let logger = Logger.null ()
×
714

715
    let () =
716
      (* Disable log messages from best_tip_diff logger. *)
717
      Logger.Consumer_registry.register ~commit_id:""
×
718
        ~id:Logger.Logger_id.best_tip_diff ~processor:(Logger.Processor.raw ())
×
719
        ~transport:
720
          (Logger.Transport.create
×
721
             ( module struct
722
               type t = unit
723

724
               let transport () _ = ()
×
725
             end )
726
             () )
727
        ()
728

729
    let trust_system =
730
      let s = Trust_system.null () in
731
      don't_wait_for
×
732
        (Pipe_lib.Strict_pipe.Reader.iter
×
733
           (Trust_system.upcall_pipe s)
×
734
           ~f:(const Deferred.unit) ) ;
×
735
      s
×
736

737
    let precomputed_values = Lazy.force Precomputed_values.for_unit_tests
×
738

739
    let proof_level = precomputed_values.proof_level
740

741
    let constraint_constants = precomputed_values.constraint_constants
742

743
    let ledger_sync_config =
744
      Syncable_ledger.create_config
×
745
        ~compile_config:Mina_compile_config.For_unit_tests.t
746
        ~max_subtree_depth:None ~default_subtree_depth:None ()
747

748
    module Context = struct
749
      let logger = logger
750

751
      let precomputed_values = precomputed_values
752

753
      let constraint_constants =
754
        Genesis_constants.For_unit_tests.Constraint_constants.t
755

756
      let consensus_constants = precomputed_values.consensus_constants
757

758
      let ledger_sync_config =
759
        Syncable_ledger.create_config
×
760
          ~compile_config:Mina_compile_config.For_unit_tests.t
761
          ~max_subtree_depth:None ~default_subtree_depth:None ()
762

763
      let proof_cache_db = Proof_cache_tag.For_tests.create_db ()
×
764

765
      let signature_kind = Mina_signature_kind.Testnet
766
    end
767

768
    let verifier =
769
      Async.Thread_safe.block_on_async_exn (fun () ->
×
770
          Verifier.For_tests.default ~constraint_constants ~logger ~proof_level
×
771
            () )
772

773
    module Genesis_ledger = (val precomputed_values.genesis_ledger)
774

775
    let downcast_transition ~sender transition =
776
      let transition =
×
777
        transition |> Mina_block.Validated.remember
×
778
        |> Mina_block.Validation.reset_frontier_dependencies_validation
×
779
        |> Mina_block.Validation.reset_staged_ledger_diff_validation
780
      in
781
      Envelope.Incoming.wrap ~data:transition
×
782
        ~sender:(Envelope.Sender.Remote sender)
783

784
    let downcast_breadcrumb ~sender breadcrumb =
785
      downcast_transition ~sender
×
786
        (Transition_frontier.Breadcrumb.validated_transition breadcrumb)
×
787

788
    let make_non_running_bootstrap ~genesis_root ~network =
789
      let transition =
×
790
        genesis_root
791
        |> Mina_block.Validation.reset_frontier_dependencies_validation
×
792
        |> Mina_block.Validation.reset_staged_ledger_diff_validation
793
      in
794
      { context = (module Context)
×
795
      ; trust_system
796
      ; verifier
797
      ; best_seen_transition = transition
798
      ; current_root = transition
799
      ; network
800
      ; num_of_root_snarked_ledger_retargeted = 0
801
      }
802

803
    let%test_unit "Bootstrap controller caches all transitions it is passed \
804
                   through the transition_reader" =
805
      let branch_size = (max_frontier_length * 2) + 2 in
×
806
      Quickcheck.test ~trials:1
807
        (let open Quickcheck.Generator.Let_syntax in
808
        (* we only need one node for this test, but we need more than one peer so that mina_networking does not throw an error *)
809
        let%bind fake_network =
810
          Fake_network.Generator.(
811
            gen ~precomputed_values ~verifier ~max_frontier_length
×
812
              ~ledger_sync_config [ fresh_peer; fresh_peer ])
813
        in
814
        let%map make_branch =
815
          Transition_frontier.Breadcrumb.For_tests.gen_seq ~precomputed_values
×
816
            ~verifier
817
            ~accounts_with_secret_keys:(Lazy.force Genesis_ledger.accounts)
×
818
            branch_size
819
        in
820
        let [ me; _ ] = fake_network.peer_networks in
×
821
        let branch =
822
          Async.Thread_safe.block_on_async_exn (fun () ->
823
              make_branch (Transition_frontier.root me.state.frontier) )
×
824
        in
825
        (fake_network, branch))
×
826
        ~f:(fun (fake_network, branch) ->
827
          let [ me; other ] = fake_network.peer_networks in
×
828
          let genesis_root =
829
            Transition_frontier.(
830
              Breadcrumb.validated_transition @@ root me.state.frontier)
×
831
            |> Mina_block.Validated.remember
832
          in
833
          let transition_graph = Transition_cache.create () in
×
834
          let sync_ledger_reader, sync_ledger_writer =
×
835
            Pipe_lib.Choosable_synchronous_pipe.create ()
836
          in
837
          let bootstrap =
×
838
            make_non_running_bootstrap ~genesis_root ~network:me.network
839
          in
840
          let root_sync_ledger =
841
            Sync_ledger.Root.create
842
              (Transition_frontier.root_snarked_ledger me.state.frontier)
×
843
              ~context:(module Context)
844
              ~trust_system
845
          in
846
          Async.Thread_safe.block_on_async_exn (fun () ->
×
847
              let sync_deferred =
×
848
                sync_ledger bootstrap ~root_sync_ledger ~transition_graph
849
                  ~preferred:[] ~sync_ledger_reader
850
              in
851
              let%bind sync_ledger_writer' =
852
                Deferred.List.fold ~init:sync_ledger_writer branch
×
853
                  ~f:(fun sync_ledger_writer breadcrumb ->
854
                    Pipe_lib.Choosable_synchronous_pipe.write sync_ledger_writer
×
855
                      ( `Block
856
                          (downcast_breadcrumb ~sender:other.peer breadcrumb)
×
857
                      , `Valid_cb None ) )
858
              in
859
              Pipe_lib.Choosable_synchronous_pipe.close sync_ledger_writer' ;
×
860
              sync_deferred ) ;
×
861
          let expected_transitions =
×
862
            List.map branch
863
              ~f:
864
                (Fn.compose
×
865
                   (With_hash.map ~f:Mina_block.header)
866
                   (Fn.compose Mina_block.Validation.block_with_hash
×
867
                      (Fn.compose Mina_block.Validated.remember
×
868
                         Transition_frontier.Breadcrumb.validated_transition ) ) )
869
          in
870
          let saved_transitions =
×
871
            Transition_cache.data transition_graph
×
872
            |> List.map ~f:(fun (x, _) ->
873
                   Transition_cache.header_with_hash @@ Envelope.Incoming.data x )
×
874
          in
875
          let module E = struct
×
876
            module T = struct
877
              type t = Mina_block.Header.t State_hash.With_state_hashes.t
×
878
              [@@deriving sexp]
879

880
              let compare = external_transition_compare ~context:(module Context)
881
            end
882

883
            include Comparable.Make (T)
884
          end in
885
          [%test_result: E.Set.t]
×
886
            (E.Set.of_list saved_transitions)
×
887
            ~expect:(E.Set.of_list expected_transitions) )
×
888

889
    let run_bootstrap ~timeout_duration ~my_net ~network_transition_pipe =
890
      let open Fake_network in
×
891
      let time_controller = Block_time.Controller.basic ~logger in
892
      let persistent_root =
893
        Transition_frontier.persistent_root my_net.state.frontier
894
      in
895
      let persistent_frontier =
×
896
        Transition_frontier.persistent_frontier my_net.state.frontier
897
      in
898
      let initial_root_transition =
×
899
        Transition_frontier.(
900
          Breadcrumb.validated_transition (root my_net.state.frontier))
×
901
      in
902
      let%bind () =
903
        Transition_frontier.close ~loc:__LOC__ my_net.state.frontier
×
904
      in
905
      [%log info] "bootstrap begin" ;
×
906
      Block_time.Timeout.await_exn time_controller ~timeout_duration
×
907
        (run
908
           ~context:(module Context)
909
           ~trust_system ~verifier ~network:my_net.network ~preferred_peers:[]
910
           ~consensus_local_state:my_net.state.consensus_local_state
911
           ~network_transition_pipe ~persistent_root ~persistent_frontier
912
           ~catchup_mode:`Super ~initial_root_transition )
913

914
    let assert_transitions_increasingly_sorted ~root
915
        (incoming_transitions : Transition_cache.element list) =
916
      let root =
×
917
        Transition_frontier.Breadcrumb.block root |> Mina_block.header
×
918
      in
919
      ignore
×
920
        ( List.fold_result ~init:root incoming_transitions
×
921
            ~f:(fun max_acc incoming_transition ->
922
              let With_hash.{ data = header; _ } =
×
923
                Transition_cache.header_with_hash
924
                  (Envelope.Incoming.data @@ fst incoming_transition)
×
925
              in
926
              let header_len h =
×
927
                Mina_block.Header.protocol_state h
×
928
                |> Protocol_state.consensus_state
×
929
                |> Consensus.Data.Consensus_state.blockchain_length
930
              in
931
              let open Result.Let_syntax in
932
              let%map () =
933
                Result.ok_if_true
×
934
                  Mina_numbers.Length.(header_len max_acc <= header_len header)
×
935
                  ~error:
936
                    (Error.of_string
×
937
                       "The blocks are not sorted in increasing order" )
938
              in
939
              header )
×
940
          |> Or_error.ok_exn
×
941
          : Mina_block.Header.t )
942

943
    let%test_unit "sync with one node after receiving a transition" =
944
      Quickcheck.test ~trials:1
×
945
        Fake_network.Generator.(
946
          gen ~precomputed_values ~verifier ~max_frontier_length
×
947
            ~ledger_sync_config
948
            [ fresh_peer
949
            ; peer_with_branch
950
                ~frontier_branch_size:((max_frontier_length * 2) + 2)
951
            ])
952
        ~f:(fun fake_network ->
953
          let [ my_net; peer_net ] = fake_network.peer_networks in
×
954
          let block =
955
            Envelope.Incoming.wrap
956
              ~data:
957
                ( Transition_frontier.best_tip peer_net.state.frontier
×
958
                |> Transition_frontier.Breadcrumb.validated_transition
×
959
                |> Mina_block.Validated.remember
×
960
                |> Mina_block.Validation.reset_frontier_dependencies_validation
×
961
                |> Mina_block.Validation.reset_staged_ledger_diff_validation )
×
962
              ~sender:(Envelope.Sender.Remote peer_net.peer)
963
          in
964
          let network_transition_pipe =
965
            Swappable.create ~name:(__MODULE__ ^ __LOC__)
966
              (Buffered (`Capacity 10, `Overflow (Drop_head ignore)))
967
          in
968
          Swappable.write network_transition_pipe (`Block block, `Valid_cb None) ;
×
969
          let new_frontier, sorted_external_transitions =
×
970
            Async.Thread_safe.block_on_async_exn (fun () ->
971
                run_bootstrap
×
972
                  ~timeout_duration:(Block_time.Span.of_ms 30_000L)
×
973
                  ~my_net ~network_transition_pipe )
974
          in
975
          assert_transitions_increasingly_sorted
×
976
            ~root:(Transition_frontier.root new_frontier)
×
977
            sorted_external_transitions ;
978
          [%test_result: Ledger_hash.t]
×
979
            ( Root_ledger.merkle_root
×
980
            @@ Transition_frontier.root_snarked_ledger new_frontier )
×
981
            ~expect:
982
              ( Root_ledger.merkle_root
×
983
              @@ Transition_frontier.root_snarked_ledger peer_net.state.frontier
×
984
              ) )
985

986
    let%test_unit "reconstruct staged_ledgers using \
987
                   of_scan_state_and_snarked_ledger" =
988
      Quickcheck.test ~trials:1
×
989
        (Transition_frontier.For_tests.gen ~precomputed_values ~verifier
×
990
           ~max_length:max_frontier_length ~size:max_frontier_length () )
991
        ~f:(fun frontier ->
992
          Thread_safe.block_on_async_exn
×
993
          @@ fun () ->
994
          Deferred.List.iter (Transition_frontier.all_breadcrumbs frontier)
×
995
            ~f:(fun breadcrumb ->
996
              let staged_ledger =
×
997
                Transition_frontier.Breadcrumb.staged_ledger breadcrumb
998
              in
999
              let expected_merkle_root =
×
1000
                Staged_ledger.ledger staged_ledger |> Ledger.merkle_root
×
1001
              in
1002
              let snarked_ledger =
×
1003
                Transition_frontier.root_snarked_ledger frontier
×
1004
                |> Root_ledger.as_masked
1005
              in
1006
              let snarked_local_state =
×
1007
                Transition_frontier.root frontier
×
1008
                |> Transition_frontier.Breadcrumb.protocol_state
×
1009
                |> Protocol_state.blockchain_state
×
1010
                |> Blockchain_state.snarked_local_state
1011
              in
1012
              let scan_state = Staged_ledger.scan_state staged_ledger in
×
1013
              let get_state hash =
×
1014
                match Transition_frontier.find_protocol_state frontier hash with
×
1015
                | Some protocol_state ->
×
1016
                    Ok protocol_state
1017
                | None ->
×
1018
                    Or_error.errorf
1019
                      !"Protocol state (for scan state transactions) for \
×
1020
                        %{sexp:State_hash.t} not found"
1021
                      hash
1022
              in
1023
              let pending_coinbases =
1024
                Staged_ledger.pending_coinbase_collection staged_ledger
1025
              in
1026
              let%map actual_staged_ledger =
1027
                Staged_ledger.of_scan_state_pending_coinbases_and_snarked_ledger
1028
                  ~scan_state ~logger ~verifier ~constraint_constants
1029
                  ~snarked_ledger ~snarked_local_state ~expected_merkle_root
1030
                  ~pending_coinbases ~get_state ~signature_kind:Testnet
1031
                |> Deferred.Or_error.ok_exn
×
1032
              in
1033
              let height =
×
1034
                Transition_frontier.Breadcrumb.consensus_state breadcrumb
×
1035
                |> Consensus.Data.Consensus_state.blockchain_length
×
1036
                |> Mina_numbers.Length.to_int
1037
              in
1038
              [%test_eq: Staged_ledger_hash.t]
×
1039
                ~message:
1040
                  (sprintf "mismatch of staged ledger hash height %d" height)
×
1041
                (Transition_frontier.Breadcrumb.staged_ledger_hash breadcrumb)
×
1042
                (Staged_ledger.hash actual_staged_ledger) ) )
×
1043

1044
    (*
1045
    let%test_unit "if we see a new transition that is better than the \
1046
                   transition that we are syncing from, than we should \
1047
                   retarget our root" =
1048
      Quickcheck.test ~trials:1
1049
        Fake_network.Generator.(
1050
          gen ~max_frontier_length
1051
            [ fresh_peer
1052
            ; peer_with_branch ~frontier_branch_size:max_frontier_length
1053
            ; peer_with_branch
1054
                ~frontier_branch_size:((max_frontier_length * 2) + 2) ])
1055
        ~f:(fun fake_network ->
1056
          let [me; weaker_chain; stronger_chain] =
1057
            fake_network.peer_networks
1058
          in
1059
          let transition_reader, transition_writer =
1060
            Pipe_lib.Strict_pipe.create ~name:(__MODULE__ ^ __LOC__)
1061
              (Buffered (`Capacity 10, `Overflow Drop_head))
1062
          in
1063
          Envelope.Incoming.wrap
1064
            ~data:
1065
              ( Transition_frontier.best_tip weaker_chain.state.frontier
1066
              |> Transition_frontier.Breadcrumb.validated_transition
1067
              |> Mina_block.Validated.to_initial_validated )
1068
            ~sender:
1069
              (Envelope.Sender.Remote
1070
                 (weaker_chain.peer.host, weaker_chain.peer.peer_id))
1071
          |> Pipe_lib.Strict_pipe.Writer.write transition_writer ;
1072
          Envelope.Incoming.wrap
1073
            ~data:
1074
              ( Transition_frontier.best_tip stronger_chain.state.frontier
1075
              |> Transition_frontier.Breadcrumb.validated_transition
1076
              |> Mina_block.Validated.to_initial_validated )
1077
            ~sender:
1078
              (Envelope.Sender.Remote
1079
                 (stronger_chain.peer.host, stronger_chain.peer.peer_id))
1080
          |> Pipe_lib.Strict_pipe.Writer.write transition_writer ;
1081
          let new_frontier, sorted_external_transitions =
1082
            Async.Thread_safe.block_on_async_exn (fun () ->
1083
                run_bootstrap
1084
                  ~timeout_duration:(Block_time.Span.of_ms 60_000L)
1085
                  ~my_net:me ~transition_reader )
1086
          in
1087
          assert_transitions_increasingly_sorted
1088
            ~root:(Transition_frontier.root new_frontier)
1089
            sorted_external_transitions ;
1090
          [%test_result: Ledger_hash.t]
1091
            ( Ledger.Db.merkle_root
1092
            @@ Transition_frontier.root_snarked_ledger new_frontier )
1093
            ~expect:
1094
              ( Ledger.Db.merkle_root
1095
              @@ Transition_frontier.root_snarked_ledger
1096
                   stronger_chain.state.frontier ) )
1097
*)
1098
  end )
284✔
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