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

MinaProtocol / mina / 1661

18 Dec 2025 03:32PM UTC coverage: 61.328% (+27.9%) from 33.382%
1661

push

buildkite

web-flow
Merge pull request #18232 from MinaProtocol/amcie-merging-release-330-to-master

Merging 3.3.0 release branch to master

1229 of 2006 new or added lines in 108 files covered. (61.27%)

54 existing lines in 27 files now uncovered.

51257 of 83578 relevant lines covered (61.33%)

472886.36 hits per line

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

0.81
/src/lib/testing/integration_test_local_engine/mina_docker.ml
1
open Core
1✔
2
open Async
3
open Currency
4
open Signature_lib
5
open Mina_base
6
open Integration_test_lib
7

8
let docker_swarm_version = "3.8"
9

10
module Network_config = struct
11
  module Cli_inputs = Cli_inputs
12

13
  type docker_config =
×
14
    { docker_swarm_version : string
×
15
    ; stack_name : string
×
16
    ; mina_image : string
×
17
    ; mina_agent_image : string
×
18
    ; mina_points_image : string
×
19
    ; mina_archive_image : string
×
20
    ; runtime_config : Yojson.Safe.t
×
21
    ; seed_configs : Docker_node_config.Seed_config.t list
×
22
    ; block_producer_configs : Docker_node_config.Block_producer_config.t list
×
23
    ; snark_coordinator_config :
24
        Docker_node_config.Snark_coordinator_config.t option
×
25
    ; archive_node_configs : Docker_node_config.Archive_node_config.t list
×
26
    ; mina_archive_schema_aux_files : string list
×
27
    ; log_precomputed_blocks : bool
×
28
    ; start_filtered_logs : string list
×
29
    }
30
  [@@deriving to_yojson]
31

32
  type t =
×
33
    { debug_arg : bool
×
34
    ; genesis_keypairs :
35
        (Network_keypair.t Core.String.Map.t
36
        [@to_yojson
37
          fun map ->
38
            `Assoc
×
39
              (Core.Map.fold_right ~init:[]
×
40
                 ~f:(fun ~key:k ~data:v accum ->
41
                   (k, Network_keypair.to_yojson v) :: accum )
×
42
                 map )] )
×
43
    ; constants : Test_config.constants
×
44
    ; docker : docker_config
×
NEW
45
    ; commit_id : string
×
46
    }
47
  [@@deriving to_yojson]
48

49
  let expand ~logger ~test_name ~(cli_inputs : Cli_inputs.t) ~(debug : bool)
50
      ~(images : Test_config.Container_images.t) ~(test_config : Test_config.t)
51
      ~(constants : Test_config.constants) =
52
    let _ = cli_inputs in
×
53
    let ({ genesis_ledger
54
         ; epoch_data
55
         ; block_producers
56
         ; snark_coordinator
57
         ; snark_worker_fee
58
         ; num_archive_nodes
59
         ; log_precomputed_blocks (* ; num_plain_nodes *)
60
         ; start_filtered_logs
61
         ; proof_config
62
         ; k
63
         ; delta
64
         ; slots_per_epoch
65
         ; slots_per_sub_window
66
         ; grace_period_slots
67
         ; txpool_max_size
68
         ; slot_tx_end
69
         ; slot_chain_end
70
         ; network_id
71
         ; _
72
         }
73
          : Test_config.t ) =
74
      test_config
75
    in
76
    let git_commit = Mina_version.commit_id_short in
77
    let stack_name = "it-" ^ git_commit ^ "-" ^ test_name in
78
    let key_names_list =
79
      List.map genesis_ledger ~f:(fun acct -> acct.account_name)
×
80
    in
81
    if List.contains_dup ~compare:String.compare key_names_list then
×
82
      failwith
×
83
        "All accounts in genesis ledger must have unique names.  Check to make \
84
         sure you are not using the same account_name more than once" ;
85
    let all_nodes_names_list =
×
86
      List.map block_producers ~f:(fun acct -> acct.node_name)
×
87
      @ match snark_coordinator with None -> [] | Some n -> [ n.node_name ]
×
88
    in
89
    if List.contains_dup ~compare:String.compare all_nodes_names_list then
90
      failwith
×
91
        "All nodes in testnet must have unique names.  Check to make sure you \
92
         are not using the same node_name more than once" ;
93
    let keypairs =
×
94
      List.take
95
        (List.tl_exn
×
96
           (Array.to_list (Lazy.force Key_gen.Sample_keypairs.keypairs)) )
×
97
        (List.length genesis_ledger)
×
98
    in
99
    let runtime_timing_of_timing = function
×
100
      | Account.Timing.Untimed ->
×
101
          None
102
      | Timed t ->
×
103
          Some
104
            { Runtime_config.Accounts.Single.Timed.initial_minimum_balance =
105
                t.initial_minimum_balance
106
            ; cliff_time = t.cliff_time
107
            ; cliff_amount = t.cliff_amount
108
            ; vesting_period = t.vesting_period
109
            ; vesting_increment = t.vesting_increment
110
            }
111
    in
112
    let add_accounts accounts_and_keypairs =
113
      List.map accounts_and_keypairs
×
114
        ~f:(fun
115
             ( { Test_config.Test_account.balance
116
               ; account_name
117
               ; timing
118
               ; permissions
119
               ; zkapp
120
               }
121
             , (pk, sk) )
122
           ->
123
          let timing = runtime_timing_of_timing timing in
×
124
          let default = Runtime_config.Accounts.Single.default in
×
125
          let account =
126
            { default with
127
              pk = Public_key.Compressed.to_string pk
×
128
            ; sk = Some (Private_key.to_base58_check sk)
×
129
            ; balance = Balance.of_mina_string_exn balance
×
130
            ; delegate = None
131
            ; timing
132
            ; permissions =
133
                Option.map
×
134
                  ~f:Runtime_config.Accounts.Single.Permissions.of_permissions
135
                  permissions
136
            ; zkapp =
137
                Option.map
×
138
                  ~f:Runtime_config.Accounts.Single.Zkapp_account.of_zkapp zkapp
139
            }
140
          in
141
          (account_name, account) )
142
    in
143
    let genesis_accounts_and_keys = List.zip_exn genesis_ledger keypairs in
144
    let genesis_ledger_accounts = add_accounts genesis_accounts_and_keys in
×
145
    let constraint_constants =
×
146
      Genesis_ledger_helper.make_constraint_constants
147
        ~default:constants.constraint_constants proof_config
148
    in
149
    let ledger_is_prefix ledger1 ledger2 =
×
150
      List.is_prefix ledger2 ~prefix:ledger1
×
151
        ~equal:(fun
152
                 ({ account_name = name1; _ } : Test_config.Test_account.t)
153
                 ({ account_name = name2; _ } : Test_config.Test_account.t)
154
               -> String.equal name1 name2 )
×
155
    in
156
    let runtime_config =
157
      { Runtime_config.daemon =
158
          Some
159
            { txpool_max_size = Some txpool_max_size
160
            ; peer_list_url = None
161
            ; zkapp_proof_update_cost = None
162
            ; zkapp_signed_single_update_cost = None
163
            ; zkapp_signed_pair_update_cost = None
164
            ; zkapp_transaction_cost_limit = None
165
            ; max_event_elements = None
166
            ; max_action_elements = None
167
            ; zkapp_cmd_limit_hardcap = None
168
            ; slot_tx_end
169
            ; slot_chain_end
170
            ; minimum_user_command_fee = None
171
            ; network_id
172
            ; sync_ledger_max_subtree_depth = None
173
            ; sync_ledger_default_subtree_depth = None
174
            }
175
      ; genesis =
176
          Some
177
            { k = Some k
178
            ; delta = Some delta
179
            ; slots_per_epoch = Some slots_per_epoch
180
            ; slots_per_sub_window = Some slots_per_sub_window
181
            ; grace_period_slots = Some grace_period_slots
182
            ; genesis_state_timestamp =
183
                Some Core.Time.(to_string_abs ~zone:Zone.utc (now ()))
×
184
            }
185
      ; proof = Some proof_config (* TODO: prebake ledger and only set hash *)
186
      ; ledger =
187
          Some
188
            { base =
189
                Accounts
190
                  (List.map genesis_ledger_accounts ~f:(fun (_name, acct) ->
×
191
                       acct ) )
×
192
            ; add_genesis_winner = None
193
            ; num_accounts = None
194
            ; balances = []
195
            ; hash = None
196
            ; s3_data_hash = None
197
            ; name = None
198
            }
199
      ; epoch_data =
200
          Option.map epoch_data ~f:(fun { staking = staking_ledger; next } ->
×
201
              let genesis_winner_account : Runtime_config.Accounts.single =
×
202
                Runtime_config.Accounts.Single.of_account
×
203
                  Mina_state.Consensus_state_hooks.genesis_winner_account
NEW
204
                |> Or_error.ok_exn
×
205
              in
206
              let ledger_of_epoch_accounts
207
                  (epoch_accounts : Test_config.Test_account.t list) =
208
                let epoch_ledger_accounts =
×
209
                  List.map epoch_accounts
210
                    ~f:(fun
211
                         { account_name; balance; timing; permissions; zkapp }
212
                       ->
213
                      let balance = Balance.of_mina_string_exn balance in
×
214
                      let timing = runtime_timing_of_timing timing in
×
215
                      let genesis_account =
×
216
                        match
217
                          List.Assoc.find genesis_ledger_accounts account_name
218
                            ~equal:String.equal
219
                        with
220
                        | Some acct ->
×
221
                            acct
222
                        | None ->
×
223
                            failwithf
×
224
                              "Epoch ledger account %s not in genesis ledger"
225
                              account_name ()
226
                      in
227
                      { genesis_account with
228
                        balance
229
                      ; timing
230
                      ; permissions =
231
                          Option.map
×
232
                            ~f:
233
                              Runtime_config.Accounts.Single.Permissions
234
                              .of_permissions permissions
235
                      ; zkapp =
236
                          Option.map
×
237
                            ~f:
238
                              Runtime_config.Accounts.Single.Zkapp_account
239
                              .of_zkapp zkapp
240
                      } )
241
                in
242
                ( { base =
×
243
                      Accounts (genesis_winner_account :: epoch_ledger_accounts)
244
                  ; add_genesis_winner = None (* no effect *)
245
                  ; num_accounts = None
246
                  ; balances = []
247
                  ; hash = None
248
                  ; s3_data_hash = None
249
                  ; name = None
250
                  }
251
                  : Runtime_config.Ledger.t )
252
              in
253
              let staking =
254
                let ({ epoch_ledger; epoch_seed }
255
                      : Test_config.Epoch_data.Data.t ) =
256
                  staking_ledger
257
                in
258
                if not (ledger_is_prefix epoch_ledger genesis_ledger) then
×
259
                  failwith "Staking epoch ledger not a prefix of genesis ledger" ;
×
260
                let ledger = ledger_of_epoch_accounts epoch_ledger in
×
261
                let seed = epoch_seed in
×
262
                ({ ledger; seed } : Runtime_config.Epoch_data.Data.t)
263
              in
264
              let next =
265
                Option.map next ~f:(fun { epoch_ledger; epoch_seed } ->
266
                    if
×
267
                      not
268
                        (ledger_is_prefix staking_ledger.epoch_ledger
×
269
                           epoch_ledger )
270
                    then
271
                      failwith
×
272
                        "Staking epoch ledger not a prefix of next epoch ledger" ;
273
                    if not (ledger_is_prefix epoch_ledger genesis_ledger) then
×
274
                      failwith
×
275
                        "Next epoch ledger not a prefix of genesis ledger" ;
276
                    let ledger = ledger_of_epoch_accounts epoch_ledger in
×
277
                    let seed = epoch_seed in
×
278
                    ({ ledger; seed } : Runtime_config.Epoch_data.Data.t) )
279
              in
280
              ({ staking; next } : Runtime_config.Epoch_data.t) )
×
281
      }
282
    in
283
    let genesis_constants =
284
      Or_error.ok_exn
285
        (Genesis_ledger_helper.make_genesis_constants ~logger
×
286
           ~default:constants.genesis_constants runtime_config )
287
    in
288
    let constants : Test_config.constants =
×
289
      { constants with genesis_constants; constraint_constants }
290
    in
291
    let mk_net_keypair keypair_name (pk, sk) =
292
      let keypair =
×
293
        { Keypair.public_key = Public_key.decompress_exn pk; private_key = sk }
×
294
      in
295
      Network_keypair.create_network_keypair ~keypair_name ~keypair
296
    in
297
    let long_commit_id =
298
      if String.is_substring Mina_version.commit_id ~substring:"[DIRTY]" then
299
        String.sub Mina_version.commit_id ~pos:7
×
300
          ~len:(String.length Mina_version.commit_id - 7)
×
301
      else Mina_version.commit_id
×
302
    in
303
    let mina_archive_base_url =
304
      "https://raw.githubusercontent.com/MinaProtocol/mina/" ^ long_commit_id
305
      ^ "/src/app/archive/"
306
    in
307
    let mina_archive_schema_aux_files =
308
      [ sprintf "%screate_schema.sql" mina_archive_base_url ]
×
309
    in
310
    let genesis_keypairs =
311
      List.fold genesis_accounts_and_keys ~init:String.Map.empty
312
        ~f:(fun map ({ account_name; _ }, (pk, sk)) ->
313
          let keypair = mk_net_keypair account_name (pk, sk) in
×
314
          String.Map.add_exn map ~key:account_name ~data:keypair )
×
315
    in
316
    let open Docker_node_config in
×
317
    let open Docker_compose.Dockerfile in
318
    let port_manager = PortManager.create ~min_port:10000 ~max_port:11000 in
319
    let docker_volumes =
320
      [ Base_node_config.runtime_config_volume
321
      ; Base_node_config.entrypoint_volume
322
      ]
323
    in
324
    let generate_random_id () =
325
      let rand_char () =
×
326
        let ascii_a = int_of_char 'a' in
×
327
        let ascii_z = int_of_char 'z' in
×
328
        char_of_int (ascii_a + Random.int (ascii_z - ascii_a + 1))
×
329
      in
330
      String.init 4 ~f:(fun _ -> rand_char ())
×
331
    in
332
    let seed_config =
333
      let config : Seed_config.config =
334
        { archive_address = None
335
        ; base_config =
336
            Base_node_config.default ~peer:None
337
              ~runtime_config_path:
338
                (Some Base_node_config.container_runtime_config_path)
339
              ~start_filtered_logs
340
        }
341
      in
342
      Seed_config.create
343
        ~service_name:(sprintf "seed-%s" (generate_random_id ()))
×
344
        ~image:images.mina
345
        ~ports:(PortManager.allocate_ports_for_node port_manager)
×
346
        ~volumes:(docker_volumes @ [ Seed_config.seed_libp2p_keypair ])
347
        ~config
348
    in
349
    let seed_config_peer =
350
      Some
351
        (Seed_config.create_libp2p_peer ~peer_name:seed_config.service_name
352
           ~external_port:PortManager.mina_internal_external_port )
353
    in
354
    let archive_node_configs =
355
      List.init num_archive_nodes ~f:(fun index ->
356
          let config =
×
357
            { Postgres_config.host =
358
                sprintf "postgres-%d-%s" (index + 1) (generate_random_id ())
×
359
            ; username = "postgres"
360
            ; password = "password"
361
            ; database = "archive"
362
            ; port = PortManager.postgres_internal_port
363
            }
364
          in
365
          let postgres_port =
366
            Service.Port.create
367
              ~published:(PortManager.allocate_port port_manager)
×
368
              ~target:PortManager.postgres_internal_port
369
          in
370
          let postgres_config =
371
            Postgres_config.create ~service_name:config.host
372
              ~image:Postgres_config.postgres_image ~ports:[ postgres_port ]
373
              ~volumes:
374
                [ Postgres_config.postgres_create_schema_volume
375
                ; Postgres_config.postgres_entrypoint_volume
376
                ]
377
              ~config
378
          in
379
          let archive_server_port =
380
            Service.Port.create
381
              ~published:(PortManager.allocate_port port_manager)
×
382
              ~target:PortManager.mina_internal_server_port
383
          in
384
          let config : Archive_node_config.config =
385
            { postgres_config
386
            ; server_port = archive_server_port.target
387
            ; base_config =
388
                Base_node_config.default ~peer:None
389
                  ~runtime_config_path:
390
                    (Some Base_node_config.container_runtime_config_path)
391
                  ~start_filtered_logs
392
            }
393
          in
394
          let archive_rest_port =
395
            Service.Port.create
396
              ~published:(PortManager.allocate_port port_manager)
×
397
              ~target:PortManager.mina_internal_rest_port
398
          in
399
          Archive_node_config.create
400
            ~service_name:
401
              (sprintf "archive-%d-%s" (index + 1) (generate_random_id ()))
×
402
            ~image:images.archive_node
403
            ~ports:[ archive_server_port; archive_rest_port ]
404
            ~volumes:
405
              [ Base_node_config.runtime_config_volume
406
              ; Archive_node_config.archive_entrypoint_volume
407
              ]
408
            ~config )
409
    in
410
    (* Each archive node has it's own seed node *)
411
    let seed_configs =
×
412
      List.mapi archive_node_configs ~f:(fun index archive_config ->
×
413
          let config : Seed_config.config =
×
414
            { archive_address =
415
                Some
416
                  (sprintf "%s:%d" archive_config.service_name
×
417
                     PortManager.mina_internal_server_port )
418
            ; base_config =
419
                Base_node_config.default ~peer:seed_config_peer
420
                  ~runtime_config_path:
421
                    (Some Base_node_config.container_runtime_config_path)
422
                  ~start_filtered_logs
423
            }
424
          in
425
          Seed_config.create
426
            ~service_name:
427
              (sprintf "seed-%d-%s" (index + 1) (generate_random_id ()))
×
428
            ~image:images.mina
429
            ~ports:(PortManager.allocate_ports_for_node port_manager)
×
430
            ~volumes:docker_volumes ~config )
431
      @ [ seed_config ]
432
    in
433
    let block_producer_configs =
434
      List.map block_producers ~f:(fun node ->
435
          let keypair =
×
436
            match
437
              List.find genesis_accounts_and_keys
438
                ~f:(fun ({ account_name; _ }, _keypair) ->
439
                  String.equal account_name node.account_name )
×
440
            with
441
            | Some (_acct, keypair) ->
×
442
                keypair |> mk_net_keypair node.account_name
×
443
            | None ->
×
444
                let failstring =
445
                  Format.sprintf
446
                    "Failing because the account key of all initial block \
447
                     producers must be in the genesis ledger.  name of Node: \
448
                     %s.  name of Account which does not exist: %s"
449
                    node.node_name node.account_name
450
                in
451
                failwith failstring
×
452
          in
453
          let priv_key_path =
454
            Base_node_config.container_keys_path ^/ node.account_name
455
          in
456
          let volumes =
×
457
            [ Service.Volume.create ("keys" ^/ node.account_name) priv_key_path
×
458
            ]
459
            @ docker_volumes
460
          in
461
          let block_producer_config : Block_producer_config.config =
462
            { keypair
463
            ; priv_key_path
464
            ; enable_peer_exchange = true
465
            ; enable_flooding = true
466
            ; base_config =
467
                Base_node_config.default ~peer:seed_config_peer
468
                  ~runtime_config_path:
469
                    (Some Base_node_config.container_runtime_config_path)
470
                  ~start_filtered_logs
471
            }
472
          in
473
          Block_producer_config.create ~service_name:node.node_name
474
            ~image:images.mina
475
            ~ports:(PortManager.allocate_ports_for_node port_manager)
×
476
            ~volumes ~config:block_producer_config )
477
    in
478
    let snark_coordinator_config =
×
479
      match snark_coordinator with
480
      | None ->
×
481
          None
482
      | Some snark_coordinator_node ->
×
483
          let network_kp =
484
            match
485
              String.Map.find genesis_keypairs
486
                snark_coordinator_node.account_name
487
            with
488
            | Some acct ->
×
489
                acct
490
            | None ->
×
491
                let failstring =
492
                  Format.sprintf
493
                    "Failing because the account key of all initial snark \
494
                     coordinators must be in the genesis ledger.  name of \
495
                     Node: %s.  name of Account which does not exist: %s"
496
                    snark_coordinator_node.node_name
497
                    snark_coordinator_node.account_name
498
                in
499
                failwith failstring
×
500
          in
501
          let public_key =
502
            Public_key.Compressed.to_base58_check
503
              (Public_key.compress network_kp.keypair.public_key)
×
504
          in
505
          let coordinator_ports =
×
506
            PortManager.allocate_ports_for_node port_manager
507
          in
508
          let daemon_port =
×
509
            coordinator_ports
510
            |> List.find_exn ~f:(fun p ->
511
                   p.target
×
512
                   = Docker_node_config.PortManager.mina_internal_client_port )
513
          in
514
          let snark_node_service_name = snark_coordinator_node.node_name in
×
515
          let worker_node_config : Snark_worker_config.config =
516
            { daemon_address = snark_node_service_name
517
            ; daemon_port = Int.to_string daemon_port.target
×
518
            ; proof_level = "full"
519
            ; base_config =
520
                Base_node_config.default ~peer:None ~runtime_config_path:None
521
                  ~start_filtered_logs:[]
522
            }
523
          in
524
          let worker_nodes =
525
            List.init snark_coordinator_node.worker_nodes ~f:(fun index ->
526
                Docker_node_config.Snark_worker_config.create
×
527
                  ~service_name:
528
                    (sprintf "snark-worker-%d-%s" (index + 1)
×
529
                       (generate_random_id ()) )
×
530
                  ~image:images.mina
531
                  ~ports:
532
                    (Docker_node_config.PortManager.allocate_ports_for_node
×
533
                       port_manager )
534
                  ~volumes:docker_volumes ~config:worker_node_config )
535
          in
536
          let snark_coordinator_config : Snark_coordinator_config.config =
×
537
            { worker_nodes
538
            ; snark_worker_fee
539
            ; snark_coordinator_key = public_key
540
            ; work_selection = "seq"
541
            ; base_config =
542
                Base_node_config.default ~peer:seed_config_peer
543
                  ~runtime_config_path:
544
                    (Some Base_node_config.container_runtime_config_path)
545
                  ~start_filtered_logs
546
            }
547
          in
548
          Some
549
            (Snark_coordinator_config.create
550
               ~service_name:snark_node_service_name ~image:images.mina
551
               ~ports:coordinator_ports ~volumes:docker_volumes
552
               ~config:snark_coordinator_config )
553
    in
554
    { debug_arg = debug
555
    ; genesis_keypairs
556
    ; commit_id = git_commit
557
    ; constants
558
    ; docker =
559
        { docker_swarm_version
560
        ; stack_name
561
        ; mina_image = images.mina
562
        ; mina_agent_image = images.user_agent
563
        ; mina_points_image = images.points
564
        ; mina_archive_image = images.archive_node
565
        ; runtime_config = Runtime_config.to_yojson runtime_config
×
566
        ; log_precomputed_blocks
567
        ; start_filtered_logs
568
        ; block_producer_configs
569
        ; seed_configs
570
        ; mina_archive_schema_aux_files
571
        ; snark_coordinator_config
572
        ; archive_node_configs
573
        }
574
    }
575

576
  (*
577
     Composes a docker_compose.json file from the network_config specification and writes to disk. This docker_compose
578
     file contains docker service definitions for each node in the local network. Each node service has different
579
     configurations which are specified as commands, environment variables, and docker bind volumes.
580
     We start by creating a runtime config volume to mount to each node service as a bind volume and then continue to create each
581
     node service. As we create each definition for a service, we specify the docker command, volume, and environment varibles to 
582
     be used (which are mostly defaults).
583
  *)
584
  let to_docker network_config =
585
    let open Docker_compose.Dockerfile in
×
586
    let block_producer_map =
587
      List.map network_config.docker.block_producer_configs ~f:(fun config ->
×
588
          (config.service_name, config.docker_config) )
×
589
      |> StringMap.of_alist_exn
590
    in
591
    let seed_map =
×
592
      List.map network_config.docker.seed_configs ~f:(fun config ->
×
593
          (config.service_name, config.docker_config) )
×
594
      |> StringMap.of_alist_exn
595
    in
596
    let snark_coordinator_map =
×
597
      match network_config.docker.snark_coordinator_config with
598
      | Some config ->
×
599
          StringMap.of_alist_exn [ (config.service_name, config.docker_config) ]
×
600
      | None ->
×
601
          StringMap.empty
602
    in
603
    let snark_worker_map =
604
      match network_config.docker.snark_coordinator_config with
605
      | Some snark_coordinator_config ->
×
606
          List.map snark_coordinator_config.config.worker_nodes
×
607
            ~f:(fun config -> (config.service_name, config.docker_config))
×
608
          |> StringMap.of_alist_exn
×
609
      | None ->
×
610
          StringMap.empty
611
    in
612
    let archive_node_map =
613
      List.map network_config.docker.archive_node_configs ~f:(fun config ->
×
614
          (config.service_name, config.docker_config) )
×
615
      |> StringMap.of_alist_exn
616
    in
617
    let postgres_map =
×
618
      List.map network_config.docker.archive_node_configs
×
619
        ~f:(fun archive_config ->
620
          let config = archive_config.config.postgres_config in
×
621
          (config.service_name, config.docker_config) )
622
      |> StringMap.of_alist_exn
623
    in
624
    let services =
×
625
      postgres_map |> merge archive_node_map |> merge snark_worker_map
×
626
      |> merge snark_coordinator_map
×
627
      |> merge block_producer_map |> merge seed_map
×
628
    in
629
    { version = docker_swarm_version; services }
×
630
end
631

632
module Network_manager = struct
633
  type t =
634
    { logger : Logger.t
635
    ; stack_name : string
636
    ; graphql_enabled : bool
637
    ; docker_dir : string
638
    ; docker_compose_file_path : string
639
    ; constants : Test_config.constants
640
    ; seed_workloads : Docker_network.Service_to_deploy.t Core.String.Map.t
641
    ; block_producer_workloads :
642
        Docker_network.Service_to_deploy.t Core.String.Map.t
643
    ; snark_coordinator_workloads :
644
        Docker_network.Service_to_deploy.t Core.String.Map.t
645
    ; snark_worker_workloads :
646
        Docker_network.Service_to_deploy.t Core.String.Map.t
647
    ; archive_workloads : Docker_network.Service_to_deploy.t Core.String.Map.t
648
    ; services_by_id : Docker_network.Service_to_deploy.t Core.String.Map.t
649
    ; mutable deployed : bool
650
    ; genesis_keypairs : Network_keypair.t Core.String.Map.t
651
    }
652

653
  let get_current_running_stacks =
654
    let open Malleable_error.Let_syntax in
655
    let%bind all_stacks_str =
656
      Util.run_cmd_or_hard_error "/" "docker"
1✔
657
        [ "stack"; "ls"; "--format"; "{{.Name}}" ]
658
    in
659
    return (String.split ~on:'\n' all_stacks_str)
×
660

661
  let remove_stack_if_exists ~logger (network_config : Network_config.t) =
662
    let open Malleable_error.Let_syntax in
×
663
    let%bind all_stacks = get_current_running_stacks in
664
    if List.mem all_stacks network_config.docker.stack_name ~equal:String.equal
×
665
    then
666
      let%bind () =
667
        if network_config.debug_arg then
668
          Deferred.bind ~f:Malleable_error.return
×
669
            (Util.prompt_continue
×
670
               "Existing stack name of same name detected, pausing startup. \
671
                Enter [y/Y] to continue on and remove existing stack name, \
672
                start clean, and run the test; press Ctrl-C to quit out: " )
673
        else
674
          Malleable_error.return
×
675
            ([%log info]
×
676
               "Existing stack of same name detected; removing to start clean" )
677
      in
678
      Util.run_cmd_or_hard_error "/" "docker"
×
679
        [ "stack"; "rm"; network_config.docker.stack_name ]
680
      >>| Fn.const ()
×
681
    else return ()
×
682

683
  let generate_docker_stack_file ~logger ~docker_dir ~docker_compose_file_path
684
      ~network_config =
685
    let open Deferred.Let_syntax in
×
686
    let%bind () =
687
      if%bind Mina_stdlib_unix.File_system.dir_exists docker_dir then (
×
688
        [%log info] "Old docker stack directory found; removing to start clean" ;
×
689
        Mina_stdlib_unix.File_system.remove_dir docker_dir )
×
690
      else return ()
691
    in
692
    [%log info] "Writing docker configuration %s" docker_dir ;
×
693
    let%bind () = Unix.mkdir docker_dir in
×
694
    let%bind _ =
695
      Docker_compose.Dockerfile.write_config ~dir:docker_dir
×
696
        ~filename:docker_compose_file_path
697
        (Network_config.to_docker network_config)
×
698
    in
699
    return ()
×
700

701
  let write_docker_bind_volumes ~logger ~docker_dir
702
      ~(network_config : Network_config.t) =
703
    let open Deferred.Let_syntax in
×
704
    [%log info] "Writing runtime_config %s" docker_dir ;
×
705
    let%bind () =
706
      Yojson.Safe.to_file
×
707
        (String.concat [ docker_dir; "/runtime_config.json" ])
×
708
        network_config.docker.runtime_config
709
      |> Deferred.return
×
710
    in
711
    [%log info] "Writing out the genesis keys to dir %s" docker_dir ;
×
712
    let kps_base_path = String.concat [ docker_dir; "/keys" ] in
×
713
    let%bind () = Unix.mkdir kps_base_path in
×
714
    [%log info] "Writing genesis keys to %s" kps_base_path ;
×
715
    let%bind () =
716
      Core.String.Map.iter network_config.genesis_keypairs ~f:(fun kp ->
×
717
          let keypath = String.concat [ kps_base_path; "/"; kp.keypair_name ] in
×
718
          Out_channel.with_file ~fail_if_exists:true keypath ~f:(fun ch ->
×
719
              kp.private_key |> Out_channel.output_string ch ) ;
×
720
          Out_channel.with_file ~fail_if_exists:true (keypath ^ ".pub")
×
721
            ~f:(fun ch -> kp.public_key |> Out_channel.output_string ch) ;
×
722
          ignore
×
723
            (Util.run_cmd_exn kps_base_path "chmod" [ "600"; kp.keypair_name ]) )
×
724
      |> Deferred.return
×
725
    in
726
    [%log info] "Writing seed libp2p keypair to %s" kps_base_path ;
×
727
    let%bind () =
728
      let keypath = String.concat [ kps_base_path; "/"; "libp2p_key" ] in
729
      Out_channel.with_file ~fail_if_exists:true keypath ~f:(fun ch ->
×
730
          Docker_node_config.Seed_config.libp2p_keypair
×
731
          |> Out_channel.output_string ch ) ;
732
      ignore (Util.run_cmd_exn kps_base_path "chmod" [ "600"; "libp2p_key" ]) ;
×
733
      return ()
×
734
    in
735
    let%bind () =
736
      ignore (Util.run_cmd_exn docker_dir "chmod" [ "700"; "keys" ])
×
737
      |> Deferred.return
×
738
    in
739
    [%log info]
×
740
      "Writing custom entrypoint script (libp2p key generation and puppeteer \
741
       context)" ;
742
    let entrypoint_filename, entrypoint_script =
×
743
      Docker_node_config.Base_node_config.entrypoint_script
744
        network_config.commit_id
745
    in
746
    Out_channel.with_file ~fail_if_exists:true
×
747
      (docker_dir ^/ entrypoint_filename) ~f:(fun ch ->
×
748
        entrypoint_script |> Out_channel.output_string ch ) ;
×
749
    [%log info]
×
750
      "Writing custom archive entrypoint script (wait for postgres to \
751
       initialize)" ;
752
    let archive_filename, archive_script =
×
753
      Docker_node_config.Archive_node_config.archive_entrypoint_script
754
    in
755
    Out_channel.with_file ~fail_if_exists:true (docker_dir ^/ archive_filename)
×
756
      ~f:(fun ch -> archive_script |> Out_channel.output_string ch) ;
×
757
    ignore (Util.run_cmd_exn docker_dir "chmod" [ "+x"; archive_filename ]) ;
×
758
    let%bind _ =
759
      Deferred.List.iter network_config.docker.mina_archive_schema_aux_files
×
760
        ~f:(fun schema_url ->
761
          let filename = Filename.basename schema_url in
×
762
          [%log info] "Downloading %s" schema_url ;
×
763
          let%bind _ =
764
            Util.run_cmd_or_hard_error docker_dir "curl"
×
765
              [ "-o"; filename; schema_url ]
766
          in
767
          [%log info]
×
768
            "Writing custom postgres entrypoint script (import archive node \
769
             schema)" ;
770

771
          Deferred.return () )
×
772
      |> Deferred.return
×
773
    in
774
    ignore (Util.run_cmd_exn docker_dir "chmod" [ "+x"; entrypoint_filename ]) ;
×
775
    [%log info] "Writing custom postgres entrypoint script (create schema)" ;
×
776
    let postgres_entrypoint_filename, postgres_entrypoint_script =
×
777
      Docker_node_config.Postgres_config.postgres_script
778
    in
779
    Out_channel.with_file ~fail_if_exists:true
780
      (docker_dir ^/ postgres_entrypoint_filename) ~f:(fun ch ->
×
781
        postgres_entrypoint_script |> Out_channel.output_string ch ) ;
×
782
    ignore
×
783
      (Util.run_cmd_exn docker_dir "chmod"
×
784
         [ "+x"; postgres_entrypoint_filename ] ) ;
785
    return ()
786

787
  let initialize_workloads ~logger (network_config : Network_config.t) =
788
    let find_rest_port ports =
×
789
      List.find_map_exn ports ~f:(fun port ->
×
790
          match port with
×
791
          | Docker_compose.Dockerfile.Service.Port.{ published; target } ->
×
792
              if target = Docker_node_config.PortManager.mina_internal_rest_port
793
              then Some published
×
794
              else None )
×
795
    in
796
    [%log info] "Initializing seed workloads" ;
×
797
    let seed_workloads =
×
798
      List.map network_config.docker.seed_configs ~f:(fun seed_config ->
×
799
          let graphql_port = find_rest_port seed_config.docker_config.ports in
×
800
          let node =
×
801
            Docker_network.Service_to_deploy.construct_service
802
              network_config.docker.stack_name seed_config.service_name
803
              (Docker_network.Service_to_deploy.init_service_to_deploy_config
804
                 ~network_keypair:None ~postgres_connection_uri:None
805
                 ~graphql_port )
806
          in
807
          (seed_config.service_name, node) )
×
808
      |> Core.String.Map.of_alist_exn
809
    in
810
    [%log info] "Initializing block producer workloads" ;
×
811
    let block_producer_workloads =
×
812
      List.map network_config.docker.block_producer_configs ~f:(fun bp_config ->
×
813
          let graphql_port = find_rest_port bp_config.docker_config.ports in
×
814
          let node =
×
815
            Docker_network.Service_to_deploy.construct_service
816
              network_config.docker.stack_name bp_config.service_name
817
              (Docker_network.Service_to_deploy.init_service_to_deploy_config
818
                 ~network_keypair:(Some bp_config.config.keypair)
819
                 ~postgres_connection_uri:None ~graphql_port )
820
          in
821
          (bp_config.service_name, node) )
×
822
      |> Core.String.Map.of_alist_exn
823
    in
824
    [%log info] "Initializing snark coordinator and worker workloads" ;
×
825
    let snark_coordinator_workloads, snark_worker_workloads =
×
826
      match network_config.docker.snark_coordinator_config with
827
      | Some snark_coordinator_config ->
×
828
          let snark_coordinator_workloads =
829
            if List.length snark_coordinator_config.config.worker_nodes > 0 then
×
830
              let graphql_port =
×
831
                find_rest_port snark_coordinator_config.docker_config.ports
832
              in
833
              let coordinator =
×
834
                Docker_network.Service_to_deploy.construct_service
835
                  network_config.docker.stack_name
836
                  snark_coordinator_config.service_name
837
                  (Docker_network.Service_to_deploy
838
                   .init_service_to_deploy_config ~network_keypair:None
839
                     ~postgres_connection_uri:None ~graphql_port )
840
              in
841
              [ (snark_coordinator_config.service_name, coordinator) ]
×
842
              |> Core.String.Map.of_alist_exn
×
843
            else Core.String.Map.empty
×
844
          in
845
          let snark_worker_workloads =
846
            List.map snark_coordinator_config.config.worker_nodes
×
847
              ~f:(fun snark_worker_config ->
848
                let graphql_port =
×
849
                  find_rest_port snark_worker_config.docker_config.ports
850
                in
851
                let worker =
×
852
                  Docker_network.Service_to_deploy.construct_service
853
                    network_config.docker.stack_name
854
                    snark_worker_config.service_name
855
                    (Docker_network.Service_to_deploy
856
                     .init_service_to_deploy_config ~network_keypair:None
857
                       ~postgres_connection_uri:None ~graphql_port )
858
                in
859

860
                (snark_worker_config.service_name, worker) )
×
861
            |> Core.String.Map.of_alist_exn
862
          in
863
          (snark_coordinator_workloads, snark_worker_workloads)
×
864
      | None ->
×
865
          (Core.String.Map.of_alist_exn [], Core.String.Map.of_alist_exn [])
×
866
    in
867
    [%log info] "Initializing archive node workloads" ;
×
868
    let archive_workloads =
×
869
      List.map network_config.docker.archive_node_configs
×
870
        ~f:(fun archive_config ->
871
          let graphql_port =
×
872
            find_rest_port archive_config.docker_config.ports
873
          in
874
          let postgres_connection_uri =
×
875
            Some
876
              (Docker_node_config.Postgres_config.to_connection_uri
×
877
                 archive_config.config.postgres_config.config )
878
          in
879
          let node =
880
            Docker_network.Service_to_deploy.construct_service
881
              network_config.docker.stack_name archive_config.service_name
882
              (Docker_network.Service_to_deploy.init_service_to_deploy_config
883
                 ~network_keypair:None ~postgres_connection_uri ~graphql_port )
884
          in
885
          (archive_config.service_name, node) )
×
886
      |> Core.String.Map.of_alist_exn
887
    in
888
    ( seed_workloads
×
889
    , block_producer_workloads
890
    , snark_coordinator_workloads
891
    , snark_worker_workloads
892
    , archive_workloads )
893

894
  let poll_until_stack_deployed ~logger =
895
    let poll_interval = Time.Span.of_sec 15.0 in
×
896
    let max_polls = 60 (* 15 mins *) in
×
897
    let get_service_statuses () =
898
      let%bind output =
899
        Util.run_cmd_exn "/" "docker"
×
900
          [ "service"; "ls"; "--format"; "{{.Name}}: {{.Replicas}}" ]
901
      in
902
      return
×
903
        ( output |> String.split_lines
×
904
        |> List.map ~f:(fun line ->
×
905
               match String.split ~on:':' line with
×
906
               | [ name; replicas ] ->
×
907
                   (String.strip name, String.strip replicas)
×
908
               | _ ->
×
909
                   failwith "Unexpected format for docker service output" ) )
910
    in
911
    let rec poll n =
912
      [%log debug] "Checking Docker service statuses, n=%d" n ;
×
913
      let%bind service_statuses = get_service_statuses () in
×
914
      let bad_service_statuses =
×
915
        List.filter service_statuses ~f:(fun (_, status) ->
916
            let parts = String.split ~on:'/' status in
×
917
            assert (List.length parts = 2) ;
×
918
            let num, denom =
919
              ( String.strip (List.nth_exn parts 0)
×
920
              , String.strip (List.nth_exn parts 1) )
×
921
            in
922
            not (String.equal num denom) )
×
923
      in
924
      let open Malleable_error.Let_syntax in
×
925
      if List.is_empty bad_service_statuses then return ()
×
926
      else if n > 0 then (
×
927
        [%log debug] "Got bad service statuses, polling again ($failed_statuses"
×
928
          ~metadata:
929
            [ ( "failed_statuses"
930
              , `Assoc
931
                  (List.Assoc.map bad_service_statuses ~f:(fun v -> `String v))
×
932
              )
933
            ] ;
934
        let%bind () =
935
          after poll_interval |> Deferred.bind ~f:Malleable_error.return
×
936
        in
937
        poll (n - 1) )
×
938
      else
939
        let bad_service_statuses_json =
×
940
          `List
941
            (List.map bad_service_statuses ~f:(fun (service_name, status) ->
×
942
                 `Assoc
×
943
                   [ ("service_name", `String service_name)
944
                   ; ("status", `String status)
945
                   ] ) )
946
        in
947
        [%log fatal]
×
948
          "Not all services could be deployed in time: $bad_service_statuses"
949
          ~metadata:[ ("bad_service_statuses", bad_service_statuses_json) ] ;
950
        Malleable_error.hard_error_string ~exit_code:4
×
951
          (Yojson.Safe.to_string bad_service_statuses_json)
×
952
    in
953
    [%log info] "Waiting for Docker services to be deployed" ;
×
954
    let res = poll max_polls in
×
955
    match%bind.Deferred res with
956
    | Error _ ->
×
957
        [%log error] "Not all Docker services were deployed, cannot proceed!" ;
×
958
        res
×
959
    | Ok _ ->
×
960
        [%log info] "Docker services deployed" ;
×
961
        res
×
962

963
  let create ~logger (network_config : Network_config.t) =
964
    let open Malleable_error.Let_syntax in
×
965
    let%bind () = remove_stack_if_exists ~logger network_config in
×
966
    let ( seed_workloads
×
967
        , block_producer_workloads
968
        , snark_coordinator_workloads
969
        , snark_worker_workloads
970
        , archive_workloads ) =
971
      initialize_workloads ~logger network_config
972
    in
973
    let services_by_id =
×
974
      let all_workloads =
975
        Core.String.Map.data seed_workloads
×
976
        @ Core.String.Map.data snark_coordinator_workloads
×
977
        @ Core.String.Map.data snark_worker_workloads
×
978
        @ Core.String.Map.data block_producer_workloads
×
979
        @ Core.String.Map.data archive_workloads
×
980
      in
981
      all_workloads
982
      |> List.map ~f:(fun w -> (w.service_name, w))
×
983
      |> String.Map.of_alist_exn
×
984
    in
985
    let open Deferred.Let_syntax in
986
    let docker_dir = network_config.docker.stack_name in
987
    let docker_compose_file_path =
988
      network_config.docker.stack_name ^ ".compose.json"
989
    in
990
    let%bind () =
991
      generate_docker_stack_file ~logger ~docker_dir ~docker_compose_file_path
992
        ~network_config
993
    in
994
    let%bind () =
995
      write_docker_bind_volumes ~logger ~docker_dir ~network_config
996
    in
997
    let t =
×
998
      { stack_name = network_config.docker.stack_name
999
      ; logger
1000
      ; docker_dir
1001
      ; docker_compose_file_path
1002
      ; constants = network_config.constants
1003
      ; graphql_enabled = true
1004
      ; seed_workloads
1005
      ; block_producer_workloads
1006
      ; snark_coordinator_workloads
1007
      ; snark_worker_workloads
1008
      ; archive_workloads
1009
      ; services_by_id
1010
      ; deployed = false
1011
      ; genesis_keypairs = network_config.genesis_keypairs
1012
      }
1013
    in
1014
    [%log info] "Initializing docker swarm" ;
×
1015
    Malleable_error.return t
×
1016

1017
  let deploy t =
1018
    let logger = t.logger in
×
1019
    if t.deployed then failwith "network already deployed" ;
×
1020
    [%log info] "Deploying stack '%s' from %s" t.stack_name t.docker_dir ;
×
1021
    let open Malleable_error.Let_syntax in
×
1022
    let%bind (_ : string) =
1023
      Util.run_cmd_or_hard_error t.docker_dir "docker"
×
1024
        [ "stack"; "deploy"; "-c"; t.docker_compose_file_path; t.stack_name ]
1025
    in
1026
    t.deployed <- true ;
×
1027
    let%bind () = poll_until_stack_deployed ~logger in
1028
    let open Malleable_error.Let_syntax in
×
1029
    let func_for_fold ~(key : string) ~data accum_M =
1030
      let%bind mp = accum_M in
1031
      let%map node =
1032
        Docker_network.Service_to_deploy.get_node_from_service data
×
1033
      in
1034
      Core.String.Map.add_exn mp ~key ~data:node
×
1035
    in
1036
    let%map seeds =
1037
      Core.String.Map.fold t.seed_workloads
×
1038
        ~init:(Malleable_error.return Core.String.Map.empty)
×
1039
        ~f:func_for_fold
1040
    and block_producers =
1041
      Core.String.Map.fold t.block_producer_workloads
×
1042
        ~init:(Malleable_error.return Core.String.Map.empty)
×
1043
        ~f:func_for_fold
1044
    and snark_coordinators =
1045
      Core.String.Map.fold t.snark_coordinator_workloads
×
1046
        ~init:(Malleable_error.return Core.String.Map.empty)
×
1047
        ~f:func_for_fold
1048
    and snark_workers =
1049
      Core.String.Map.fold t.snark_worker_workloads
×
1050
        ~init:(Malleable_error.return Core.String.Map.empty)
×
1051
        ~f:func_for_fold
1052
    and archive_nodes =
1053
      Core.String.Map.fold t.archive_workloads
×
1054
        ~init:(Malleable_error.return Core.String.Map.empty)
×
1055
        ~f:func_for_fold
1056
    in
1057
    let network =
×
1058
      { Docker_network.namespace = t.stack_name
1059
      ; constants = t.constants
1060
      ; seeds
1061
      ; block_producers
1062
      ; snark_coordinators
1063
      ; snark_workers
1064
      ; archive_nodes
1065
      ; genesis_keypairs = t.genesis_keypairs
1066
      }
1067
    in
1068
    let nodes_to_string =
1069
      Fn.compose (String.concat ~sep:", ") (List.map ~f:Docker_network.Node.id)
1070
    in
1071
    [%log info] "Network deployed" ;
×
1072
    [%log info] "testnet namespace: %s" t.stack_name ;
×
1073
    [%log info] "snark coordinators: %s"
×
1074
      (nodes_to_string (Core.String.Map.data network.snark_coordinators)) ;
×
1075
    [%log info] "snark workers: %s"
×
1076
      (nodes_to_string (Core.String.Map.data network.snark_workers)) ;
×
1077
    [%log info] "block producers: %s"
×
1078
      (nodes_to_string (Core.String.Map.data network.block_producers)) ;
×
1079
    [%log info] "archive nodes: %s"
×
1080
      (nodes_to_string (Core.String.Map.data network.archive_nodes)) ;
×
1081
    network
×
1082

1083
  let destroy t =
1084
    [%log' info t.logger] "Destroying network" ;
×
1085
    if not t.deployed then failwith "network not deployed" ;
×
1086
    let%bind _ =
1087
      Util.run_cmd_exn "/" "docker" [ "stack"; "rm"; t.stack_name ]
×
1088
    in
1089
    t.deployed <- false ;
×
1090
    Deferred.unit
1091

1092
  let cleanup t =
1093
    let%bind () = if t.deployed then destroy t else return () in
×
1094
    [%log' info t.logger] "Cleaning up network configuration" ;
×
1095
    let%bind () = Mina_stdlib_unix.File_system.remove_dir t.docker_dir in
×
1096
    Deferred.unit
×
1097

1098
  let destroy t =
1099
    Deferred.Or_error.try_with ~here:[%here] (fun () -> destroy t)
×
1100
    |> Deferred.bind ~f:Malleable_error.or_hard_error
1101
end
1✔
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