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

MinaProtocol / mina / 2863

05 Nov 2024 06:20PM UTC coverage: 30.754% (-16.6%) from 47.311%
2863

push

buildkite

web-flow
Merge pull request #16296 from MinaProtocol/dkijania/more_multi_jobs

more multi jobs in CI

20276 of 65930 relevant lines covered (30.75%)

8631.7 hits per line

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

0.39
/src/lib/network_pool/transaction_pool.ml
1
(** A pool of transactions that can be included in future blocks. Combined with
2
    the Network_pool module, this handles storing and gossiping the correct
3
    transactions (user commands) and providing them to the block producer code.
4
*)
5

6
(* Only show stdout for failed inline tests.*)
1✔
7
open Inline_test_quiet_logs
8
open Core
9
open Async
10
open Mina_base
11
open Mina_transaction
12
open Pipe_lib
13
open Network_peer
14

15
let max_per_15_seconds = 10
16

17
(* TEMP HACK UNTIL DEFUNCTORING: transition frontier interface is simplified *)
18
module type Transition_frontier_intf = sig
19
  type t
20

21
  type staged_ledger
22

23
  module Breadcrumb : sig
24
    type t
25

26
    val staged_ledger : t -> staged_ledger
27
  end
28

29
  type best_tip_diff =
30
    { new_commands : User_command.Valid.t With_status.t list
31
    ; removed_commands : User_command.Valid.t With_status.t list
32
    ; reorg_best_tip : bool
33
    }
34

35
  val best_tip : t -> Breadcrumb.t
36

37
  val best_tip_diff_pipe : t -> best_tip_diff Broadcast_pipe.Reader.t
38
end
39

40
(* versioned type, outside of functors *)
41
module Diff_versioned = struct
42
  [%%versioned
43
  module Stable = struct
44
    [@@@no_toplevel_latest_type]
45

46
    module V2 = struct
47
      type t = User_command.Stable.V2.t list [@@deriving sexp, yojson, hash]
×
48

49
      let to_latest = Fn.id
50
    end
51
  end]
52

53
  (* We defer do any checking on signed-commands until the call to
54
     [add_from_gossip_gossip_exn].
55

56
     The real solution would be to have more explicit queueing to make sure things don't happen out of order, factor
57
     [add_from_gossip_gossip_exn] into [check_from_gossip_exn] (which just does
58
     the checks) and [set_from_gossip_exn] (which just does the mutating the pool),
59
     and do the same for snapp commands as well.
60
  *)
61
  type t = User_command.t list [@@deriving sexp, yojson]
×
62

63
  module Diff_error = struct
64
    [%%versioned
65
    module Stable = struct
66
      [@@@no_toplevel_latest_type]
67

68
      module V3 = struct
69
        type t =
2✔
70
          | Insufficient_replace_fee
×
71
          | Duplicate
×
72
          | Invalid_nonce
×
73
          | Insufficient_funds
×
74
          | Overflow
×
75
          | Bad_token
×
76
          | Unwanted_fee_token
×
77
          | Expired
×
78
          | Overloaded
×
79
          | Fee_payer_account_not_found
×
80
          | Fee_payer_not_permitted_to_send
×
81
          | After_slot_tx_end
×
82
        [@@deriving sexp, yojson, compare]
5✔
83

84
        let to_latest = Fn.id
85
      end
86
    end]
87

88
    (* IMPORTANT! Do not change the names of these errors as to adjust the
89
     * to_yojson output without updating Rosetta's construction API to handle
90
     * the changes *)
91
    type t = Stable.Latest.t =
×
92
      | Insufficient_replace_fee
×
93
      | Duplicate
×
94
      | Invalid_nonce
×
95
      | Insufficient_funds
×
96
      | Overflow
×
97
      | Bad_token
×
98
      | Unwanted_fee_token
×
99
      | Expired
×
100
      | Overloaded
×
101
      | Fee_payer_account_not_found
×
102
      | Fee_payer_not_permitted_to_send
×
103
      | After_slot_tx_end
×
104
    [@@deriving sexp, yojson]
105

106
    let to_string_name = function
107
      | Insufficient_replace_fee ->
×
108
          "insufficient_replace_fee"
109
      | Duplicate ->
×
110
          "duplicate"
111
      | Invalid_nonce ->
×
112
          "invalid_nonce"
113
      | Insufficient_funds ->
×
114
          "insufficient_funds"
115
      | Overflow ->
×
116
          "overflow"
117
      | Bad_token ->
×
118
          "bad_token"
119
      | Unwanted_fee_token ->
×
120
          "unwanted_fee_token"
121
      | Expired ->
×
122
          "expired"
123
      | Overloaded ->
×
124
          "overloaded"
125
      | Fee_payer_account_not_found ->
×
126
          "fee_payer_account_not_found"
127
      | Fee_payer_not_permitted_to_send ->
×
128
          "fee_payer_not_permitted_to_send"
129
      | After_slot_tx_end ->
×
130
          "after_slot_tx_end"
131

132
    let to_string_hum = function
133
      | Insufficient_replace_fee ->
×
134
          "This transaction would have replaced an existing transaction in the \
135
           pool, but the fee was too low"
136
      | Duplicate ->
×
137
          "This transaction is a duplicate of one already in the pool"
138
      | Invalid_nonce ->
×
139
          "This transaction had an invalid nonce"
140
      | Insufficient_funds ->
×
141
          "There are not enough funds in the fee-payer's account to execute \
142
           this transaction"
143
      | Overflow ->
×
144
          "Executing this transaction would result in an integer overflow"
145
      | Bad_token ->
×
146
          "This transaction uses non-default tokens where they are not \
147
           permitted"
148
      | Unwanted_fee_token ->
×
149
          "This transaction pays fees in a non-default token that this pool \
150
           does not accept"
151
      | Expired ->
×
152
          "This transaction has expired"
153
      | Overloaded ->
×
154
          "The diff containing this transaction was too large"
155
      | Fee_payer_account_not_found ->
×
156
          "Fee payer account was not found in the best tip ledger"
157
      | Fee_payer_not_permitted_to_send ->
×
158
          "Fee payer account permissions don't allow sending funds"
159
      | After_slot_tx_end ->
×
160
          "This transaction was submitted after the slot defined to stop \
161
           accepting transactions"
162
  end
163

164
  module Rejected = struct
165
    [%%versioned
166
    module Stable = struct
167
      [@@@no_toplevel_latest_type]
168

169
      module V3 = struct
170
        type t = (User_command.Stable.V2.t * Diff_error.Stable.V3.t) list
×
171
        [@@deriving sexp, yojson, compare]
5✔
172

173
        let to_latest = Fn.id
174
      end
175
    end]
176

177
    type t = Stable.Latest.t [@@deriving sexp, yojson, compare]
×
178
  end
179

180
  type rejected = Rejected.t [@@deriving sexp, yojson, compare]
×
181

182
  type verified = Transaction_hash.User_command_with_valid_signature.t list
×
183
  [@@deriving sexp, to_yojson]
184

185
  let summary t =
186
    Printf.sprintf
×
187
      !"Transaction_pool_diff of length %d with fee payer summary %s"
188
      (List.length t)
×
189
      ( String.concat ~sep:","
×
190
      @@ List.map ~f:User_command.fee_payer_summary_string t )
×
191

192
  let is_empty t = List.is_empty t
×
193
end
194

195
type Structured_log_events.t +=
196
  | Rejecting_command_for_reason of
197
      { command : User_command.t
×
198
      ; reason : Diff_versioned.Diff_error.t
×
199
      ; error_extra : (string * Yojson.Safe.t) list
×
200
      }
201
  [@@deriving register_event { msg = "Rejecting command because: $reason" }]
×
202

203
module type S = sig
204
  open Intf
205

206
  type transition_frontier
207

208
  module Resource_pool : sig
209
    include
210
      Transaction_resource_pool_intf
211
        with type transition_frontier := transition_frontier
212

213
    module Diff :
214
      Transaction_pool_diff_intf
215
        with type resource_pool := t
216
         and type Diff_error.t = Diff_versioned.Diff_error.t
217
         and type Rejected.t = Diff_versioned.Rejected.t
218
  end
219

220
  include
221
    Network_pool_base_intf
222
      with type resource_pool := Resource_pool.t
223
       and type transition_frontier := transition_frontier
224
       and type resource_pool_diff := Diff_versioned.t
225
       and type resource_pool_diff_verified := Diff_versioned.verified
226
       and type config := Resource_pool.Config.t
227
       and type transition_frontier_diff :=
228
        Resource_pool.transition_frontier_diff
229
       and type rejected_diff := Diff_versioned.rejected
230
end
231

232
(* Functor over user command, base ledger and transaction validator for
233
   mocking. *)
234
module Make0
235
    (Base_ledger : Intf.Base_ledger_intf) (Staged_ledger : sig
236
      type t
237

238
      val ledger : t -> Base_ledger.t
239
    end)
240
    (Transition_frontier : Transition_frontier_intf
241
                             with type staged_ledger := Staged_ledger.t) =
242
struct
243
  type verification_failure =
×
244
    | Command_failure of Diff_versioned.Diff_error.t
×
245
    | Invalid_failure of Verifier.invalid
×
246
  [@@deriving to_yojson]
247

248
  module Breadcrumb = Transition_frontier.Breadcrumb
249

250
  module Resource_pool = struct
251
    type transition_frontier_diff =
252
      Transition_frontier.best_tip_diff * Base_ledger.t
253

254
    let label = "transaction_pool"
255

256
    let preload_accounts ledger account_ids =
257
      let existing_account_ids, existing_account_locs =
×
258
        Set.to_list account_ids
×
259
        |> Base_ledger.location_of_account_batch ledger
×
260
        |> List.filter_map ~f:(function
×
261
             | id, Some loc ->
×
262
                 Some (id, loc)
263
             | _, None ->
×
264
                 None )
265
        |> List.unzip
266
      in
267
      Base_ledger.get_batch ledger existing_account_locs
×
268
      |> List.map ~f:snd
×
269
      |> List.zip_exn existing_account_ids
×
270
      |> List.fold ~init:Account_id.Map.empty ~f:(fun map (id, maybe_account) ->
271
             let account =
×
272
               Option.value_exn maybe_account
273
                 ~message:"Somehow a public key has a location but no account"
274
             in
275
             Map.add_exn map ~key:id ~data:account )
×
276

277
    module Config = struct
278
      type t =
×
279
        { trust_system : (Trust_system.t[@sexp.opaque])
×
280
        ; pool_max_size : int
×
281
              (* note this value needs to be mostly the same across gossipping nodes, so
282
                 nodes with larger pools don't send nodes with smaller pools lots of
283
                 low fee transactions the smaller-pooled nodes consider useless and get
284
                 themselves banned.
285

286
                 we offer this value separately from the one in genesis_constants, because
287
                 we may wish a different value for testing
288
              *)
289
        ; verifier : (Verifier.t[@sexp.opaque])
×
290
        ; genesis_constants : Genesis_constants.t
×
291
        ; slot_tx_end : Mina_numbers.Global_slot_since_hard_fork.t option
×
292
        }
293
      [@@deriving sexp_of]
294

295
      (* remove next line if there's a way to force [@@deriving make] write a
296
         named parameter instead of an optional parameter *)
297
      let make ~trust_system ~pool_max_size ~verifier ~genesis_constants
298
          ~slot_tx_end =
299
        { trust_system
×
300
        ; pool_max_size
301
        ; verifier
302
        ; genesis_constants
303
        ; slot_tx_end
304
        }
305
    end
306

307
    let make_config = Config.make
308

309
    module Batcher = Batcher.Transaction_pool
310

311
    module Mutex = struct
312
      open Async
313

314
      type t = unit Mvar.Read_write.t
315

316
      let acquire (t : t) = Mvar.take t
×
317

318
      let release (t : t) =
319
        assert (Mvar.is_empty t) ;
×
320
        don't_wait_for (Mvar.put t ())
×
321

322
      let with_ t ~f =
323
        let%bind () = acquire t in
×
324
        let%map x = f () in
×
325
        release t ; x
×
326

327
      let create () =
328
        let t = Mvar.create () in
×
329
        don't_wait_for (Mvar.put t ()) ;
×
330
        t
×
331
    end
332

333
    module Vk_refcount_table = struct
334
      type t =
335
        { verification_keys :
336
            (int * Verification_key_wire.t) Zkapp_basic.F_map.Table.t
337
        ; account_id_to_vks : int Zkapp_basic.F_map.Map.t Account_id.Table.t
338
        ; vk_to_account_ids : int Account_id.Map.t Zkapp_basic.F_map.Table.t
339
        }
340

341
      let create () =
342
        { verification_keys = Zkapp_basic.F_map.Table.create ()
×
343
        ; account_id_to_vks = Account_id.Table.create ()
×
344
        ; vk_to_account_ids = Zkapp_basic.F_map.Table.create ()
×
345
        }
346

347
      let find_vk (t : t) = Hashtbl.find t.verification_keys
×
348

349
      let find_vks_by_account_id (t : t) account_id =
350
        match Hashtbl.find t.account_id_to_vks account_id with
×
351
        | None ->
×
352
            []
353
        | Some vks ->
×
354
            Map.keys vks
×
355
            |> List.map ~f:(find_vk t)
×
356
            |> Option.all
×
357
            |> Option.value_exn ~message:"malformed Vk_refcount_table.t"
×
358
            |> List.map ~f:snd
359

360
      let inc (t : t) ~account_id ~(vk : Verification_key_wire.t) =
361
        let inc_map ~default_map key map =
×
362
          Map.update (Option.value map ~default:default_map) key ~f:(function
×
363
            | None ->
×
364
                1
365
            | Some count ->
×
366
                count + 1 )
367
        in
368
        Hashtbl.update t.verification_keys vk.hash ~f:(function
369
          | None ->
×
370
              (1, vk)
371
          | Some (count, vk) ->
×
372
              (count + 1, vk) ) ;
373
        Hashtbl.update t.account_id_to_vks account_id
×
374
          ~f:(inc_map ~default_map:Zkapp_basic.F_map.Map.empty vk.hash) ;
×
375
        Hashtbl.update t.vk_to_account_ids vk.hash
×
376
          ~f:(inc_map ~default_map:Account_id.Map.empty account_id) ;
×
377
        Mina_metrics.(
×
378
          Gauge.set Transaction_pool.vk_refcount_table_size
379
            (Float.of_int (Zkapp_basic.F_map.Table.length t.verification_keys)))
×
380

381
      let dec (t : t) ~account_id ~vk_hash =
382
        let open Option.Let_syntax in
×
383
        let dec count = if count = 1 then None else Some (count - 1) in
×
384
        let dec_map key map =
385
          let map' = Map.change map key ~f:(Option.bind ~f:dec) in
×
386
          if Map.is_empty map' then None else Some map'
×
387
        in
388
        Hashtbl.change t.verification_keys vk_hash
389
          ~f:
390
            (Option.bind ~f:(fun (count, value) ->
391
                 let%map count' = dec count in
×
392
                 (count', value) ) ) ;
×
393
        Hashtbl.change t.account_id_to_vks account_id
×
394
          ~f:(Option.bind ~f:(dec_map vk_hash)) ;
×
395
        Hashtbl.change t.vk_to_account_ids vk_hash
×
396
          ~f:(Option.bind ~f:(dec_map account_id)) ;
×
397
        Mina_metrics.(
×
398
          Gauge.set Transaction_pool.vk_refcount_table_size
399
            (Float.of_int (Zkapp_basic.F_map.Table.length t.verification_keys)))
×
400

401
      let lift_common (t : t) table_modify cmd =
402
        User_command.extract_vks cmd
×
403
        |> List.iter ~f:(fun (account_id, vk) -> table_modify t ~account_id ~vk)
×
404

405
      let lift (t : t) table_modify (cmd : User_command.Valid.t With_status.t) =
406
        With_status.data cmd |> User_command.forget_check
×
407
        |> lift_common t table_modify
408

409
      let lift_hashed (t : t) table_modify cmd =
410
        Transaction_hash.User_command_with_valid_signature.forget_check cmd
×
411
        |> With_hash.data |> lift_common t table_modify
×
412
    end
413

414
    type t =
×
415
      { mutable pool : Indexed_pool.t
×
416
      ; locally_generated_uncommitted :
×
417
          ( Transaction_hash.User_command_with_valid_signature.t
418
          , Time.t * [ `Batch of int ] )
419
          Hashtbl.t
420
            (** Commands generated on this machine, that are not included in the
421
          current best tip, along with the time they were added. *)
422
      ; locally_generated_committed :
×
423
          ( Transaction_hash.User_command_with_valid_signature.t
424
          , Time.t * [ `Batch of int ] )
425
          Hashtbl.t
426
            (** Ones that are included in the current best tip. *)
427
      ; mutable current_batch : int
×
428
      ; mutable remaining_in_batch : int
×
429
      ; config : Config.t
×
430
      ; logger : (Logger.t[@sexp.opaque])
×
431
      ; batcher : Batcher.t
×
432
      ; mutable best_tip_diff_relay : (unit Deferred.t[@sexp.opaque]) Option.t
×
433
      ; mutable best_tip_ledger : (Base_ledger.t[@sexp.opaque]) Option.t
×
434
      ; verification_key_table : (Vk_refcount_table.t[@sexp.opaque])
×
435
      }
436
    [@@deriving sexp_of]
437

438
    let member t x =
439
      Indexed_pool.member t.pool (Transaction_hash.User_command.of_checked x)
×
440

441
    let transactions t = Indexed_pool.transactions ~logger:t.logger t.pool
×
442

443
    let all_from_account { pool; _ } = Indexed_pool.all_from_account pool
×
444

445
    let get_all { pool; _ } = Indexed_pool.get_all pool
×
446

447
    let find_by_hash x hash = Indexed_pool.find_by_hash x.pool hash
×
448

449
    (** Get the best tip ledger*)
450
    let get_best_tip_ledger frontier =
451
      Transition_frontier.best_tip frontier
×
452
      |> Breadcrumb.staged_ledger |> Staged_ledger.ledger
×
453

454
    let drop_until_below_max_size :
455
           pool_max_size:int
456
        -> Indexed_pool.t
457
        -> Indexed_pool.t
458
           * Transaction_hash.User_command_with_valid_signature.t Sequence.t =
459
     fun ~pool_max_size pool ->
460
      let rec go pool' dropped =
×
461
        if Indexed_pool.size pool' > pool_max_size then (
×
462
          let dropped', pool'' = Indexed_pool.remove_lowest_fee pool' in
463
          assert (not (Sequence.is_empty dropped')) ;
×
464
          go pool'' @@ Sequence.append dropped dropped' )
×
465
        else (pool', dropped)
×
466
      in
467
      go pool @@ Sequence.empty
468

469
    let has_sufficient_fee ~pool_max_size pool cmd : bool =
470
      match Indexed_pool.min_fee pool with
×
471
      | None ->
×
472
          true
473
      | Some min_fee ->
×
474
          if Indexed_pool.size pool >= pool_max_size then
×
475
            Currency.Fee_rate.(User_command.fee_per_wu cmd > min_fee)
×
476
          else true
×
477

478
    let diff_error_of_indexed_pool_error :
479
        Command_error.t -> Diff_versioned.Diff_error.t = function
480
      | Invalid_nonce _ ->
×
481
          Invalid_nonce
482
      | Insufficient_funds _ ->
×
483
          Insufficient_funds
484
      | Insufficient_replace_fee _ ->
×
485
          Insufficient_replace_fee
486
      | Overflow ->
×
487
          Overflow
488
      | Bad_token ->
×
489
          Bad_token
490
      | Unwanted_fee_token _ ->
×
491
          Unwanted_fee_token
492
      | Expired _ ->
×
493
          Expired
494
      | After_slot_tx_end ->
×
495
          After_slot_tx_end
496

497
    let indexed_pool_error_metadata = function
498
      | Command_error.Invalid_nonce (`Between (low, hi), nonce) ->
×
499
          let nonce_json = Account.Nonce.to_yojson in
500
          [ ( "between"
501
            , `Assoc [ ("low", nonce_json low); ("hi", nonce_json hi) ] )
×
502
          ; ("nonce", nonce_json nonce)
×
503
          ]
504
      | Invalid_nonce (`Expected enonce, nonce) ->
×
505
          let nonce_json = Account.Nonce.to_yojson in
506
          [ ("expected_nonce", nonce_json enonce); ("nonce", nonce_json nonce) ]
×
507
      | Insufficient_funds (`Balance bal, amt) ->
×
508
          let amt_json = Currency.Amount.to_yojson in
509

510
          [ ("balance", amt_json bal); ("amount", amt_json amt) ]
×
511
      | Insufficient_replace_fee (`Replace_fee rfee, fee) ->
×
512
          let fee_json = Currency.Fee.to_yojson in
513
          [ ("replace_fee", fee_json rfee); ("fee", fee_json fee) ]
×
514
      | Overflow ->
×
515
          []
516
      | Bad_token ->
×
517
          []
518
      | Unwanted_fee_token fee_token ->
×
519
          [ ("fee_token", Token_id.to_yojson fee_token) ]
×
520
      | Expired
×
521
          ( `Valid_until valid_until
522
          , `Global_slot_since_genesis global_slot_since_genesis ) ->
523
          [ ( "valid_until"
524
            , Mina_numbers.Global_slot_since_genesis.to_yojson valid_until )
×
525
          ; ( "current_global_slot_since_genesis"
526
            , Mina_numbers.Global_slot_since_genesis.to_yojson
×
527
                global_slot_since_genesis )
528
          ]
529
      | After_slot_tx_end ->
×
530
          []
531

532
    let indexed_pool_error_log_info e =
533
      ( Diff_versioned.Diff_error.to_string_name
×
534
          (diff_error_of_indexed_pool_error e)
×
535
      , indexed_pool_error_metadata e )
×
536

537
    let handle_transition_frontier_diff
538
        ( ({ new_commands; removed_commands; reorg_best_tip = _ } :
539
            Transition_frontier.best_tip_diff )
540
        , best_tip_ledger ) t =
541
      (* This runs whenever the best tip changes. The simple case is when the
542
         new best tip is an extension of the old one. There, we just remove any
543
         user commands that were included in it from the transaction pool.
544
         Dealing with a fork is more intricate. In general we want to remove any
545
         commands from the pool that are included in the new best tip; and add
546
         any commands to the pool that were included in the old one but not the
547
         new one, provided they are still valid against the ledger of the best
548
         tip. The goal is that transactions are carried from losing forks to
549
         winning ones as much as possible.
550

551
         The locally generated commands need to move from
552
         locally_generated_uncommitted to locally_generated_committed and vice
553
         versa so those hashtables remain in sync with reality.
554

555
         Don't forget to modify the refcount table as well as remove from the
556
         index pool.
557
      *)
558
      let vk_table_inc = Vk_refcount_table.inc in
×
559
      let vk_table_dec t ~account_id ~(vk : Verification_key_wire.t) =
560
        Vk_refcount_table.dec t ~account_id ~vk_hash:vk.hash
×
561
      in
562
      let vk_table_lift = Vk_refcount_table.lift t.verification_key_table in
563
      let vk_table_lift_hashed =
×
564
        Vk_refcount_table.lift_hashed t.verification_key_table
565
      in
566
      let global_slot = Indexed_pool.global_slot_since_genesis t.pool in
×
567
      t.best_tip_ledger <- Some best_tip_ledger ;
×
568
      let pool_max_size = t.config.pool_max_size in
569
      let log_indexed_pool_error error_str ~metadata cmd =
570
        [%log' debug t.logger]
×
571
          "Couldn't re-add locally generated command $cmd, not valid against \
572
           new ledger. Error: $error"
573
          ~metadata:
574
            ( [ ( "cmd"
575
                , Transaction_hash.User_command_with_valid_signature.to_yojson
×
576
                    cmd )
577
              ; ("error", `String error_str)
578
              ]
579
            @ metadata )
580
      in
581
      List.iter new_commands ~f:(vk_table_lift vk_table_inc) ;
×
582
      List.iter removed_commands ~f:(vk_table_lift vk_table_dec) ;
×
583
      let compact_json =
×
584
        Fn.compose User_command.fee_payer_summary_json User_command.forget_check
585
      in
586
      [%log' trace t.logger]
×
587
        ~metadata:
588
          [ ( "removed"
589
            , `List
590
                (List.map removed_commands
×
591
                   ~f:(With_status.to_yojson compact_json) ) )
×
592
          ; ( "added"
593
            , `List
594
                (List.map new_commands ~f:(With_status.to_yojson compact_json))
×
595
            )
596
          ]
597
        "Diff: removed: $removed added: $added from best tip" ;
598
      let pool', dropped_backtrack =
×
599
        List.fold (List.rev removed_commands) ~init:(t.pool, Sequence.empty)
×
600
          ~f:(fun (pool, dropped_so_far) unhashed_cmd ->
601
            let cmd =
×
602
              Transaction_hash.User_command_with_valid_signature.create
603
                unhashed_cmd.data
604
            in
605
            ( match
×
606
                Hashtbl.find_and_remove t.locally_generated_committed cmd
607
              with
608
            | None ->
×
609
                ()
610
            | Some time_added ->
×
611
                [%log' info t.logger]
×
612
                  "Locally generated command $cmd committed in a block!"
613
                  ~metadata:
614
                    [ ( "cmd"
615
                      , With_status.to_yojson User_command.Valid.to_yojson
×
616
                          unhashed_cmd )
617
                    ] ;
618
                Hashtbl.add_exn t.locally_generated_uncommitted ~key:cmd
×
619
                  ~data:time_added ) ;
620
            let pool', dropped_seq =
621
              match cmd |> Indexed_pool.add_from_backtrack pool with
622
              | Error e ->
×
623
                  let error_str, metadata = indexed_pool_error_log_info e in
624
                  log_indexed_pool_error error_str ~metadata cmd ;
×
625
                  (pool, Sequence.empty)
×
626
              | Ok indexed_pool ->
×
627
                  drop_until_below_max_size ~pool_max_size indexed_pool
×
628
            in
629
            (pool', Sequence.append dropped_so_far dropped_seq) )
×
630
      in
631
      Sequence.iter dropped_backtrack ~f:(vk_table_lift_hashed vk_table_dec) ;
×
632
      (* Track what locally generated commands were removed from the pool
633
         during backtracking due to the max size constraint. *)
634
      let locally_generated_dropped =
×
635
        Sequence.filter dropped_backtrack
×
636
          ~f:(Hashtbl.mem t.locally_generated_uncommitted)
×
637
        |> Sequence.to_list_rev
638
      in
639
      if not (List.is_empty locally_generated_dropped) then
×
640
        [%log' debug t.logger]
×
641
          "Dropped locally generated commands $cmds during backtracking to \
642
           maintain max size. Will attempt to re-add after forwardtracking."
643
          ~metadata:
644
            [ ( "cmds"
645
              , `List
646
                  (List.map
×
647
                     ~f:
648
                       Transaction_hash.User_command_with_valid_signature
649
                       .to_yojson locally_generated_dropped ) )
650
            ] ;
651
      let pool'', dropped_commands =
×
652
        let accounts_to_check =
653
          List.fold (new_commands @ removed_commands) ~init:Account_id.Set.empty
654
            ~f:(fun set cmd ->
655
              let set' =
×
656
                With_status.data cmd |> User_command.forget_check
×
657
                |> User_command.accounts_referenced |> Account_id.Set.of_list
×
658
              in
659
              Set.union set set' )
×
660
        in
661
        let get_account =
×
662
          let existing_account_states_by_id =
663
            preload_accounts best_tip_ledger accounts_to_check
664
          in
665
          fun id ->
×
666
            match Map.find existing_account_states_by_id id with
×
667
            | Some account ->
×
668
                account
669
            | None ->
×
670
                if Set.mem accounts_to_check id then Account.empty
×
671
                else
672
                  failwith
×
673
                    "did not expect Indexed_pool.revalidate to call \
674
                     get_account on account not in accounts_to_check"
675
        in
676
        Indexed_pool.revalidate pool' ~logger:t.logger
×
677
          (`Subset accounts_to_check) get_account
678
      in
679
      let committed_commands, dropped_commit_conflicts =
680
        let command_hashes =
681
          List.fold_left new_commands ~init:Transaction_hash.Set.empty
682
            ~f:(fun set cmd ->
683
              let cmd_hash =
×
684
                With_status.data cmd
×
685
                |> Transaction_hash.User_command_with_valid_signature.create
×
686
                |> Transaction_hash.User_command_with_valid_signature.hash
687
              in
688
              Set.add set cmd_hash )
×
689
        in
690
        Sequence.to_list dropped_commands
×
691
        |> List.partition_tf ~f:(fun cmd ->
×
692
               Set.mem command_hashes
×
693
                 (Transaction_hash.User_command_with_valid_signature.hash cmd) )
×
694
      in
695
      List.iter committed_commands ~f:(fun cmd ->
696
          vk_table_lift_hashed vk_table_dec cmd ;
×
697
          Hashtbl.find_and_remove t.locally_generated_uncommitted cmd
×
698
          |> Option.iter ~f:(fun data ->
699
                 Hashtbl.add_exn t.locally_generated_committed ~key:cmd ~data ) ) ;
×
700
      let commit_conflicts_locally_generated =
×
701
        List.filter dropped_commit_conflicts ~f:(fun cmd ->
702
            Hashtbl.find_and_remove t.locally_generated_uncommitted cmd
×
703
            |> Option.is_some )
704
      in
705
      if not (List.is_empty commit_conflicts_locally_generated) then
×
706
        [%log' info t.logger]
×
707
          "Locally generated commands $cmds dropped because they conflicted \
708
           with a committed command."
709
          ~metadata:
710
            [ ( "cmds"
711
              , `List
712
                  (List.map commit_conflicts_locally_generated
×
713
                     ~f:
714
                       Transaction_hash.User_command_with_valid_signature
715
                       .to_yojson ) )
716
            ] ;
717
      [%log' debug t.logger]
×
718
        !"Finished handling diff. Old pool size %i, new pool size %i. Dropped \
719
          %i commands during backtracking to maintain max size."
720
        (Indexed_pool.size t.pool) (Indexed_pool.size pool'')
×
721
        (Sequence.length dropped_backtrack) ;
×
722
      Mina_metrics.(
×
723
        Gauge.set Transaction_pool.pool_size
×
724
          (Float.of_int (Indexed_pool.size pool''))) ;
×
725
      t.pool <- pool'' ;
726
      List.iter locally_generated_dropped ~f:(fun cmd ->
727
          (* If the dropped transaction was included in the winning chain, it'll
728
             be in locally_generated_committed. If it wasn't, try re-adding to
729
             the pool. *)
730
          let remove_cmd () =
×
731
            vk_table_lift_hashed vk_table_dec cmd ;
×
732
            assert (
×
733
              Option.is_some
×
734
              @@ Hashtbl.find_and_remove t.locally_generated_uncommitted cmd )
×
735
          in
736
          let log_and_remove ?(metadata = []) error_str =
×
737
            log_indexed_pool_error error_str ~metadata cmd ;
×
738
            remove_cmd ()
×
739
          in
740
          if not (Hashtbl.mem t.locally_generated_committed cmd) then
×
741
            if
×
742
              not
743
                (has_sufficient_fee t.pool
×
744
                   (Transaction_hash.User_command_with_valid_signature.command
×
745
                      cmd )
746
                   ~pool_max_size )
747
            then (
×
748
              [%log' info t.logger]
×
749
                "Not re-adding locally generated command $cmd to pool, \
750
                 insufficient fee"
751
                ~metadata:
752
                  [ ( "cmd"
753
                    , Transaction_hash.User_command_with_valid_signature
754
                      .to_yojson cmd )
×
755
                  ] ;
756
              remove_cmd () )
×
757
            else
758
              let unchecked =
×
759
                Transaction_hash.User_command_with_valid_signature.command cmd
760
              in
761
              match
×
762
                Option.bind
763
                  (Base_ledger.location_of_account best_tip_ledger
×
764
                     (User_command.fee_payer unchecked) )
×
765
                  ~f:(Base_ledger.get best_tip_ledger)
×
766
              with
767
              | Some acct -> (
×
768
                  match
769
                    Indexed_pool.add_from_gossip_exn t.pool cmd acct.nonce
770
                      ( Account.liquid_balance_at_slot ~global_slot acct
×
771
                      |> Currency.Balance.to_amount )
×
772
                  with
773
                  | Error e ->
×
774
                      let error_str, metadata = indexed_pool_error_log_info e in
775
                      log_and_remove error_str
×
776
                        ~metadata:
777
                          ( ("user_command", User_command.to_yojson unchecked)
×
778
                          :: metadata )
779
                  | Ok (_, pool''', _) ->
×
780
                      [%log' debug t.logger]
×
781
                        "re-added locally generated command $cmd to \
782
                         transaction pool after reorg"
783
                        ~metadata:
784
                          [ ( "cmd"
785
                            , Transaction_hash.User_command_with_valid_signature
786
                              .to_yojson cmd )
×
787
                          ] ;
788
                      vk_table_lift_hashed Vk_refcount_table.inc cmd ;
×
789
                      Mina_metrics.(
×
790
                        Gauge.set Transaction_pool.pool_size
×
791
                          (Float.of_int (Indexed_pool.size pool'''))) ;
×
792
                      t.pool <- pool''' )
793
              | None ->
×
794
                  log_and_remove "Fee_payer_account not found"
795
                    ~metadata:
796
                      [ ("user_command", User_command.to_yojson unchecked) ] ) ;
×
797
      (*Remove any expired user commands*)
798
      let expired_commands, pool = Indexed_pool.remove_expired t.pool in
×
799
      Sequence.iter expired_commands ~f:(fun cmd ->
×
800
          [%log' debug t.logger]
×
801
            "Dropping expired user command from the pool $cmd"
802
            ~metadata:
803
              [ ( "cmd"
804
                , Transaction_hash.User_command_with_valid_signature.to_yojson
×
805
                    cmd )
806
              ] ;
807
          vk_table_lift_hashed vk_table_dec cmd ;
×
808
          ignore
×
809
            ( Hashtbl.find_and_remove t.locally_generated_uncommitted cmd
×
810
              : (Time.t * [ `Batch of int ]) option ) ) ;
811
      Mina_metrics.(
×
812
        Gauge.set Transaction_pool.pool_size
×
813
          (Float.of_int (Indexed_pool.size pool))) ;
×
814
      t.pool <- pool
815

816
    let create ~constraint_constants ~consensus_constants ~time_controller
817
        ~frontier_broadcast_pipe ~config ~logger ~tf_diff_writer =
818
      let t =
×
819
        { pool =
820
            Indexed_pool.empty ~constraint_constants ~consensus_constants
821
              ~time_controller ~slot_tx_end:config.Config.slot_tx_end
822
        ; locally_generated_uncommitted =
823
            Hashtbl.create
×
824
              ( module Transaction_hash.User_command_with_valid_signature.Stable
825
                       .Latest )
826
        ; locally_generated_committed =
827
            Hashtbl.create
×
828
              ( module Transaction_hash.User_command_with_valid_signature.Stable
829
                       .Latest )
830
        ; current_batch = 0
831
        ; remaining_in_batch = max_per_15_seconds
832
        ; config
833
        ; logger
834
        ; batcher = Batcher.create config.verifier
×
835
        ; best_tip_diff_relay = None
836
        ; best_tip_ledger = None
837
        ; verification_key_table = Vk_refcount_table.create ()
×
838
        }
839
      in
840
      don't_wait_for
841
        (Broadcast_pipe.Reader.iter frontier_broadcast_pipe
×
842
           ~f:(fun frontier_opt ->
843
             match frontier_opt with
×
844
             | None -> (
×
845
                 [%log debug] "no frontier" ;
×
846
                 t.best_tip_ledger <- None ;
×
847
                 (* Sanity check: the view pipe should have been closed before
848
                    the frontier was destroyed. *)
849
                 match t.best_tip_diff_relay with
850
                 | None ->
×
851
                     Deferred.unit
852
                 | Some hdl ->
×
853
                     let is_finished = ref false in
854
                     Deferred.any_unit
855
                       [ (let%map () = hdl in
856
                          t.best_tip_diff_relay <- None ;
×
857
                          is_finished := true )
858
                       ; (let%map () = Async.after (Time.Span.of_sec 5.) in
×
859
                          if not !is_finished then (
×
860
                            [%log fatal]
×
861
                              "Transition frontier closed without first \
862
                               closing best tip view pipe" ;
863
                            assert false )
×
864
                          else () )
×
865
                       ] )
866
             | Some frontier ->
×
867
                 [%log debug] "Got frontier!" ;
×
868
                 let validation_ledger = get_best_tip_ledger frontier in
×
869
                 (* update our cache *)
870
                 t.best_tip_ledger <- Some validation_ledger ;
×
871
                 (* The frontier has changed, so transactions in the pool may
872
                    not be valid against the current best tip. *)
873
                 let new_pool, dropped =
874
                   Indexed_pool.revalidate t.pool ~logger:t.logger `Entire_pool
875
                     (fun sender ->
876
                       match
×
877
                         Base_ledger.location_of_account validation_ledger
878
                           sender
879
                       with
880
                       | None ->
×
881
                           Account.empty
882
                       | Some loc ->
×
883
                           Option.value_exn
884
                             ~message:
885
                               "Somehow a public key has a location but no \
886
                                account"
887
                             (Base_ledger.get validation_ledger loc) )
×
888
                 in
889
                 let dropped_locally_generated =
×
890
                   Sequence.filter dropped ~f:(fun cmd ->
891
                       let find_remove_bool tbl =
×
892
                         Hashtbl.find_and_remove tbl cmd |> Option.is_some
×
893
                       in
894
                       let dropped_committed =
895
                         find_remove_bool t.locally_generated_committed
896
                       in
897
                       let dropped_uncommitted =
×
898
                         find_remove_bool t.locally_generated_uncommitted
899
                       in
900
                       (* Nothing should be in both tables. *)
901
                       assert (not (dropped_committed && dropped_uncommitted)) ;
×
902
                       dropped_committed || dropped_uncommitted )
×
903
                 in
904
                 (* In this situation we don't know whether the commands aren't
905
                    valid against the new ledger because they were already
906
                    committed or because they conflict with others,
907
                    unfortunately. *)
908
                 if not (Sequence.is_empty dropped_locally_generated) then
×
909
                   [%log info]
×
910
                     "Dropped locally generated commands $cmds from pool when \
911
                      transition frontier was recreated."
912
                     ~metadata:
913
                       [ ( "cmds"
914
                         , `List
915
                             (List.map
×
916
                                (Sequence.to_list dropped_locally_generated)
×
917
                                ~f:
918
                                  Transaction_hash
919
                                  .User_command_with_valid_signature
920
                                  .to_yojson ) )
921
                       ] ;
922
                 [%log debug]
×
923
                   !"Re-validated transaction pool after restart: dropped %i \
924
                     of %i previously in pool"
925
                   (Sequence.length dropped) (Indexed_pool.size t.pool) ;
×
926
                 Mina_metrics.(
×
927
                   Gauge.set Transaction_pool.pool_size
×
928
                     (Float.of_int (Indexed_pool.size new_pool))) ;
×
929
                 t.pool <- new_pool ;
930
                 t.best_tip_diff_relay <-
931
                   Some
932
                     (Broadcast_pipe.Reader.iter
×
933
                        (Transition_frontier.best_tip_diff_pipe frontier)
×
934
                        ~f:(fun diff ->
935
                          Strict_pipe.Writer.write tf_diff_writer
×
936
                            (diff, get_best_tip_ledger frontier)
×
937
                          |> Deferred.don't_wait_for ;
938
                          Deferred.unit ) ) ;
×
939
                 Deferred.unit ) ) ;
940
      t
×
941

942
    type pool = t
943

944
    module Diff = struct
945
      type t = User_command.t list [@@deriving sexp, yojson]
×
946

947
      let (_ : (t, Diff_versioned.t) Type_equal.t) = Type_equal.T
948

949
      let label = label
950

951
      module Diff_error = struct
952
        type t = Diff_versioned.Diff_error.t =
×
953
          (*Indexed_pool*)
954
          | Insufficient_replace_fee
×
955
          (*apply*)
956
          | Duplicate
×
957
          (*Indexed_pool*)
958
          | Invalid_nonce
×
959
          (*Indexed_pool*)
960
          | Insufficient_funds
×
961
          (*Indexed_pool*)
962
          | Overflow
×
963
          (*Indexed_pool*)
964
          | Bad_token
×
965
          (*Indexed_pool*)
966
          | Unwanted_fee_token
×
967
          (*Indexed_pool*)
968
          | Expired
×
969
          (*Sink*)
970
          | Overloaded
×
971
          (*apply*)
972
          | Fee_payer_account_not_found
×
973
          | Fee_payer_not_permitted_to_send
×
974
          (*Indexed_pool*)
975
          | After_slot_tx_end
×
976
        [@@deriving sexp, yojson, compare]
977

978
        let to_string_hum = Diff_versioned.Diff_error.to_string_hum
979

980
        let grounds_for_diff_rejection = function
981
          | Expired
×
982
          | Invalid_nonce
×
983
          | Insufficient_funds
×
984
          | Insufficient_replace_fee
×
985
          | Duplicate
×
986
          | Overloaded
×
987
          | Fee_payer_account_not_found
×
988
          | Fee_payer_not_permitted_to_send
×
989
          | After_slot_tx_end ->
×
990
              false
991
          | Overflow | Bad_token | Unwanted_fee_token ->
×
992
              true
993
      end
994

995
      module Rejected = struct
996
        type t = (User_command.t * Diff_error.t) list
×
997
        [@@deriving sexp, yojson, compare]
998

999
        let (_ : (t, Diff_versioned.Rejected.t) Type_equal.t) = Type_equal.T
1000
      end
1001

1002
      type rejected = Rejected.t [@@deriving sexp, yojson, compare]
×
1003

1004
      type verified = Diff_versioned.verified [@@deriving sexp, to_yojson]
×
1005

1006
      let reject_overloaded_diff (diff : verified) : rejected =
1007
        List.map diff ~f:(fun cmd ->
×
1008
            ( Transaction_hash.User_command_with_valid_signature.command cmd
×
1009
            , Diff_error.Overloaded ) )
1010

1011
      let empty = []
1012

1013
      let size = List.length
1014

1015
      let score x = Int.max 1 (List.length x)
×
1016

1017
      let max_per_15_seconds = max_per_15_seconds
1018

1019
      let summary t =
1020
        Printf.sprintf
×
1021
          !"Transaction_pool_diff of length %d with fee payer summary %s"
1022
          (List.length t)
×
1023
          ( String.concat ~sep:","
×
1024
          @@ List.map ~f:User_command.fee_payer_summary_string t )
×
1025

1026
      let is_empty t = List.is_empty t
×
1027

1028
      let log_and_punish ?(punish = true) t d e =
×
1029
        let sender = Envelope.Incoming.sender d in
×
1030
        let trust_record =
×
1031
          Trust_system.record_envelope_sender t.config.trust_system t.logger
1032
            sender
1033
        in
1034
        let is_local = Envelope.Sender.(equal Local sender) in
×
1035
        let metadata =
1036
          [ ("error", Error_json.error_to_yojson e)
×
1037
          ; ("sender", Envelope.Sender.to_yojson sender)
×
1038
          ]
1039
        in
1040
        [%log' error t.logger] ~metadata
×
1041
          "Error verifying transaction pool diff from $sender: $error" ;
1042
        if punish && not is_local then
×
1043
          (* TODO: Make this error more specific (could also be a bad signature). *)
1044
          trust_record
×
1045
            ( Trust_system.Actions.Sent_invalid_proof
1046
            , Some ("Error verifying transaction pool diff: $error", metadata)
1047
            )
1048
        else Deferred.return ()
×
1049

1050
      let of_indexed_pool_error e =
1051
        (diff_error_of_indexed_pool_error e, indexed_pool_error_metadata e)
×
1052

1053
      let report_command_error ~logger ~is_sender_local tx (e : Command_error.t)
1054
          =
1055
        let diff_err, error_extra = of_indexed_pool_error e in
×
1056
        if is_sender_local then
×
1057
          [%str_log error]
×
1058
            (Rejecting_command_for_reason
1059
               { command = tx; reason = diff_err; error_extra } ) ;
1060
        let log = if is_sender_local then [%log error] else [%log debug] in
×
1061
        match e with
1062
        | Insufficient_replace_fee (`Replace_fee rfee, fee) ->
×
1063
            log
1064
              "rejecting $cmd because of insufficient replace fee ($rfee > \
1065
               $fee)"
1066
              ~metadata:
1067
                [ ("cmd", User_command.to_yojson tx)
×
1068
                ; ("rfee", Currency.Fee.to_yojson rfee)
×
1069
                ; ("fee", Currency.Fee.to_yojson fee)
×
1070
                ]
1071
        | Unwanted_fee_token fee_token ->
×
1072
            log "rejecting $cmd because we don't accept fees in $token"
1073
              ~metadata:
1074
                [ ("cmd", User_command.to_yojson tx)
×
1075
                ; ("token", Token_id.to_yojson fee_token)
×
1076
                ]
1077
        | _ ->
×
1078
            ()
1079

1080
      (** DO NOT mutate any transaction pool state in this function, you may only mutate in the synchronous `apply` function. *)
1081
      let verify (t : pool) (diff : t Envelope.Incoming.t) :
1082
          ( verified Envelope.Incoming.t
1083
          , Intf.Verification_error.t )
1084
          Deferred.Result.t =
1085
        let open Deferred.Result.Let_syntax in
×
1086
        let open Intf.Verification_error in
1087
        let%bind () =
1088
          let well_formedness_errors =
1089
            List.fold (Envelope.Incoming.data diff) ~init:[]
×
1090
              ~f:(fun acc user_cmd ->
1091
                match
×
1092
                  User_command.check_well_formedness
1093
                    ~genesis_constants:t.config.genesis_constants user_cmd
1094
                with
1095
                | Ok () ->
×
1096
                    acc
1097
                | Error errs ->
×
1098
                    [%log' debug t.logger]
×
1099
                      "User command $cmd from $sender has one or more \
1100
                       well-formedness errors."
1101
                      ~metadata:
1102
                        [ ("cmd", User_command.to_yojson user_cmd)
×
1103
                        ; ( "sender"
1104
                          , Envelope.(Sender.to_yojson (Incoming.sender diff))
×
1105
                          )
1106
                        ; ( "errors"
1107
                          , `List
1108
                              (List.map errs
×
1109
                                 ~f:User_command.Well_formedness_error.to_yojson )
1110
                          )
1111
                        ] ;
1112
                    errs @ acc )
×
1113
          in
1114
          match
×
1115
            List.dedup_and_sort well_formedness_errors
1116
              ~compare:User_command.Well_formedness_error.compare
1117
          with
1118
          | [] ->
×
1119
              return ()
×
1120
          | errs ->
×
1121
              let err_str =
1122
                List.map errs ~f:User_command.Well_formedness_error.to_string
×
1123
                |> String.concat ~sep:","
1124
              in
1125
              Deferred.Result.fail
×
1126
              @@ Invalid
1127
                   (Error.createf
×
1128
                      "Some commands have one or more well-formedness errors: \
1129
                       %s "
1130
                      err_str )
1131
        in
1132
        let%bind ledger =
1133
          match t.best_tip_ledger with
1134
          | Some ledger ->
×
1135
              return ledger
×
1136
          | None ->
×
1137
              Deferred.Result.fail
×
1138
              @@ Failure
1139
                   (Error.of_string
×
1140
                      "We don't have a transition frontier at the moment, so \
1141
                       we're unable to verify any transactions." )
1142
        in
1143

1144
        let%bind diff' =
1145
          O1trace.sync_thread "convert_transactions_to_verifiable" (fun () ->
×
1146
              Envelope.Incoming.map diff ~f:(fun diff ->
×
1147
                  User_command.Unapplied_sequence.to_all_verifiable diff
×
1148
                    ~load_vk_cache:(fun account_ids ->
1149
                      let account_ids = Set.to_list account_ids in
×
1150
                      let ledger_vks =
×
1151
                        Zkapp_command.Verifiable.load_vks_from_ledger
1152
                          ~location_of_account_batch:
1153
                            (Base_ledger.location_of_account_batch ledger)
×
1154
                          ~get_batch:(Base_ledger.get_batch ledger)
×
1155
                          account_ids
1156
                      in
1157
                      let ledger_vks =
×
1158
                        Map.map ledger_vks ~f:(fun vk ->
1159
                            Zkapp_basic.F_map.Map.singleton vk.hash vk )
×
1160
                      in
1161
                      let mempool_vks =
×
1162
                        List.map account_ids ~f:(fun account_id ->
×
1163
                            let vks =
×
1164
                              Vk_refcount_table.find_vks_by_account_id
1165
                                t.verification_key_table account_id
1166
                            in
1167
                            let vks =
×
1168
                              vks
1169
                              |> List.map ~f:(fun vk -> (vk.hash, vk))
×
1170
                              |> Zkapp_basic.F_map.Map.of_alist_exn
1171
                            in
1172
                            (account_id, vks) )
×
1173
                        |> Account_id.Map.of_alist_exn
1174
                      in
1175
                      Map.merge_skewed ledger_vks mempool_vks
×
1176
                        ~combine:(fun ~key:_ ->
1177
                          Map.merge_skewed ~combine:(fun ~key:_ _ x -> x) ) ) ) )
×
1178
          |> Envelope.Incoming.lift_error
×
1179
          |> Result.map_error ~f:(fun e -> Invalid e)
×
1180
          |> Deferred.return
×
1181
        in
1182
        match%bind.Deferred
1183
          O1trace.thread "batching_transaction_verification" (fun () ->
×
1184
              Batcher.verify t.batcher diff' )
×
1185
        with
1186
        | Error e ->
×
1187
            [%log' error t.logger] "Transaction verification error: $error"
×
1188
              ~metadata:[ ("error", `String (Error.to_string_hum e)) ] ;
×
1189
            [%log' debug t.logger]
×
1190
              "Failed to batch verify $transaction_pool_diff"
1191
              ~metadata:
1192
                [ ( "transaction_pool_diff"
1193
                  , Diff_versioned.to_yojson (Envelope.Incoming.data diff) )
×
1194
                ] ;
1195
            Deferred.Result.fail (Failure e)
×
1196
        | Ok (Error invalid) ->
×
1197
            let err = Verifier.invalid_to_error invalid in
1198
            [%log' error t.logger]
×
1199
              "Batch verification failed when adding from gossip"
1200
              ~metadata:[ ("error", Error_json.error_to_yojson err) ] ;
×
1201
            let%map.Deferred () =
1202
              Trust_system.record_envelope_sender t.config.trust_system t.logger
×
1203
                (Envelope.Incoming.sender diff)
×
1204
                ( Trust_system.Actions.Sent_useless_gossip
1205
                , Some
1206
                    ( "rejecting command because had invalid signature or proof"
1207
                    , [] ) )
1208
            in
1209
            Error (Invalid err)
×
1210
        | Ok (Ok commands) ->
×
1211
            (* TODO: avoid duplicate hashing (#11706) *)
1212
            O1trace.sync_thread "hashing_transactions_after_verification"
1213
              (fun () ->
1214
                return
×
1215
                  { diff with
1216
                    data =
1217
                      List.map commands
×
1218
                        ~f:
1219
                          Transaction_hash.User_command_with_valid_signature
1220
                          .create
1221
                  } )
1222

1223
      let register_locally_generated t txn =
1224
        Hashtbl.update t.locally_generated_uncommitted txn ~f:(function
×
1225
          | Some (_, `Batch batch_num) ->
×
1226
              (* Use the existing [batch_num] on a re-issue, to avoid splitting
1227
                 existing batches.
1228
              *)
1229
              (Time.now (), `Batch batch_num)
×
1230
          | None ->
×
1231
              let batch_num =
1232
                if t.remaining_in_batch > 0 then (
×
1233
                  t.remaining_in_batch <- t.remaining_in_batch - 1 ;
1234
                  t.current_batch )
1235
                else (
×
1236
                  t.remaining_in_batch <- max_per_15_seconds - 1 ;
1237
                  t.current_batch <- t.current_batch + 1 ;
1238
                  t.current_batch )
1239
              in
1240
              (Time.now (), `Batch batch_num) )
×
1241

1242
      (* This must be synchronous, but you MAY modify state here (do not modify pool state in `verify` *)
1243
      let apply t (diff : verified Envelope.Incoming.t) =
1244
        let open Or_error.Let_syntax in
×
1245
        let is_sender_local =
1246
          Envelope.Sender.(equal Local) (Envelope.Incoming.sender diff)
×
1247
        in
1248
        let pool_size_before = Indexed_pool.size t.pool in
×
1249
        (* preload fee payer accounts from the best tip ledger *)
1250
        let%map ledger =
1251
          match t.best_tip_ledger with
1252
          | None ->
×
1253
              Or_error.error_string
×
1254
                "Got transaction pool diff when transitin frontier is \
1255
                 unavailable, ignoring."
1256
          | Some ledger ->
×
1257
              return ledger
×
1258
        in
1259
        let fee_payer_account_ids =
×
1260
          List.map (Envelope.Incoming.data diff) ~f:(fun cmd ->
×
1261
              Transaction_hash.User_command_with_valid_signature.command cmd
×
1262
              |> User_command.fee_payer )
1263
          |> Account_id.Set.of_list
1264
        in
1265
        let fee_payer_accounts =
×
1266
          preload_accounts ledger fee_payer_account_ids
1267
        in
1268
        (* add new commands to the pool *)
1269
        let fee_payer =
×
1270
          Fn.compose User_command.fee_payer
1271
            Transaction_hash.User_command_with_valid_signature.command
1272
        in
1273
        let check_command pool cmd =
×
1274
          let already_in_pool =
×
1275
            Indexed_pool.member pool
1276
              (Transaction_hash.User_command.of_checked cmd)
×
1277
          in
1278
          let%map.Result () =
1279
            if already_in_pool then
1280
              if is_sender_local then Ok () else Error Diff_error.Duplicate
×
1281
            else
1282
              match Map.find fee_payer_accounts (fee_payer cmd) with
×
1283
              | None ->
×
1284
                  Error Diff_error.Fee_payer_account_not_found
1285
              | Some account ->
×
1286
                  Result.ok_if_true
×
1287
                    ( Account.has_permission_to_send account
×
1288
                    && Account.has_permission_to_increment_nonce account )
×
1289
                    ~error:Diff_error.Fee_payer_not_permitted_to_send
1290
          in
1291
          already_in_pool
×
1292
        in
1293
        (* Dedicated variant to track whether the transaction was already in
1294
           the pool. We use this to signal that the user wants to re-broadcast
1295
           a txn that already exists in their local pool.
1296
        *)
1297
        let module Command_state = struct
1298
          type t = New_command | Rebroadcast
1299
        end in
1300
        let pool, add_results =
1301
          List.fold_map (Envelope.Incoming.data diff) ~init:t.pool
×
1302
            ~f:(fun pool cmd ->
1303
              let result =
×
1304
                let%bind.Result already_in_pool = check_command pool cmd in
×
1305
                let global_slot =
×
1306
                  Indexed_pool.global_slot_since_genesis t.pool
1307
                in
1308
                let account = Map.find_exn fee_payer_accounts (fee_payer cmd) in
×
1309
                if already_in_pool then
×
1310
                  Ok ((cmd, pool, Sequence.empty), Command_state.Rebroadcast)
×
1311
                else
1312
                  match
×
1313
                    Indexed_pool.add_from_gossip_exn pool cmd account.nonce
1314
                      ( Account.liquid_balance_at_slot ~global_slot account
×
1315
                      |> Currency.Balance.to_amount )
×
1316
                  with
1317
                  | Ok x ->
×
1318
                      Ok (x, Command_state.New_command)
1319
                  | Error err ->
×
1320
                      report_command_error ~logger:t.logger ~is_sender_local
1321
                        (Transaction_hash.User_command_with_valid_signature
1322
                         .command cmd )
×
1323
                        err ;
1324
                      Error (diff_error_of_indexed_pool_error err)
×
1325
              in
1326
              match result with
×
1327
              | Ok ((cmd', pool', dropped), cmd_state) ->
×
1328
                  (pool', Ok (cmd', dropped, cmd_state))
1329
              | Error err ->
×
1330
                  (pool, Error (cmd, err)) )
1331
        in
1332
        let added_cmds =
×
1333
          List.filter_map add_results ~f:(function
1334
            | Ok (cmd, _, Command_state.New_command) ->
×
1335
                Some cmd
1336
            | Ok (_, _, Command_state.Rebroadcast) | Error _ ->
×
1337
                None )
1338
        in
1339
        let dropped_for_add =
×
1340
          List.filter_map add_results ~f:(function
×
1341
            | Ok (_, dropped, Command_state.New_command) ->
×
1342
                Some (Sequence.to_list dropped)
×
1343
            | Ok (_, _, Command_state.Rebroadcast) | Error _ ->
×
1344
                None )
1345
          |> List.concat
1346
        in
1347
        (* drop commands from the pool to retain max size *)
1348
        let pool, dropped_for_size =
×
1349
          let pool, dropped =
1350
            drop_until_below_max_size pool ~pool_max_size:t.config.pool_max_size
1351
          in
1352
          (pool, Sequence.to_list dropped)
×
1353
        in
1354
        (* handle drops of locally generated commands *)
1355
        let all_dropped_cmds = dropped_for_add @ dropped_for_size in
1356

1357
        (* apply changes to the vk-refcount-table here *)
1358
        let () =
1359
          let lift = Vk_refcount_table.lift_hashed t.verification_key_table in
1360
          List.iter added_cmds ~f:(lift Vk_refcount_table.inc) ;
×
1361
          List.iter all_dropped_cmds
×
1362
            ~f:
1363
              (lift (fun t ~account_id ~vk ->
×
1364
                   Vk_refcount_table.dec t ~account_id ~vk_hash:vk.hash ) )
×
1365
        in
1366
        let dropped_for_add_hashes =
1367
          List.map dropped_for_add
×
1368
            ~f:Transaction_hash.User_command_with_valid_signature.hash
1369
          |> Transaction_hash.Set.of_list
1370
        in
1371
        let dropped_for_size_hashes =
×
1372
          List.map dropped_for_size
×
1373
            ~f:Transaction_hash.User_command_with_valid_signature.hash
1374
          |> Transaction_hash.Set.of_list
1375
        in
1376
        let all_dropped_cmd_hashes =
×
1377
          Transaction_hash.Set.union dropped_for_add_hashes
1378
            dropped_for_size_hashes
1379
        in
1380
        [%log' debug t.logger]
×
1381
          "Dropping $num_for_add commands from pool while adding new commands, \
1382
           and $num_for_size commands due to pool size"
1383
          ~metadata:
1384
            [ ("num_for_add", `Int (List.length dropped_for_add))
×
1385
            ; ("num_for_size", `Int (List.length dropped_for_size))
×
1386
            ] ;
1387
        let locally_generated_dropped =
×
1388
          List.filter all_dropped_cmds ~f:(fun cmd ->
1389
              Hashtbl.find_and_remove t.locally_generated_uncommitted cmd
×
1390
              |> Option.is_some )
1391
        in
1392
        if not (List.is_empty locally_generated_dropped) then
×
1393
          [%log' info t.logger]
×
1394
            "Dropped locally generated commands $cmds from transaction pool \
1395
             due to replacement or max size"
1396
            ~metadata:
1397
              [ ( "cmds"
1398
                , `List
1399
                    (List.map
×
1400
                       ~f:
1401
                         Transaction_hash.User_command_with_valid_signature
1402
                         .to_yojson locally_generated_dropped ) )
1403
              ] ;
1404
        (* register locally generated commands *)
1405
        if is_sender_local then
×
1406
          List.iter add_results ~f:(function
×
1407
            | Ok (cmd, _dropped, _command_type) ->
×
1408
                if
1409
                  not
1410
                    (Set.mem all_dropped_cmd_hashes
×
1411
                       (Transaction_hash.User_command_with_valid_signature.hash
×
1412
                          cmd ) )
1413
                then register_locally_generated t cmd
×
1414
            | Error _ ->
×
1415
                () ) ;
1416
        (* finalize the update to the pool *)
1417
        t.pool <- pool ;
×
1418
        let pool_size_after = Indexed_pool.size pool in
1419
        Mina_metrics.(
×
1420
          Gauge.set Transaction_pool.pool_size (Float.of_int pool_size_after) ;
×
1421
          List.iter
×
1422
            (List.init (max 0 (pool_size_after - pool_size_before)) ~f:Fn.id)
×
1423
            ~f:(fun _ ->
1424
              Counter.inc_one Transaction_pool.transactions_added_to_pool )) ;
×
1425
        (* partition the results *)
1426
        let accepted, rejected, _dropped =
1427
          List.partition3_map add_results ~f:(function
1428
            | Ok (cmd, _dropped, _cmd_state) ->
×
1429
                (* NB: We ignore the command state here, so that commands only
1430
                   for rebroadcast are still included in the bundle that we
1431
                   rebroadcast.
1432
                *)
1433
                if
1434
                  Set.mem all_dropped_cmd_hashes
1435
                    (Transaction_hash.User_command_with_valid_signature.hash cmd)
×
1436
                then `Trd cmd
×
1437
                else `Fst cmd
×
1438
            | Error (cmd, error) ->
×
1439
                `Snd (cmd, error) )
1440
        in
1441
        (* determine if we should re-broadcast this diff *)
1442
        let decision =
×
1443
          if
1444
            List.exists rejected ~f:(fun (_, error) ->
1445
                Diff_error.grounds_for_diff_rejection error )
×
1446
          then `Reject
×
1447
          else `Accept
×
1448
        in
1449
        (decision, accepted, rejected)
1450

1451
      let unsafe_apply (t : pool) (diff : verified Envelope.Incoming.t) :
1452
          ([ `Accept | `Reject ] * t * rejected, _) Result.t =
1453
        match apply t diff with
×
1454
        | Ok (decision, accepted, rejected) ->
×
1455
            ( if not (List.is_empty accepted) then
×
1456
              Mina_metrics.(
×
1457
                Gauge.set Transaction_pool.useful_transactions_received_time_sec
×
1458
                  (let x =
1459
                     Time.(now () |> to_span_since_epoch |> Span.to_sec)
×
1460
                   in
1461
                   x -. Mina_metrics.time_offset_sec )) ) ;
1462
            let forget_cmd =
×
1463
              Transaction_hash.User_command_with_valid_signature.command
1464
            in
1465
            Ok
1466
              ( decision
1467
              , List.map ~f:forget_cmd accepted
×
1468
              , List.map ~f:(Tuple2.map_fst ~f:forget_cmd) rejected )
×
1469
        | Error e ->
×
1470
            Error (`Other e)
1471

1472
      type Structured_log_events.t +=
1473
        | Transactions_received of
1474
            { fee_payer_summaries : User_command.fee_payer_summary_t list
×
1475
            ; sender : Envelope.Sender.t
×
1476
            }
1477
        [@@deriving
1478
          register_event
×
1479
            { msg =
1480
                "Received transaction-pool $fee_payer_summaries from $sender"
1481
            }]
1482

1483
      let update_metrics ~logger ~log_gossip_heard envelope valid_cb =
1484
        Mina_metrics.(Counter.inc_one Network.gossip_messages_received) ;
×
1485
        Mina_metrics.(Gauge.inc_one Network.transaction_pool_diff_received) ;
×
1486
        let diff = Envelope.Incoming.data envelope in
1487
        if log_gossip_heard then (
×
1488
          let fee_payer_summaries =
1489
            List.map ~f:User_command.fee_payer_summary diff
1490
          in
1491
          [%str_log debug]
×
1492
            (Transactions_received
1493
               { fee_payer_summaries
1494
               ; sender = Envelope.Incoming.sender envelope
×
1495
               } ) ;
1496
          Mina_net2.Validation_callback.set_message_type valid_cb `Transaction ;
×
1497
          Mina_metrics.(Counter.inc_one Network.Transaction.received) )
×
1498

1499
      let log_internal ?reason ~logger msg
1500
          { Envelope.Incoming.data = diff; sender; _ } =
1501
        let metadata =
×
1502
          [ ( "diff"
1503
            , `List
1504
                (List.map diff
×
1505
                   ~f:Mina_transaction.Transaction.yojson_summary_of_command )
1506
            )
1507
          ]
1508
        in
1509
        let metadata =
1510
          match sender with
1511
          | Remote addr ->
×
1512
              ("sender", `String (Core.Unix.Inet_addr.to_string @@ Peer.ip addr))
×
1513
              :: metadata
1514
          | Local ->
×
1515
              metadata
1516
        in
1517
        let metadata =
1518
          Option.value_map reason
1519
            ~f:(fun r -> List.cons ("reason", `String r))
×
1520
            ~default:ident metadata
1521
        in
1522
        if not (is_empty diff) then
×
1523
          [%log internal] "%s" ("Transaction_diff_" ^ msg) ~metadata
×
1524

1525
      let t_of_verified =
1526
        List.map ~f:Transaction_hash.User_command_with_valid_signature.command
1527
    end
1528

1529
    let get_rebroadcastable (t : t) ~has_timed_out =
1530
      let metadata ~key ~time =
×
1531
        [ ( "cmd"
×
1532
          , Transaction_hash.User_command_with_valid_signature.to_yojson key )
×
1533
        ; ("time", `String (Time.to_string_abs ~zone:Time.Zone.utc time))
×
1534
        ]
1535
      in
1536
      let added_str =
1537
        "it was added at $time and its rebroadcast period is now expired."
1538
      in
1539
      let logger = t.logger in
1540
      Hashtbl.filteri_inplace t.locally_generated_uncommitted
1541
        ~f:(fun ~key ~data:(time, `Batch _) ->
1542
          match has_timed_out time with
×
1543
          | `Timed_out ->
×
1544
              [%log info]
×
1545
                "No longer rebroadcasting uncommitted command $cmd, %s"
1546
                added_str ~metadata:(metadata ~key ~time) ;
1547
              false
×
1548
          | `Ok ->
×
1549
              true ) ;
1550
      Hashtbl.filteri_inplace t.locally_generated_committed
×
1551
        ~f:(fun ~key ~data:(time, `Batch _) ->
1552
          match has_timed_out time with
×
1553
          | `Timed_out ->
×
1554
              [%log debug]
×
1555
                "Removing committed locally generated command $cmd from \
1556
                 possible rebroadcast pool, %s"
1557
                added_str ~metadata:(metadata ~key ~time) ;
1558
              false
×
1559
          | `Ok ->
×
1560
              true ) ;
1561
      (* Important to maintain ordering here *)
1562
      let rebroadcastable_txs =
×
1563
        Hashtbl.to_alist t.locally_generated_uncommitted
×
1564
        |> List.sort
×
1565
             ~compare:(fun (txn1, (_, `Batch batch1)) (txn2, (_, `Batch batch2))
1566
                      ->
1567
               let cmp = compare batch1 batch2 in
×
1568
               let get_hash =
×
1569
                 Transaction_hash.User_command_with_valid_signature.hash
1570
               in
1571
               let get_nonce txn =
1572
                 Transaction_hash.User_command_with_valid_signature.command txn
×
1573
                 |> User_command.applicable_at_nonce
1574
               in
1575
               if cmp <> 0 then cmp
×
1576
               else
1577
                 let cmp =
×
1578
                   Mina_numbers.Account_nonce.compare (get_nonce txn1)
×
1579
                     (get_nonce txn2)
×
1580
                 in
1581
                 if cmp <> 0 then cmp
×
1582
                 else Transaction_hash.compare (get_hash txn1) (get_hash txn2) )
×
1583
        |> List.group
×
1584
             ~break:(fun (_, (_, `Batch batch1)) (_, (_, `Batch batch2)) ->
1585
               batch1 <> batch2 )
×
1586
        |> List.map
1587
             ~f:
1588
               (List.map ~f:(fun (txn, _) ->
1589
                    Transaction_hash.User_command_with_valid_signature.command
×
1590
                      txn ) )
1591
      in
1592
      rebroadcastable_txs
×
1593
  end
1594

1595
  include Network_pool_base.Make (Transition_frontier) (Resource_pool)
1596
end
1597

1598
(* Use this one in downstream consumers *)
1599
module Make (Staged_ledger : sig
1600
  type t
1601

1602
  val ledger : t -> Mina_ledger.Ledger.t
1603
end)
1604
(Transition_frontier : Transition_frontier_intf
1605
                         with type staged_ledger := Staged_ledger.t) :
1606
  S with type transition_frontier := Transition_frontier.t =
1607
  Make0 (Mina_ledger.Ledger) (Staged_ledger) (Transition_frontier)
1608

1609
(* TODO: defunctor or remove monkey patching (#3731) *)
1610
include
1611
  Make
1612
    (Staged_ledger)
1613
    (struct
1614
      include Transition_frontier
1615

1616
      type best_tip_diff = Extensions.Best_tip_diff.view =
1617
        { new_commands : User_command.Valid.t With_status.t list
1618
        ; removed_commands : User_command.Valid.t With_status.t list
1619
        ; reorg_best_tip : bool
1620
        }
1621

1622
      let best_tip_diff_pipe t =
1623
        Extensions.(get_view_pipe (extensions t) Best_tip_diff)
×
1624
    end)
1625

1626
let%test_module _ =
1627
  ( module struct
1628
    open Signature_lib
1629
    module Mock_base_ledger = Mocks.Base_ledger
1630
    module Mock_staged_ledger = Mocks.Staged_ledger
1631

1632
    let () =
1633
      Core.Backtrace.elide := false ;
1634
      Async.Scheduler.set_record_backtraces true
×
1635

1636
    let num_test_keys = 10
1637

1638
    (* keys for accounts in the ledger *)
1639
    let test_keys =
1640
      Array.init num_test_keys ~f:(fun _ -> Signature_lib.Keypair.create ())
×
1641

1642
    let num_extra_keys = 30
1643

1644
    let block_window_duration =
1645
      Mina_compile_config.For_unit_tests.t.block_window_duration
1646

1647
    (* keys that can be used when generating new accounts *)
1648
    let extra_keys =
1649
      Array.init num_extra_keys ~f:(fun _ -> Signature_lib.Keypair.create ())
×
1650

1651
    let precomputed_values = Lazy.force Precomputed_values.for_unit_tests
×
1652

1653
    let constraint_constants = precomputed_values.constraint_constants
1654

1655
    let consensus_constants = precomputed_values.consensus_constants
1656

1657
    let proof_level = precomputed_values.proof_level
1658

1659
    let genesis_constants = precomputed_values.genesis_constants
1660

1661
    let minimum_fee =
1662
      Currency.Fee.to_nanomina_int genesis_constants.minimum_user_command_fee
×
1663

1664
    let logger = Logger.create ()
×
1665

1666
    let time_controller = Block_time.Controller.basic ~logger
1667

1668
    let verifier =
1669
      Async.Thread_safe.block_on_async_exn (fun () ->
×
1670
          Verifier.create ~logger ~proof_level ~constraint_constants
×
1671
            ~conf_dir:None
1672
            ~pids:(Child_processes.Termination.create_pid_table ())
×
1673
            ~commit_id:"not specified for unit tests" () )
1674

1675
    let `VK vk, `Prover prover =
1676
      Transaction_snark.For_tests.create_trivial_snapp ~constraint_constants ()
×
1677

1678
    let vk = Async.Thread_safe.block_on_async_exn (fun () -> vk)
×
1679

1680
    let dummy_state_view =
1681
      let state_body =
1682
        let consensus_constants =
1683
          Consensus.Constants.create ~constraint_constants
1684
            ~protocol_constants:genesis_constants.protocol
1685
        in
1686
        let compile_time_genesis =
1687
          (*not using Precomputed_values.for_unit_test because of dependency cycle*)
1688
          Mina_state.Genesis_protocol_state.t
1689
            ~genesis_ledger:Genesis_ledger.(Packed.t for_unit_tests)
×
1690
            ~genesis_epoch_data:Consensus.Genesis_epoch_data.for_unit_tests
1691
            ~constraint_constants ~consensus_constants
1692
            ~genesis_body_reference:Staged_ledger_diff.genesis_body_reference
1693
        in
1694
        compile_time_genesis.data |> Mina_state.Protocol_state.body
×
1695
      in
1696
      { (Mina_state.Protocol_state.Body.view state_body) with
×
1697
        global_slot_since_genesis = Mina_numbers.Global_slot_since_genesis.zero
1698
      }
1699

1700
    module Mock_transition_frontier = struct
1701
      module Breadcrumb = struct
1702
        type t = Mock_staged_ledger.t
1703

1704
        let staged_ledger = Fn.id
1705
      end
1706

1707
      type best_tip_diff =
1708
        { new_commands : User_command.Valid.t With_status.t list
1709
        ; removed_commands : User_command.Valid.t With_status.t list
1710
        ; reorg_best_tip : bool
1711
        }
1712

1713
      type t = best_tip_diff Broadcast_pipe.Reader.t * Breadcrumb.t ref
1714

1715
      let create ?permissions :
1716
          unit -> t * best_tip_diff Broadcast_pipe.Writer.t =
1717
       fun () ->
×
1718
        let zkappify_account (account : Account.t) : Account.t =
×
1719
          let zkapp =
×
1720
            Some { Zkapp_account.default with verification_key = Some vk }
1721
          in
1722
          { account with
1723
            zkapp
1724
          ; permissions =
1725
              ( match permissions with
1726
              | Some p ->
×
1727
                  p
1728
              | None ->
×
1729
                  Permissions.user_default )
1730
          }
1731
        in
1732
        let pipe_r, pipe_w =
1733
          Broadcast_pipe.create
1734
            { new_commands = []; removed_commands = []; reorg_best_tip = false }
1735
        in
1736
        let initial_balance =
×
1737
          Currency.Balance.of_mina_string_exn "900000000.0"
1738
        in
1739
        let ledger = Mina_ledger.Ledger.create_ephemeral ~depth:10 () in
×
1740
        Array.iteri test_keys ~f:(fun i kp ->
×
1741
            let account_id =
×
1742
              Account_id.create
1743
                (Public_key.compress kp.public_key)
×
1744
                Token_id.default
1745
            in
1746
            let _tag, account, loc =
×
1747
              Or_error.ok_exn
1748
              @@ Mina_ledger.Ledger.Ledger_inner.get_or_create ledger account_id
×
1749
            in
1750
            (* set the account balance *)
1751
            let account = { account with balance = initial_balance } in
×
1752
            (* zkappify every other account *)
1753
            let account =
1754
              if i mod 2 = 0 then account else zkappify_account account
×
1755
            in
1756
            Mina_ledger.Ledger.Ledger_inner.set ledger loc account ) ;
1757
        ((pipe_r, ref ledger), pipe_w)
×
1758

1759
      let best_tip (_, best_tip) = !best_tip
×
1760

1761
      let best_tip_diff_pipe (pipe, _) = pipe
×
1762
    end
1763

1764
    module Test =
1765
      Make0 (Mock_base_ledger) (Mock_staged_ledger) (Mock_transition_frontier)
1766

1767
    type test =
1768
      { txn_pool : Test.Resource_pool.t
1769
      ; best_tip_diff_w :
1770
          Mock_transition_frontier.best_tip_diff Broadcast_pipe.Writer.t
1771
      ; best_tip_ref : Mina_ledger.Ledger.t ref
1772
      ; frontier_pipe_w :
1773
          Mock_transition_frontier.t option Broadcast_pipe.Writer.t
1774
      }
1775

1776
    let pool_max_size = 25
1777

1778
    let assert_user_command_sets_equal cs1 cs2 =
1779
      let index cs =
×
1780
        let decompose c =
×
1781
          ( Transaction_hash.User_command.hash c
×
1782
          , Transaction_hash.User_command.command c )
×
1783
        in
1784
        List.map cs ~f:decompose |> Transaction_hash.Map.of_alist_exn
×
1785
      in
1786
      let index1 = index cs1 in
1787
      let index2 = index cs2 in
×
1788
      let set1 = Transaction_hash.Set.of_list @@ Map.keys index1 in
×
1789
      let set2 = Transaction_hash.Set.of_list @@ Map.keys index2 in
×
1790
      if not (Set.equal set1 set2) then (
×
1791
        let additional1, additional2 =
1792
          Set.symmetric_diff set1 set2
×
1793
          |> Sequence.map
×
1794
               ~f:
1795
                 (Either.map ~first:(Map.find_exn index1)
×
1796
                    ~second:(Map.find_exn index2) )
×
1797
          |> Sequence.to_list
×
1798
          |> List.partition_map ~f:Fn.id
1799
        in
1800
        assert (List.length additional1 + List.length additional2 > 0) ;
×
1801
        let report_additional commands a b =
1802
          Core.Printf.printf "%s user commands not in %s:\n" a b ;
×
1803
          List.iter commands ~f:(fun c ->
×
1804
              Core.Printf.printf !"  %{Sexp}\n" (User_command.sexp_of_t c) )
×
1805
        in
1806
        if List.length additional1 > 0 then
×
1807
          report_additional additional1 "actual" "expected" ;
×
1808
        if List.length additional2 > 0 then
×
1809
          report_additional additional2 "expected" "actual" ) ;
×
1810
      [%test_eq: Transaction_hash.Set.t] set1 set2
×
1811

1812
    let replace_valid_zkapp_command_authorizations ~keymap ~ledger valid_cmds :
1813
        User_command.Valid.t list Deferred.t =
1814
      let open Deferred.Let_syntax in
×
1815
      let%map zkapp_commands_fixed =
1816
        Deferred.List.map
×
1817
          (valid_cmds : User_command.Valid.t list)
1818
          ~f:(function
1819
            | Zkapp_command zkapp_command_dummy_auths ->
×
1820
                let%map cmd =
1821
                  Zkapp_command_builder.replace_authorizations ~keymap ~prover
×
1822
                    (Zkapp_command.Valid.forget zkapp_command_dummy_auths)
×
1823
                in
1824
                User_command.Zkapp_command cmd
×
1825
            | Signed_command _ ->
×
1826
                failwith "Expected Zkapp_command valid user command" )
1827
      in
1828
      match
×
1829
        User_command.Unapplied_sequence.to_all_verifiable zkapp_commands_fixed
×
1830
          ~load_vk_cache:(fun account_ids ->
1831
            Set.to_list account_ids
×
1832
            |> Zkapp_command.Verifiable.load_vks_from_ledger
×
1833
                 ~get_batch:(Mina_ledger.Ledger.get_batch ledger)
×
1834
                 ~location_of_account_batch:
1835
                   (Mina_ledger.Ledger.location_of_account_batch ledger)
×
1836
            |> Map.map ~f:(fun vk ->
1837
                   Zkapp_basic.F_map.Map.singleton vk.hash vk ) )
×
1838
        |> Or_error.bind ~f:(fun xs ->
1839
               List.map xs ~f:User_command.check_verifiable
×
1840
               |> Or_error.combine_errors )
1841
      with
1842
      | Ok cmds ->
×
1843
          cmds
1844
      | Error err ->
×
1845
          Error.raise
1846
          @@ Error.tag ~tag:"Could not create Zkapp_command.Valid.t" err
×
1847

1848
    (** Assert the invariants of the locally generated command tracking system. *)
1849
    let assert_locally_generated (pool : Test.Resource_pool.t) =
1850
      ignore
×
1851
        ( Hashtbl.merge pool.locally_generated_committed
×
1852
            pool.locally_generated_uncommitted ~f:(fun ~key -> function
1853
            | `Both ((committed, _), (uncommitted, _)) ->
×
1854
                failwithf
1855
                  !"Command \
×
1856
                    %{sexp:Transaction_hash.User_command_with_valid_signature.t} \
1857
                    in both locally generated committed and uncommitted with \
1858
                    times %s and %s"
1859
                  key (Time.to_string committed)
×
1860
                  (Time.to_string uncommitted)
×
1861
                  ()
1862
            | `Left cmd ->
×
1863
                Some cmd
1864
            | `Right cmd ->
×
1865
                (* Locally generated uncommitted transactions should be in the
1866
                   pool, so long as we're not in the middle of updating it. *)
1867
                assert (
×
1868
                  Indexed_pool.member pool.pool
×
1869
                    (Transaction_hash.User_command.of_checked key) ) ;
×
1870
                Some cmd )
1871
          : ( Transaction_hash.User_command_with_valid_signature.t
1872
            , Time.t * [ `Batch of int ] )
1873
            Hashtbl.t )
1874

1875
    let assert_fee_wu_ordering (pool : Test.Resource_pool.t) =
1876
      let txns = Test.Resource_pool.transactions pool |> Sequence.to_list in
×
1877
      let compare txn1 txn2 =
×
1878
        let open Transaction_hash.User_command_with_valid_signature in
×
1879
        let cmd1 = command txn1 in
1880
        let cmd2 = command txn2 in
×
1881
        (* ascending order of nonces, if same fee payer *)
1882
        if
×
1883
          Account_id.equal
1884
            (User_command.fee_payer cmd1)
×
1885
            (User_command.fee_payer cmd2)
×
1886
        then
1887
          Account.Nonce.compare
×
1888
            (User_command.applicable_at_nonce cmd1)
×
1889
            (User_command.applicable_at_nonce cmd2)
×
1890
        else
1891
          let get_fee_wu cmd = User_command.fee_per_wu cmd in
×
1892
          (* descending order of fee/weight *)
1893
          Currency.Fee_rate.compare (get_fee_wu cmd2) (get_fee_wu cmd1)
×
1894
      in
1895
      assert (List.is_sorted txns ~compare)
×
1896

1897
    let assert_pool_txs test txs =
1898
      Indexed_pool.For_tests.assert_pool_consistency test.txn_pool.pool ;
×
1899
      assert_locally_generated test.txn_pool ;
×
1900
      assert_fee_wu_ordering test.txn_pool ;
×
1901
      assert_user_command_sets_equal
×
1902
        ( Sequence.to_list
×
1903
        @@ Sequence.map ~f:Transaction_hash.User_command.of_checked
×
1904
        @@ Test.Resource_pool.transactions test.txn_pool )
×
1905
        (List.map
×
1906
           ~f:
1907
             (Fn.compose Transaction_hash.User_command.create
×
1908
                User_command.forget_check )
1909
           txs )
1910

1911
    let setup_test ?(verifier = verifier) ?permissions ?slot_tx_end () =
×
1912
      let frontier, best_tip_diff_w =
×
1913
        Mock_transition_frontier.create ?permissions ()
1914
      in
1915
      let _, best_tip_ref = frontier in
×
1916
      let frontier_pipe_r, frontier_pipe_w =
1917
        Broadcast_pipe.create @@ Some frontier
1918
      in
1919
      let trust_system = Trust_system.null () in
×
1920
      let config =
×
1921
        Test.Resource_pool.make_config ~trust_system ~pool_max_size ~verifier
1922
          ~genesis_constants ~slot_tx_end
1923
      in
1924
      let pool_, _, _ =
1925
        Test.create ~config ~logger ~constraint_constants ~consensus_constants
1926
          ~time_controller ~frontier_broadcast_pipe:frontier_pipe_r
1927
          ~log_gossip_heard:false ~on_remote_push:(Fn.const Deferred.unit)
×
1928
          ~block_window_duration
1929
      in
1930
      let txn_pool = Test.resource_pool pool_ in
1931
      let%map () = Async.Scheduler.yield_until_no_jobs_remain () in
×
1932
      { txn_pool; best_tip_diff_w; best_tip_ref; frontier_pipe_w }
×
1933

1934
    let independent_cmds : User_command.Valid.t list =
1935
      let rec go n cmds =
1936
        let open Quickcheck.Generator.Let_syntax in
×
1937
        if n < Array.length test_keys then
×
1938
          let%bind cmd =
1939
            let sender = test_keys.(n) in
1940
            User_command.Valid.Gen.payment ~sign_type:`Real
×
1941
              ~key_gen:
1942
                (Quickcheck.Generator.tuple2 (return sender)
×
1943
                   (Quickcheck_lib.of_array test_keys) )
×
1944
              ~max_amount:1_000_000_000 ~fee_range:1_000_000_000 ()
1945
          in
1946
          go (n + 1) (cmd :: cmds)
×
1947
        else Quickcheck.Generator.return @@ List.rev cmds
×
1948
      in
1949
      Quickcheck.random_value ~seed:(`Deterministic "constant") (go 0 [])
×
1950

1951
    let mk_payment' ?valid_until ~sender_idx ~receiver_idx ~fee ~nonce ~amount
1952
        () =
1953
      let get_pk idx = Public_key.compress test_keys.(idx).public_key in
×
1954
      Signed_command.sign test_keys.(sender_idx)
1955
        (Signed_command_payload.create
1956
           ~fee:(Currency.Fee.of_nanomina_int_exn fee)
×
1957
           ~fee_payer_pk:(get_pk sender_idx) ~valid_until
×
1958
           ~nonce:(Account.Nonce.of_int nonce)
×
1959
           ~memo:(Signed_command_memo.create_by_digesting_string_exn "foo")
×
1960
           ~body:
1961
             (Signed_command_payload.Body.Payment
1962
                { receiver_pk = get_pk receiver_idx
×
1963
                ; amount = Currency.Amount.of_nanomina_int_exn amount
×
1964
                } ) )
1965

1966
    let mk_single_account_update ~chain ~fee_payer_idx ~zkapp_account_idx ~fee
1967
        ~nonce ~ledger =
1968
      let fee = Currency.Fee.of_nanomina_int_exn fee in
×
1969
      let fee_payer_kp = test_keys.(fee_payer_idx) in
×
1970
      let nonce = Account.Nonce.of_int nonce in
×
1971
      let spec : Transaction_snark.For_tests.Single_account_update_spec.t =
×
1972
        Transaction_snark.For_tests.Single_account_update_spec.
1973
          { fee_payer = (fee_payer_kp, nonce)
1974
          ; fee
1975
          ; memo = Signed_command_memo.create_from_string_exn "invalid proof"
×
1976
          ; zkapp_account_keypair = test_keys.(zkapp_account_idx)
1977
          ; update = { Account_update.Update.noop with zkapp_uri = Set "abcd" }
1978
          ; call_data = Snark_params.Tick.Field.zero
1979
          ; events = []
1980
          ; actions = []
1981
          }
1982
      in
1983
      let%map zkapp_command =
1984
        Transaction_snark.For_tests.single_account_update ~chain
×
1985
          ~constraint_constants spec
1986
      in
1987
      Or_error.ok_exn
×
1988
        (Zkapp_command.Verifiable.create ~failed:false
×
1989
           ~find_vk:
1990
             (Zkapp_command.Verifiable.load_vk_from_ledger
1991
                ~get:(Mina_ledger.Ledger.get ledger)
×
1992
                ~location_of_account:
1993
                  (Mina_ledger.Ledger.location_of_account ledger) )
×
1994
           zkapp_command )
1995

1996
    let mk_transfer_zkapp_command ?valid_period ?fee_payer_idx ~sender_idx
1997
        ~receiver_idx ~fee ~nonce ~amount () =
1998
      let sender_kp = test_keys.(sender_idx) in
×
1999
      let sender_nonce = Account.Nonce.of_int nonce in
×
2000
      let sender = (sender_kp, sender_nonce) in
×
2001
      let amount = Currency.Amount.of_nanomina_int_exn amount in
2002
      let receiver_kp = test_keys.(receiver_idx) in
×
2003
      let receiver =
×
2004
        receiver_kp.public_key |> Signature_lib.Public_key.compress
2005
      in
2006
      let fee_payer =
×
2007
        match fee_payer_idx with
2008
        | None ->
×
2009
            None
2010
        | Some (idx, nonce) ->
×
2011
            let fee_payer_kp = test_keys.(idx) in
2012
            let fee_payer_nonce = Account.Nonce.of_int nonce in
×
2013
            Some (fee_payer_kp, fee_payer_nonce)
×
2014
      in
2015
      let fee = Currency.Fee.of_nanomina_int_exn fee in
2016
      let protocol_state_precondition =
×
2017
        match valid_period with
2018
        | None ->
×
2019
            Zkapp_precondition.Protocol_state.accept
2020
        | Some time ->
×
2021
            Zkapp_precondition.Protocol_state.valid_until time
×
2022
      in
2023
      let test_spec : Transaction_snark.For_tests.Multiple_transfers_spec.t =
2024
        { sender
2025
        ; fee_payer
2026
        ; fee
2027
        ; receivers = [ (receiver, amount) ]
2028
        ; amount
2029
        ; zkapp_account_keypairs = []
2030
        ; memo = Signed_command_memo.create_from_string_exn "expiry tests"
×
2031
        ; new_zkapp_account = false
2032
        ; snapp_update = Account_update.Update.dummy
2033
        ; call_data = Snark_params.Tick.Field.zero
2034
        ; events = []
2035
        ; actions = []
2036
        ; preconditions =
2037
            Some
2038
              { Account_update.Preconditions.network =
2039
                  protocol_state_precondition
2040
              ; account =
2041
                  (let nonce =
2042
                     if Option.is_none fee_payer then
2043
                       Account.Nonce.succ sender_nonce
×
2044
                     else sender_nonce
×
2045
                   in
2046
                   Zkapp_precondition.Account.nonce nonce )
×
2047
              ; valid_while = Ignore
2048
              }
2049
        }
2050
      in
2051
      let zkapp_command =
2052
        Transaction_snark.For_tests.multiple_transfers ~constraint_constants
2053
          test_spec
2054
      in
2055
      let zkapp_command =
×
2056
        Or_error.ok_exn
2057
          (Zkapp_command.Valid.to_valid ~failed:false
×
2058
             ~find_vk:
2059
               (Zkapp_command.Verifiable.load_vk_from_ledger
2060
                  ~get:(fun _ -> failwith "Not expecting proof zkapp_command")
×
2061
                  ~location_of_account:(fun _ ->
2062
                    failwith "Not expecting proof zkapp_command" ) )
×
2063
             zkapp_command )
2064
      in
2065
      User_command.Zkapp_command zkapp_command
×
2066

2067
    let mk_payment ?valid_until ~sender_idx ~receiver_idx ~fee ~nonce ~amount ()
2068
        =
2069
      User_command.Signed_command
×
2070
        (mk_payment' ?valid_until ~sender_idx ~fee ~nonce ~receiver_idx ~amount
×
2071
           () )
2072

2073
    let mk_zkapp_commands_single_block num_cmds (pool : Test.Resource_pool.t) :
2074
        User_command.Valid.t list Deferred.t =
2075
      assert (num_cmds < Array.length test_keys - 1) ;
×
2076
      let best_tip_ledger = Option.value_exn pool.best_tip_ledger in
2077
      let keymap =
×
2078
        Array.fold (Array.append test_keys extra_keys)
×
2079
          ~init:Public_key.Compressed.Map.empty
2080
          ~f:(fun map { public_key; private_key } ->
2081
            let key = Public_key.compress public_key in
×
2082
            Public_key.Compressed.Map.add_exn map ~key ~data:private_key )
×
2083
      in
2084
      let account_state_tbl =
×
2085
        List.take (Array.to_list test_keys) num_cmds
×
2086
        |> List.map ~f:(fun kp ->
×
2087
               let id =
×
2088
                 Account_id.create
2089
                   (Public_key.compress kp.public_key)
×
2090
                   Token_id.default
2091
               in
2092
               let state =
×
2093
                 Option.value_exn
2094
                   (let%bind.Option loc =
2095
                      Mina_ledger.Ledger.location_of_account best_tip_ledger id
×
2096
                    in
2097
                    Mina_ledger.Ledger.get best_tip_ledger loc )
×
2098
               in
2099
               (id, (state, `Fee_payer)) )
×
2100
        |> Account_id.Table.of_alist_exn
2101
      in
2102
      let rec go n cmds =
×
2103
        let open Quickcheck.Generator.Let_syntax in
×
2104
        if n >= num_cmds then Quickcheck.Generator.return @@ List.rev cmds
×
2105
        else
2106
          let%bind cmd =
2107
            let fee_payer_keypair = test_keys.(n) in
2108
            let%map (zkapp_command : Zkapp_command.t) =
2109
              Mina_generators.Zkapp_command_generators.gen_zkapp_command_from
×
2110
                ~max_token_updates:1 ~keymap ~account_state_tbl
2111
                ~fee_payer_keypair ~ledger:best_tip_ledger ~constraint_constants
2112
                ~genesis_constants ()
2113
            in
2114
            let zkapp_command =
×
2115
              { zkapp_command with
2116
                account_updates =
2117
                  Zkapp_command.Call_forest.map zkapp_command.account_updates
×
2118
                    ~f:(fun (p : Account_update.t) ->
2119
                      { p with
×
2120
                        body =
2121
                          { p.body with
2122
                            preconditions =
2123
                              { p.body.preconditions with
2124
                                account =
2125
                                  ( match p.body.preconditions.account.nonce with
2126
                                  | Zkapp_basic.Or_ignore.Check n as c
×
2127
                                    when Zkapp_precondition.Numeric.(
2128
                                           is_constant Tc.nonce c) ->
×
2129
                                      Zkapp_precondition.Account.nonce n.lower
×
2130
                                  | _ ->
×
2131
                                      Zkapp_precondition.Account.accept )
2132
                              }
2133
                          }
2134
                      } )
2135
              }
2136
            in
2137
            let zkapp_command_valid_vk_hashes =
2138
              Zkapp_command.For_tests.replace_vks zkapp_command vk
2139
            in
2140
            let valid_zkapp_command =
×
2141
              Or_error.ok_exn
2142
                (Zkapp_command.Valid.to_valid ~failed:false
×
2143
                   ~find_vk:
2144
                     (Zkapp_command.Verifiable.load_vk_from_ledger
2145
                        ~get:(Mina_ledger.Ledger.get best_tip_ledger)
×
2146
                        ~location_of_account:
2147
                          (Mina_ledger.Ledger.location_of_account
×
2148
                             best_tip_ledger ) )
2149
                   zkapp_command_valid_vk_hashes )
2150
            in
2151
            User_command.Zkapp_command valid_zkapp_command
×
2152
          in
2153
          go (n + 1) (cmd :: cmds)
×
2154
      in
2155
      let valid_zkapp_commands =
2156
        Quickcheck.random_value ~seed:(`Deterministic "zkapp_command") (go 0 [])
×
2157
      in
2158
      replace_valid_zkapp_command_authorizations ~keymap ~ledger:best_tip_ledger
×
2159
        valid_zkapp_commands
2160

2161
    type pool_apply = (User_command.t list, [ `Other of Error.t ]) Result.t
×
2162
    [@@deriving sexp, compare]
2163

2164
    let canonicalize t =
2165
      Result.map t ~f:(List.sort ~compare:User_command.compare)
×
2166

2167
    let compare_pool_apply (t1 : pool_apply) (t2 : pool_apply) =
2168
      compare_pool_apply (canonicalize t1) (canonicalize t2)
×
2169

2170
    let assert_pool_apply expected_commands result =
2171
      let accepted_commands =
×
2172
        Result.map result ~f:(fun (_, accepted, _) -> accepted)
×
2173
      in
2174
      [%test_eq: pool_apply] accepted_commands
×
2175
        (Ok (List.map ~f:User_command.forget_check expected_commands))
×
2176

2177
    let mk_with_status (cmd : User_command.Valid.t) =
2178
      { With_status.data = cmd; status = Applied }
×
2179

2180
    let add_commands ?(local = true) test cs =
×
2181
      let sender =
×
2182
        if local then Envelope.Sender.Local
×
2183
        else
2184
          Envelope.Sender.Remote
×
2185
            (Peer.create
×
2186
               (Unix.Inet_addr.of_string "1.2.3.4")
×
2187
               ~peer_id:
2188
                 (Peer.Id.unsafe_of_string "contents should be irrelevant")
×
2189
               ~libp2p_port:8302 )
2190
      in
2191
      let tm0 = Time.now () in
2192
      let%map verified =
2193
        Test.Resource_pool.Diff.verify test.txn_pool
×
2194
          (Envelope.Incoming.wrap
2195
             ~data:(List.map ~f:User_command.forget_check cs)
×
2196
             ~sender )
2197
        >>| Fn.compose Or_error.ok_exn
×
2198
              (Result.map_error ~f:Intf.Verification_error.to_error)
2199
      in
2200
      let result =
×
2201
        Test.Resource_pool.Diff.unsafe_apply test.txn_pool verified
2202
      in
2203
      let tm1 = Time.now () in
×
2204
      [%log' info test.txn_pool.logger] "Time for add_commands: %0.04f sec"
×
2205
        (Time.diff tm1 tm0 |> Time.Span.to_sec) ;
×
2206
      ( match result with
×
2207
      | Ok (`Accept, _, rejects) ->
×
2208
          List.iter rejects ~f:(fun (cmd, err) ->
×
2209
              Core.Printf.printf
×
2210
                !"command was rejected because %s: %{Yojson.Safe}\n%!"
×
2211
                (Diff_versioned.Diff_error.to_string_name err)
×
2212
                (User_command.to_yojson cmd) )
×
2213
      | Ok (`Reject, _, _) ->
×
2214
          failwith "diff was rejected during application"
2215
      | Error (`Other err) ->
×
2216
          Core.Printf.printf
×
2217
            !"failed to apply diff to pool: %s\n%!"
2218
            (Error.to_string_hum err) ) ;
×
2219
      result
2220

2221
    let add_commands' ?local test cs =
2222
      add_commands ?local test cs >>| assert_pool_apply cs
×
2223

2224
    let reorg ?(reorg_best_tip = false) test new_commands removed_commands =
×
2225
      let%bind () =
2226
        Broadcast_pipe.Writer.write test.best_tip_diff_w
×
2227
          { Mock_transition_frontier.new_commands =
2228
              List.map ~f:mk_with_status new_commands
×
2229
          ; removed_commands = List.map ~f:mk_with_status removed_commands
×
2230
          ; reorg_best_tip
2231
          }
2232
      in
2233
      Async.Scheduler.yield_until_no_jobs_remain ()
×
2234

2235
    let commit_commands test cs =
2236
      let ledger = Option.value_exn test.txn_pool.best_tip_ledger in
×
2237
      List.iter cs ~f:(fun c ->
×
2238
          match User_command.forget_check c with
×
2239
          | User_command.Signed_command c -> (
×
2240
              let (`If_this_is_used_it_should_have_a_comment_justifying_it valid)
2241
                  =
2242
                Signed_command.to_valid_unsafe c
2243
              in
2244
              let applied =
×
2245
                Or_error.ok_exn
2246
                @@ Mina_ledger.Ledger.apply_user_command ~constraint_constants
×
2247
                     ~txn_global_slot:
2248
                       Mina_numbers.Global_slot_since_genesis.zero ledger valid
2249
              in
2250
              match applied.body with
×
2251
              | Failed ->
×
2252
                  failwith "failed to apply user command to ledger"
2253
              | _ ->
×
2254
                  () )
2255
          | User_command.Zkapp_command p -> (
×
2256
              let applied, _ =
2257
                Or_error.ok_exn
2258
                @@ Mina_ledger.Ledger.apply_zkapp_command_unchecked
×
2259
                     ~constraint_constants
2260
                     ~global_slot:dummy_state_view.global_slot_since_genesis
2261
                     ~state_view:dummy_state_view ledger p
2262
              in
2263
              match With_status.status applied.command with
×
2264
              | Failed failures ->
×
2265
                  failwithf
2266
                    "failed to apply zkapp_command transaction to ledger: [%s]"
2267
                    ( String.concat ~sep:", "
×
2268
                    @@ List.bind
×
2269
                         ~f:(List.map ~f:Transaction_status.Failure.to_string)
2270
                         failures )
2271
                    ()
2272
              | Applied ->
×
2273
                  () ) )
2274

2275
    let commit_commands' test cs =
2276
      let open Mina_ledger in
×
2277
      let ledger = Option.value_exn test.txn_pool.best_tip_ledger in
2278
      test.best_tip_ref :=
×
2279
        Ledger.Maskable.register_mask
×
2280
          (Ledger.Any_ledger.cast (module Mina_ledger.Ledger) ledger)
×
2281
          (Ledger.Mask.create ~depth:(Ledger.depth ledger) ()) ;
×
2282
      let%map () = reorg test [] [] in
×
2283
      assert (
×
2284
        not (phys_equal (Option.value_exn test.txn_pool.best_tip_ledger) ledger) ) ;
×
2285
      assert (
×
2286
        phys_equal
×
2287
          (Option.value_exn test.txn_pool.best_tip_ledger)
×
2288
          !(test.best_tip_ref) ) ;
2289
      commit_commands test cs ;
2290
      assert (
×
2291
        not (phys_equal (Option.value_exn test.txn_pool.best_tip_ledger) ledger) ) ;
×
2292
      assert (
×
2293
        phys_equal
×
2294
          (Option.value_exn test.txn_pool.best_tip_ledger)
×
2295
          !(test.best_tip_ref) ) ;
2296
      ledger
2297

2298
    let advance_chain test cs = commit_commands test cs ; reorg test cs []
×
2299

2300
    (* TODO: remove this (all of these test should be expressed by committing txns to the ledger, not mutating accounts *)
2301
    let modify_ledger ledger ~idx ~balance ~nonce =
2302
      let id =
×
2303
        Account_id.create
2304
          (Signature_lib.Public_key.compress test_keys.(idx).public_key)
×
2305
          Token_id.default
2306
      in
2307
      let loc =
×
2308
        Option.value_exn @@ Mina_ledger.Ledger.location_of_account ledger id
×
2309
      in
2310
      let account = Option.value_exn @@ Mina_ledger.Ledger.get ledger loc in
×
2311
      Mina_ledger.Ledger.set ledger loc
×
2312
        { account with
2313
          balance = Currency.Balance.of_nanomina_int_exn balance
×
2314
        ; nonce = Account.Nonce.of_int nonce
×
2315
        }
2316

2317
    let mk_linear_case_test t cmds =
2318
      assert_pool_txs t [] ;
×
2319
      let%bind () = add_commands' t cmds in
×
2320
      let%bind () = advance_chain t (List.take independent_cmds 1) in
×
2321
      assert_pool_txs t (List.drop cmds 1) ;
×
2322
      let%bind () =
2323
        advance_chain t (List.take (List.drop independent_cmds 1) 2)
×
2324
      in
2325
      assert_pool_txs t (List.drop cmds 3) ;
×
2326
      Deferred.unit
×
2327

2328
    let%test_unit "transactions are removed in linear case (user cmds)" =
2329
      Thread_safe.block_on_async_exn (fun () ->
×
2330
          let%bind test = setup_test () in
×
2331
          mk_linear_case_test test independent_cmds )
×
2332

2333
    let%test_unit "transactions are removed in linear case (zkapps)" =
2334
      Thread_safe.block_on_async_exn (fun () ->
×
2335
          let%bind test = setup_test () in
×
2336
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2337
          >>= mk_linear_case_test test )
×
2338

2339
    let mk_remove_and_add_test t cmds =
2340
      assert_pool_txs t [] ;
×
2341
      (* omit the 1st (0-based) command *)
2342
      let%bind () = add_commands' t (List.hd_exn cmds :: List.drop cmds 2) in
×
2343
      commit_commands t (List.take cmds 1) ;
×
2344
      let%bind () = reorg t (List.take cmds 1) (List.slice cmds 1 2) in
×
2345
      assert_pool_txs t (List.tl_exn cmds) ;
×
2346
      Deferred.unit
×
2347

2348
    let%test_unit "Transactions are removed and added back in fork changes \
2349
                   (user cmds)" =
2350
      Thread_safe.block_on_async_exn (fun () ->
×
2351
          let%bind test = setup_test () in
×
2352
          mk_remove_and_add_test test independent_cmds )
×
2353

2354
    let%test_unit "Transactions are removed and added back in fork changes \
2355
                   (zkapps)" =
2356
      Thread_safe.block_on_async_exn (fun () ->
×
2357
          let%bind test = setup_test () in
×
2358
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2359
          >>= mk_remove_and_add_test test )
×
2360

2361
    let mk_invalid_test t cmds =
2362
      assert_pool_txs t [] ;
×
2363
      let%bind () = advance_chain t (List.take cmds 2) in
×
2364
      let%bind () =
2365
        add_commands t cmds >>| assert_pool_apply (List.drop cmds 2)
×
2366
      in
2367
      assert_pool_txs t (List.drop cmds 2) ;
×
2368
      Deferred.unit
×
2369

2370
    let%test_unit "invalid transactions are not accepted (user cmds)" =
2371
      Thread_safe.block_on_async_exn (fun () ->
×
2372
          let%bind test = setup_test () in
×
2373
          mk_invalid_test test independent_cmds )
×
2374

2375
    let%test_unit "invalid transactions are not accepted (zkapps)" =
2376
      Thread_safe.block_on_async_exn (fun () ->
×
2377
          let%bind test = setup_test () in
×
2378
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2379
          >>= mk_invalid_test test )
×
2380

2381
    let current_global_slot () =
2382
      let current_time = Block_time.now time_controller in
×
2383
      (* for testing, consider this slot to be a since-genesis slot *)
2384
      Consensus.Data.Consensus_time.(
×
2385
        of_time_exn ~constants:consensus_constants current_time
×
2386
        |> to_global_slot)
×
2387
      |> Mina_numbers.Global_slot_since_hard_fork.to_uint32
×
2388
      |> Mina_numbers.Global_slot_since_genesis.of_uint32
2389

2390
    let mk_now_invalid_test t _cmds ~mk_command =
2391
      let cmd1 =
×
2392
        mk_command ~sender_idx:0 ~receiver_idx:5 ~fee:minimum_fee ~nonce:0
2393
          ~amount:99_999_999_999 ()
2394
      in
2395
      let cmd2 =
×
2396
        mk_command ~sender_idx:0 ~receiver_idx:5 ~fee:minimum_fee ~nonce:0
2397
          ~amount:999_000_000_000 ()
2398
      in
2399
      assert_pool_txs t [] ;
×
2400
      let%bind () = add_commands' t [ cmd1 ] in
×
2401
      assert_pool_txs t [ cmd1 ] ;
×
2402
      let%bind () = advance_chain t [ cmd2 ] in
×
2403
      assert_pool_txs t [] ; Deferred.unit
×
2404

2405
    let%test_unit "Now-invalid transactions are removed from the pool on fork \
2406
                   changes (user cmds)" =
2407
      Thread_safe.block_on_async_exn (fun () ->
×
2408
          let%bind test = setup_test () in
×
2409
          mk_now_invalid_test test independent_cmds
×
2410
            ~mk_command:(mk_payment ?valid_until:None) )
2411

2412
    let%test_unit "Now-invalid transactions are removed from the pool on fork \
2413
                   changes (zkapps)" =
2414
      Thread_safe.block_on_async_exn (fun () ->
×
2415
          let%bind test = setup_test () in
×
2416
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2417
          >>= mk_now_invalid_test test
×
2418
                ~mk_command:
2419
                  (mk_transfer_zkapp_command ?valid_period:None
2420
                     ?fee_payer_idx:None ) )
2421

2422
    let mk_expired_not_accepted_test t ~padding cmds =
2423
      assert_pool_txs t [] ;
×
2424
      let%bind () =
2425
        let current_time = Block_time.now time_controller in
2426
        let slot_end =
×
2427
          Consensus.Data.Consensus_time.(
2428
            of_time_exn ~constants:consensus_constants current_time
×
2429
            |> end_time ~constants:consensus_constants)
×
2430
        in
2431
        at (Block_time.to_time_exn slot_end)
×
2432
      in
2433
      let curr_slot = current_global_slot () in
×
2434
      let slot_padding = Mina_numbers.Global_slot_span.of_int padding in
×
2435
      let curr_slot_plus_padding =
×
2436
        Mina_numbers.Global_slot_since_genesis.add curr_slot slot_padding
2437
      in
2438
      let valid_command =
×
2439
        mk_payment ~valid_until:curr_slot_plus_padding ~sender_idx:1
2440
          ~fee:minimum_fee ~nonce:1 ~receiver_idx:7 ~amount:1_000_000_000 ()
2441
      in
2442
      let expired_commands =
×
2443
        [ mk_payment ~valid_until:curr_slot ~sender_idx:0 ~fee:minimum_fee
×
2444
            ~nonce:1 ~receiver_idx:9 ~amount:1_000_000_000 ()
2445
        ; mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:2 ~receiver_idx:9
×
2446
            ~amount:1_000_000_000 ()
2447
        ]
2448
      in
2449
      (* Wait till global slot increases by 1 which invalidates
2450
         the commands with valid_until = curr_slot
2451
      *)
2452
      let%bind () =
2453
        after
×
2454
          (Block_time.Span.to_time_span
×
2455
             consensus_constants.block_window_duration_ms )
2456
      in
2457
      let all_valid_commands = cmds @ [ valid_command ] in
×
2458
      let%bind () =
2459
        add_commands t (all_valid_commands @ expired_commands)
×
2460
        >>| assert_pool_apply all_valid_commands
×
2461
      in
2462
      assert_pool_txs t all_valid_commands ;
×
2463
      Deferred.unit
×
2464

2465
    let%test_unit "expired transactions are not accepted (user cmds)" =
2466
      Thread_safe.block_on_async_exn (fun () ->
×
2467
          let%bind test = setup_test () in
×
2468
          mk_expired_not_accepted_test test ~padding:10 independent_cmds )
×
2469

2470
    let%test_unit "expired transactions are not accepted (zkapps)" =
2471
      Thread_safe.block_on_async_exn (fun () ->
×
2472
          let%bind test = setup_test () in
×
2473
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2474
          >>= mk_expired_not_accepted_test test ~padding:55 )
×
2475

2476
    let%test_unit "Expired transactions that are already in the pool are \
2477
                   removed from the pool when best tip changes (user commands)"
2478
        =
2479
      Thread_safe.block_on_async_exn (fun () ->
×
2480
          let%bind t = setup_test () in
×
2481
          assert_pool_txs t [] ;
×
2482
          let curr_slot = current_global_slot () in
×
2483
          let curr_slot_plus_three =
×
2484
            Mina_numbers.Global_slot_since_genesis.add curr_slot
2485
              (Mina_numbers.Global_slot_span.of_int 3)
×
2486
          in
2487
          let curr_slot_plus_seven =
×
2488
            Mina_numbers.Global_slot_since_genesis.add curr_slot
2489
              (Mina_numbers.Global_slot_span.of_int 7)
×
2490
          in
2491
          let few_now =
×
2492
            List.take independent_cmds (List.length independent_cmds / 2)
×
2493
          in
2494
          let expires_later1 =
×
2495
            mk_payment ~valid_until:curr_slot_plus_three ~sender_idx:0
2496
              ~fee:minimum_fee ~nonce:1 ~receiver_idx:9 ~amount:10_000_000_000
2497
              ()
2498
          in
2499
          let expires_later2 =
×
2500
            mk_payment ~valid_until:curr_slot_plus_seven ~sender_idx:0
2501
              ~fee:minimum_fee ~nonce:2 ~receiver_idx:9 ~amount:10_000_000_000
2502
              ()
2503
          in
2504
          let valid_commands = few_now @ [ expires_later1; expires_later2 ] in
×
2505
          let%bind () = add_commands' t valid_commands in
×
2506
          assert_pool_txs t valid_commands ;
×
2507
          (* new commands from best tip diff should be removed from the pool *)
2508
          (* update the nonce to be consistent with the commands in the block *)
2509
          modify_ledger !(t.best_tip_ref) ~idx:0 ~balance:1_000_000_000_000_000
×
2510
            ~nonce:2 ;
2511
          let%bind () = reorg t [ List.nth_exn few_now 0; expires_later1 ] [] in
×
2512
          let%bind () = Async.Scheduler.yield_until_no_jobs_remain () in
×
2513
          assert_pool_txs t (expires_later2 :: List.drop few_now 1) ;
×
2514
          (* Add new commands, remove old commands some of which are now expired *)
2515
          let expired_command =
×
2516
            mk_payment ~valid_until:curr_slot ~sender_idx:9 ~fee:minimum_fee
2517
              ~nonce:0 ~receiver_idx:5 ~amount:1_000_000_000 ()
2518
          in
2519
          let unexpired_command =
×
2520
            mk_payment ~valid_until:curr_slot_plus_seven ~sender_idx:8
2521
              ~fee:minimum_fee ~nonce:0 ~receiver_idx:9 ~amount:1_000_000_000 ()
2522
          in
2523
          let valid_forever = List.nth_exn few_now 0 in
×
2524
          let removed_commands =
×
2525
            [ valid_forever
2526
            ; expires_later1
2527
            ; expired_command
2528
            ; unexpired_command
2529
            ]
2530
          in
2531
          let n_block_times n =
2532
            Int64.(
×
2533
              Block_time.Span.to_ms consensus_constants.block_window_duration_ms
×
2534
              * n)
2535
            |> Block_time.Span.of_ms
2536
          in
2537
          let%bind () =
2538
            after (Block_time.Span.to_time_span (n_block_times 3L))
×
2539
          in
2540
          modify_ledger !(t.best_tip_ref) ~idx:0 ~balance:1_000_000_000_000_000
×
2541
            ~nonce:1 ;
2542
          let%bind _ = reorg t [ valid_forever ] removed_commands in
×
2543
          (* expired_command should not be in the pool because they are expired
2544
             and (List.nth few_now 0) because it was committed in a block
2545
          *)
2546
          assert_pool_txs t
×
2547
            ( expires_later1 :: expires_later2 :: unexpired_command
2548
            :: List.drop few_now 1 ) ;
×
2549
          (* after 5 block times there should be no expired transactions *)
2550
          let%bind () =
2551
            after (Block_time.Span.to_time_span (n_block_times 5L))
×
2552
          in
2553
          let%bind _ = reorg t [] [] in
×
2554
          assert_pool_txs t (List.drop few_now 1) ;
×
2555
          Deferred.unit )
×
2556

2557
    let%test_unit "Expired transactions that are already in the pool are \
2558
                   removed from the pool when best tip changes (zkapps)" =
2559
      Thread_safe.block_on_async_exn (fun () ->
×
2560
          let%bind t = setup_test () in
×
2561
          assert_pool_txs t [] ;
×
2562
          let curr_slot = current_global_slot () in
×
2563
          let curr_slot_plus_three =
×
2564
            Mina_numbers.Global_slot_since_genesis.add curr_slot
2565
              (Mina_numbers.Global_slot_span.of_int 3)
×
2566
          in
2567
          let curr_slot_plus_seven =
×
2568
            Mina_numbers.Global_slot_since_genesis.add curr_slot
2569
              (Mina_numbers.Global_slot_span.of_int 7)
×
2570
          in
2571
          let few_now =
×
2572
            List.take independent_cmds (List.length independent_cmds / 2)
×
2573
          in
2574
          let expires_later1 =
×
2575
            mk_transfer_zkapp_command
2576
              ~valid_period:{ lower = curr_slot; upper = curr_slot_plus_three }
2577
              ~fee_payer_idx:(0, 1) ~sender_idx:1 ~receiver_idx:9
2578
              ~fee:minimum_fee ~amount:10_000_000_000 ~nonce:1 ()
2579
          in
2580
          let expires_later2 =
×
2581
            mk_transfer_zkapp_command
2582
              ~valid_period:{ lower = curr_slot; upper = curr_slot_plus_seven }
2583
              ~fee_payer_idx:(2, 1) ~sender_idx:3 ~receiver_idx:9
2584
              ~fee:minimum_fee ~amount:10_000_000_000 ~nonce:1 ()
2585
          in
2586
          let valid_commands = few_now @ [ expires_later1; expires_later2 ] in
×
2587
          let%bind () = add_commands' t valid_commands in
×
2588
          assert_pool_txs t valid_commands ;
×
2589
          let n_block_times n =
×
2590
            Int64.(
×
2591
              Block_time.Span.to_ms consensus_constants.block_window_duration_ms
×
2592
              * n)
2593
            |> Block_time.Span.of_ms
2594
          in
2595
          let%bind () =
2596
            after (Block_time.Span.to_time_span (n_block_times 4L))
×
2597
          in
2598
          let%bind () = reorg t [] [] in
×
2599
          assert_pool_txs t (expires_later2 :: few_now) ;
×
2600
          (* after 5 block times there should be no expired transactions *)
2601
          let%bind () =
2602
            after (Block_time.Span.to_time_span (n_block_times 5L))
×
2603
          in
2604
          let%bind () = reorg t [] [] in
×
2605
          assert_pool_txs t few_now ; Deferred.unit )
×
2606

2607
    let%test_unit "Now-invalid transactions are removed from the pool when the \
2608
                   transition frontier is recreated (user cmds)" =
2609
      Thread_safe.block_on_async_exn (fun () ->
×
2610
          (* Set up initial frontier *)
2611
          let%bind t = setup_test () in
×
2612
          assert_pool_txs t [] ;
×
2613
          let%bind _ = add_commands t independent_cmds in
×
2614
          assert_pool_txs t independent_cmds ;
×
2615
          (* Destroy initial frontier *)
2616
          Broadcast_pipe.Writer.close t.best_tip_diff_w ;
×
2617
          let%bind _ = Broadcast_pipe.Writer.write t.frontier_pipe_w None in
×
2618
          (* Set up second frontier *)
2619
          let ((_, ledger_ref2) as frontier2), _best_tip_diff_w2 =
×
2620
            Mock_transition_frontier.create ()
2621
          in
2622
          modify_ledger !ledger_ref2 ~idx:0 ~balance:20_000_000_000_000 ~nonce:5 ;
×
2623
          modify_ledger !ledger_ref2 ~idx:1 ~balance:0 ~nonce:0 ;
×
2624
          modify_ledger !ledger_ref2 ~idx:2 ~balance:0 ~nonce:1 ;
×
2625
          let%bind _ =
2626
            Broadcast_pipe.Writer.write t.frontier_pipe_w (Some frontier2)
×
2627
          in
2628
          assert_pool_txs t (List.drop independent_cmds 3) ;
×
2629
          Deferred.unit )
×
2630

2631
    let%test_unit "transaction replacement works" =
2632
      Thread_safe.block_on_async_exn
×
2633
      @@ fun () ->
2634
      let%bind t = setup_test () in
×
2635
      let set_sender idx (tx : Signed_command.t) =
×
2636
        let sender_kp = test_keys.(idx) in
×
2637
        let sender_pk = Public_key.compress sender_kp.public_key in
×
2638
        let payload : Signed_command.Payload.t =
×
2639
          match tx.payload with
2640
          | { common; body = Payment payload } ->
×
2641
              { common = { common with fee_payer_pk = sender_pk }
2642
              ; body = Payment payload
2643
              }
2644
          | { common; body = Stake_delegation (Set_delegate payload) } ->
×
2645
              { common = { common with fee_payer_pk = sender_pk }
2646
              ; body = Stake_delegation (Set_delegate payload)
2647
              }
2648
        in
2649
        User_command.Signed_command (Signed_command.sign sender_kp payload)
×
2650
      in
2651
      let txs0 =
2652
        [ mk_payment' ~sender_idx:0 ~fee:minimum_fee ~nonce:0 ~receiver_idx:9
×
2653
            ~amount:20_000_000_000 ()
2654
        ; mk_payment' ~sender_idx:0 ~fee:minimum_fee ~nonce:1 ~receiver_idx:9
×
2655
            ~amount:12_000_000_000 ()
2656
        ; mk_payment' ~sender_idx:0 ~fee:minimum_fee ~nonce:2 ~receiver_idx:9
×
2657
            ~amount:500_000_000_000 ()
2658
        ]
2659
      in
2660
      let txs0' = List.map txs0 ~f:Signed_command.forget_check in
2661
      let txs1 = List.map ~f:(set_sender 1) txs0' in
×
2662
      let txs2 = List.map ~f:(set_sender 2) txs0' in
×
2663
      let txs3 = List.map ~f:(set_sender 3) txs0' in
×
2664
      let txs_all =
×
2665
        List.map ~f:(fun x -> User_command.Signed_command x) txs0
×
2666
        @ txs1 @ txs2 @ txs3
2667
      in
2668
      let%bind () = add_commands' t txs_all in
×
2669
      assert_pool_txs t txs_all ;
×
2670
      let replace_txs =
×
2671
        [ (* sufficient fee *)
2672
          mk_payment ~sender_idx:0
×
2673
            ~fee:
2674
              ( minimum_fee
2675
              + Currency.Fee.to_nanomina_int Indexed_pool.replace_fee )
×
2676
            ~nonce:0 ~receiver_idx:1 ~amount:440_000_000_000 ()
2677
        ; (* insufficient fee *)
2678
          mk_payment ~sender_idx:1 ~fee:minimum_fee ~nonce:0 ~receiver_idx:1
×
2679
            ~amount:788_000_000_000 ()
2680
        ; (* sufficient *)
2681
          mk_payment ~sender_idx:2
×
2682
            ~fee:
2683
              ( minimum_fee
2684
              + Currency.Fee.to_nanomina_int Indexed_pool.replace_fee )
×
2685
            ~nonce:1 ~receiver_idx:4 ~amount:721_000_000_000 ()
2686
        ; (* insufficient *)
2687
          (let amount = 927_000_000_000 in
2688
           let fee =
2689
             let ledger = !(t.best_tip_ref) in
2690
             let sender_kp = test_keys.(3) in
2691
             let sender_pk = Public_key.compress sender_kp.public_key in
×
2692
             let sender_aid = Account_id.create sender_pk Token_id.default in
×
2693
             let location =
×
2694
               Mock_base_ledger.location_of_account ledger sender_aid
×
2695
               |> Option.value_exn
2696
             in
2697
             (* Spend all of the tokens in the account. Should fail because the
2698
                command with nonce=0 will already have spent some.
2699
             *)
2700
             let account =
×
2701
               Mock_base_ledger.get ledger location |> Option.value_exn
×
2702
             in
2703
             Currency.Balance.to_nanomina_int account.balance - amount
×
2704
           in
2705
           mk_payment ~sender_idx:3 ~fee ~nonce:1 ~receiver_idx:4 ~amount () )
×
2706
        ]
2707
      in
2708
      add_commands t replace_txs
×
2709
      >>| assert_pool_apply
×
2710
            [ List.nth_exn replace_txs 0; List.nth_exn replace_txs 2 ]
×
2711

2712
    let%test_unit "it drops queued transactions if a committed one makes there \
2713
                   be insufficient funds" =
2714
      Thread_safe.block_on_async_exn
×
2715
      @@ fun () ->
2716
      let%bind t = setup_test () in
×
2717
      let txs =
×
2718
        [ mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:0 ~receiver_idx:9
×
2719
            ~amount:20_000_000_000 ()
2720
        ; mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:1 ~receiver_idx:5
×
2721
            ~amount:77_000_000_000 ()
2722
        ; mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:2 ~receiver_idx:3
×
2723
            ~amount:891_000_000_000 ()
2724
        ]
2725
      in
2726
      let committed_tx =
2727
        mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:0 ~receiver_idx:2
2728
          ~amount:25_000_000_000 ()
2729
      in
2730
      let%bind () = add_commands' t txs in
×
2731
      assert_pool_txs t txs ;
×
2732
      modify_ledger !(t.best_tip_ref) ~idx:0 ~balance:970_000_000_000 ~nonce:1 ;
×
2733
      let%bind () = reorg t [ committed_tx ] [] in
×
2734
      assert_pool_txs t [ List.nth_exn txs 1 ] ;
×
2735
      Deferred.unit
×
2736

2737
    let%test_unit "max size is maintained" =
2738
      Quickcheck.test ~trials:500
×
2739
        (let open Quickcheck.Generator.Let_syntax in
2740
        let%bind init_ledger_state =
2741
          Mina_ledger.Ledger.gen_initial_ledger_state
2742
        in
2743
        let%bind cmds_count = Int.gen_incl pool_max_size (pool_max_size * 2) in
×
2744
        let%bind cmds =
2745
          User_command.Valid.Gen.sequence ~sign_type:`Real ~length:cmds_count
×
2746
            init_ledger_state
2747
        in
2748
        return (init_ledger_state, cmds))
×
2749
        ~f:(fun (init_ledger_state, cmds) ->
2750
          Thread_safe.block_on_async_exn (fun () ->
×
2751
              let%bind t = setup_test () in
×
2752
              let new_ledger =
×
2753
                Mina_ledger.Ledger.create_ephemeral
2754
                  ~depth:(Mina_ledger.Ledger.depth !(t.best_tip_ref))
×
2755
                  ()
2756
              in
2757
              Mina_ledger.Ledger.apply_initial_ledger_state new_ledger
×
2758
                init_ledger_state ;
2759
              t.best_tip_ref := new_ledger ;
×
2760
              let%bind () = reorg ~reorg_best_tip:true t [] [] in
×
2761
              let cmds1, cmds2 = List.split_n cmds pool_max_size in
×
2762
              let%bind apply_res1 = add_commands t cmds1 in
×
2763
              assert (Result.is_ok apply_res1) ;
×
2764
              [%test_eq: int] pool_max_size (Indexed_pool.size t.txn_pool.pool) ;
×
2765
              let%map _apply_res2 = add_commands t cmds2 in
×
2766
              (* N.B. Adding a transaction when the pool is full may drop > 1
2767
                 command, so the size now is not necessarily the maximum.
2768
                 Applying the diff may also return an error if none of the new
2769
                 commands have higher fee than the lowest one already in the
2770
                 pool.
2771
              *)
2772
              assert (Indexed_pool.size t.txn_pool.pool <= pool_max_size) ) )
×
2773

2774
    let assert_rebroadcastable test cmds =
2775
      let expected =
×
2776
        if List.is_empty cmds then []
×
2777
        else
2778
          [ List.map cmds
×
2779
              ~f:
2780
                (Fn.compose Transaction_hash.User_command.create
×
2781
                   User_command.forget_check )
2782
          ]
2783
      in
2784
      let actual =
2785
        Test.Resource_pool.get_rebroadcastable test.txn_pool
×
2786
          ~has_timed_out:(Fn.const `Ok)
×
2787
        |> List.map ~f:(List.map ~f:Transaction_hash.User_command.create)
2788
      in
2789
      if List.length actual > 1 then
×
2790
        failwith "unexpected number of rebroadcastable diffs" ;
×
2791

2792
      List.iter (List.zip_exn actual expected) ~f:(fun (a, b) ->
×
2793
          assert_user_command_sets_equal a b )
×
2794

2795
    let mk_rebroadcastable_test t cmds =
2796
      assert_pool_txs t [] ;
×
2797
      assert_rebroadcastable t [] ;
×
2798
      (* Locally generated transactions are rebroadcastable *)
2799
      let%bind () = add_commands' ~local:true t (List.take cmds 2) in
×
2800
      assert_pool_txs t (List.take cmds 2) ;
×
2801
      assert_rebroadcastable t (List.take cmds 2) ;
×
2802
      (* Adding non-locally-generated transactions doesn't affect
2803
         rebroadcastable pool *)
2804
      let%bind () = add_commands' ~local:false t (List.slice cmds 2 5) in
×
2805
      assert_pool_txs t (List.take cmds 5) ;
×
2806
      assert_rebroadcastable t (List.take cmds 2) ;
×
2807
      (* When locally generated transactions are committed they are no
2808
         longer rebroadcastable *)
2809
      let%bind () = add_commands' ~local:true t (List.slice cmds 5 7) in
×
2810
      let%bind checkpoint_1 = commit_commands' t (List.take cmds 1) in
×
2811
      let%bind checkpoint_2 = commit_commands' t (List.slice cmds 1 5) in
×
2812
      let%bind () = reorg t (List.take cmds 5) [] in
×
2813
      assert_pool_txs t (List.slice cmds 5 7) ;
×
2814
      assert_rebroadcastable t (List.slice cmds 5 7) ;
×
2815
      (* Reorgs put locally generated transactions back into the
2816
         rebroadcastable pool, if they were removed and not re-added *)
2817
      (* restore up to after the application of the first command *)
2818
      t.best_tip_ref := checkpoint_2 ;
×
2819
      (* reorg both removes and re-adds the first command (which is local) *)
2820
      let%bind () = reorg t (List.take cmds 1) (List.take cmds 5) in
×
2821
      assert_pool_txs t (List.slice cmds 1 7) ;
×
2822
      assert_rebroadcastable t (List.nth_exn cmds 1 :: List.slice cmds 5 7) ;
×
2823
      (* Committing them again removes them from the pool again. *)
2824
      commit_commands t (List.slice cmds 1 5) ;
×
2825
      let%bind () = reorg t (List.slice cmds 1 5) [] in
×
2826
      assert_pool_txs t (List.slice cmds 5 7) ;
×
2827
      assert_rebroadcastable t (List.slice cmds 5 7) ;
×
2828
      (* When transactions expire from rebroadcast pool they are gone. This
2829
         doesn't affect the main pool.
2830
      *)
2831
      t.best_tip_ref := checkpoint_1 ;
×
2832
      let%bind () = reorg t [] (List.take cmds 5) in
×
2833
      assert_pool_txs t (List.take cmds 7) ;
×
2834
      assert_rebroadcastable t (List.take cmds 2 @ List.slice cmds 5 7) ;
×
2835
      ignore
×
2836
        ( Test.Resource_pool.get_rebroadcastable t.txn_pool
×
2837
            ~has_timed_out:(Fn.const `Timed_out)
×
2838
          : User_command.t list list ) ;
2839
      assert_rebroadcastable t [] ;
2840
      Deferred.unit
×
2841

2842
    let%test_unit "rebroadcastable transaction behavior (user cmds)" =
2843
      Thread_safe.block_on_async_exn (fun () ->
×
2844
          let%bind test = setup_test () in
×
2845
          mk_rebroadcastable_test test independent_cmds )
×
2846

2847
    let%test_unit "rebroadcastable transaction behavior (zkapps)" =
2848
      Thread_safe.block_on_async_exn (fun () ->
×
2849
          let%bind test = setup_test () in
×
2850
          mk_zkapp_commands_single_block 7 test.txn_pool
×
2851
          >>= mk_rebroadcastable_test test )
×
2852

2853
    let%test_unit "apply user cmds and zkapps" =
2854
      Thread_safe.block_on_async_exn (fun () ->
×
2855
          let%bind t = setup_test () in
×
2856
          let num_cmds = Array.length test_keys in
×
2857
          (* the user cmds and snapp cmds are taken from the same list of keys,
2858
             so splitting by the order from that list makes sure that they
2859
             don't share fee payer keys
2860
             therefore, the original nonces in the accounts are valid
2861
          *)
2862
          let take_len = num_cmds / 2 in
×
2863
          let%bind snapp_cmds =
2864
            let%map cmds = mk_zkapp_commands_single_block 7 t.txn_pool in
×
2865
            List.take cmds take_len
×
2866
          in
2867
          let user_cmds = List.drop independent_cmds take_len in
×
2868
          let all_cmds = snapp_cmds @ user_cmds in
×
2869
          assert_pool_txs t [] ;
2870
          let%bind () = add_commands' t all_cmds in
×
2871
          assert_pool_txs t all_cmds ; Deferred.unit )
×
2872

2873
    let mk_zkapp_user_cmd (pool : Test.Resource_pool.t) zkapp_command =
2874
      let best_tip_ledger = Option.value_exn pool.best_tip_ledger in
×
2875
      let keymap =
×
2876
        Array.fold (Array.append test_keys extra_keys)
×
2877
          ~init:Public_key.Compressed.Map.empty
2878
          ~f:(fun map { public_key; private_key } ->
2879
            let key = Public_key.compress public_key in
×
2880
            Public_key.Compressed.Map.add_exn map ~key ~data:private_key )
×
2881
      in
2882
      let zkapp_command =
×
2883
        Or_error.ok_exn
2884
          (Zkapp_command.Valid.to_valid ~failed:false
×
2885
             ~find_vk:
2886
               (Zkapp_command.Verifiable.load_vk_from_ledger
2887
                  ~get:(Mina_ledger.Ledger.get best_tip_ledger)
×
2888
                  ~location_of_account:
2889
                    (Mina_ledger.Ledger.location_of_account best_tip_ledger) )
×
2890
             zkapp_command )
2891
      in
2892
      let zkapp_command = User_command.Zkapp_command zkapp_command in
×
2893
      let%bind zkapp_command =
2894
        replace_valid_zkapp_command_authorizations ~keymap
×
2895
          ~ledger:best_tip_ledger [ zkapp_command ]
2896
      in
2897
      let zkapp_command = List.hd_exn zkapp_command in
×
2898
      Deferred.return zkapp_command
×
2899

2900
    let mk_basic_zkapp ?(fee = 10_000_000_000) ?(empty_update = false)
×
2901
        ?preconditions ?permissions nonce fee_payer_kp =
2902
      let open Zkapp_command_builder in
×
2903
      let preconditions =
2904
        Option.value preconditions
2905
          ~default:
2906
            Account_update.Preconditions.
2907
              { network = Zkapp_precondition.Protocol_state.accept
2908
              ; account = Zkapp_precondition.Account.accept
2909
              ; valid_while = Ignore
2910
              }
2911
      in
2912
      let update : Account_update.Update.t =
×
2913
        let permissions =
2914
          match permissions with
2915
          | None ->
×
2916
              Zkapp_basic.Set_or_keep.Keep
2917
          | Some perms ->
×
2918
              Zkapp_basic.Set_or_keep.Set perms
2919
        in
2920
        { Account_update.Update.noop with permissions }
2921
      in
2922
      let account_updates =
2923
        if empty_update then []
×
2924
        else
2925
          mk_forest
×
2926
            [ mk_node
×
2927
                (mk_account_update_body Signature No fee_payer_kp
×
2928
                   Token_id.default 0 ~preconditions ~update )
2929
                []
2930
            ]
2931
      in
2932
      account_updates
2933
      |> mk_zkapp_command ~memo:"" ~fee
2934
           ~fee_payer_pk:(Public_key.compress fee_payer_kp.public_key)
×
2935
           ~fee_payer_nonce:(Account.Nonce.of_int nonce)
×
2936

2937
    let%test_unit "zkapp cmd with same nonce should replace previous submitted \
2938
                   zkapp with same nonce" =
2939
      Thread_safe.block_on_async_exn (fun () ->
×
2940
          let%bind () = after (Time.Span.of_sec 2.) in
×
2941
          let%bind t = setup_test () in
×
2942
          assert_pool_txs t [] ;
×
2943
          let fee_payer_kp = test_keys.(0) in
×
2944
          let%bind valid_command1 =
2945
            mk_basic_zkapp ~fee:10_000_000_000 0 fee_payer_kp
×
2946
            |> mk_zkapp_user_cmd t.txn_pool
×
2947
          in
2948
          let%bind valid_command2 =
2949
            mk_basic_zkapp ~fee:20_000_000_000 ~empty_update:true 0 fee_payer_kp
×
2950
            |> mk_zkapp_user_cmd t.txn_pool
×
2951
          in
2952
          let%bind () =
2953
            add_commands t ([ valid_command1 ] @ [ valid_command2 ])
×
2954
            >>| assert_pool_apply [ valid_command2 ]
×
2955
          in
2956
          Deferred.unit )
×
2957

2958
    let%test_unit "commands are rejected if fee payer permissions are not \
2959
                   handled" =
2960
      let test_permissions ~is_able_to_send send_command permissions =
×
2961
        let%bind t = setup_test () in
×
2962
        assert_pool_txs t [] ;
×
2963
        let%bind set_permissions_command =
2964
          mk_basic_zkapp 0 test_keys.(0) ~permissions
×
2965
          |> mk_zkapp_user_cmd t.txn_pool
×
2966
        in
2967
        let%bind () = add_commands' t [ set_permissions_command ] in
×
2968
        let%bind () = advance_chain t [ set_permissions_command ] in
×
2969
        assert_pool_txs t [] ;
×
2970
        let%map result = add_commands t [ send_command ] in
×
2971
        let expectation = if is_able_to_send then [ send_command ] else [] in
×
2972
        assert_pool_apply expectation result
2973
      in
2974
      let run_test_cases send_cmd =
2975
        let%bind () =
2976
          test_permissions ~is_able_to_send:true send_cmd
×
2977
            { Permissions.user_default with
2978
              send = Permissions.Auth_required.Signature
2979
            }
2980
        in
2981
        let%bind () =
2982
          test_permissions ~is_able_to_send:true send_cmd
×
2983
            { Permissions.user_default with
2984
              send = Permissions.Auth_required.Either
2985
            }
2986
        in
2987
        let%bind () =
2988
          test_permissions ~is_able_to_send:true send_cmd
×
2989
            { Permissions.user_default with
2990
              send = Permissions.Auth_required.None
2991
            }
2992
        in
2993
        let%bind () =
2994
          test_permissions ~is_able_to_send:false send_cmd
×
2995
            { Permissions.user_default with
2996
              send = Permissions.Auth_required.Impossible
2997
            }
2998
        in
2999
        let%bind () =
3000
          test_permissions ~is_able_to_send:false send_cmd
×
3001
            { Permissions.user_default with
3002
              send = Permissions.Auth_required.Proof
3003
            }
3004
        in
3005
        let%bind () =
3006
          test_permissions ~is_able_to_send:true send_cmd
×
3007
            { Permissions.user_default with
3008
              increment_nonce = Permissions.Auth_required.Signature
3009
            }
3010
        in
3011
        let%bind () =
3012
          test_permissions ~is_able_to_send:true send_cmd
×
3013
            { Permissions.user_default with
3014
              increment_nonce = Permissions.Auth_required.Either
3015
            }
3016
        in
3017
        let%bind () =
3018
          test_permissions ~is_able_to_send:true send_cmd
×
3019
            { Permissions.user_default with
3020
              increment_nonce = Permissions.Auth_required.None
3021
            }
3022
        in
3023
        let%bind () =
3024
          test_permissions ~is_able_to_send:false send_cmd
×
3025
            { Permissions.user_default with
3026
              increment_nonce = Permissions.Auth_required.Impossible
3027
            }
3028
        in
3029
        let%bind () =
3030
          test_permissions ~is_able_to_send:false send_cmd
×
3031
            { Permissions.user_default with
3032
              increment_nonce = Permissions.Auth_required.Proof
3033
            }
3034
        in
3035
        let%bind () =
3036
          test_permissions ~is_able_to_send:true send_cmd
×
3037
            { Permissions.user_default with
3038
              access = Permissions.Auth_required.Signature
3039
            }
3040
        in
3041
        let%bind () =
3042
          test_permissions ~is_able_to_send:true send_cmd
×
3043
            { Permissions.user_default with
3044
              access = Permissions.Auth_required.Either
3045
            }
3046
        in
3047
        let%bind () =
3048
          test_permissions ~is_able_to_send:true send_cmd
×
3049
            { Permissions.user_default with
3050
              access = Permissions.Auth_required.None
3051
            }
3052
        in
3053
        let%bind () =
3054
          test_permissions ~is_able_to_send:false send_cmd
×
3055
            { Permissions.user_default with
3056
              access = Permissions.Auth_required.Impossible
3057
            }
3058
        in
3059
        let%bind () =
3060
          test_permissions ~is_able_to_send:false send_cmd
×
3061
            { Permissions.user_default with
3062
              access = Permissions.Auth_required.Proof
3063
            }
3064
        in
3065
        return ()
×
3066
      in
3067
      Thread_safe.block_on_async_exn (fun () ->
3068
          let%bind () =
3069
            let send_command =
3070
              mk_payment ~sender_idx:0 ~fee:minimum_fee ~nonce:1 ~receiver_idx:1
3071
                ~amount:1_000_000 ()
3072
            in
3073
            run_test_cases send_command
×
3074
          in
3075
          let%bind () =
3076
            let send_command =
3077
              mk_transfer_zkapp_command ~fee_payer_idx:(0, 1) ~sender_idx:0
3078
                ~fee:minimum_fee ~nonce:2 ~receiver_idx:1 ~amount:1_000_000 ()
3079
            in
3080
            run_test_cases send_command
×
3081
          in
3082
          return () )
×
3083

3084
    let%test "account update with a different network id that uses proof \
3085
              authorization would be rejected" =
3086
      Thread_safe.block_on_async_exn (fun () ->
×
3087
          let%bind verifier_full =
3088
            Verifier.create ~logger ~proof_level:Full ~constraint_constants
×
3089
              ~conf_dir:None
3090
              ~pids:(Child_processes.Termination.create_pid_table ())
×
3091
              ~commit_id:"not specified for unit tests" ()
3092
          in
3093
          let%bind test =
3094
            setup_test ~verifier:verifier_full
×
3095
              ~permissions:
3096
                { Permissions.user_default with set_zkapp_uri = Proof }
3097
              ()
3098
          in
3099
          let%bind zkapp_command =
3100
            mk_single_account_update
3101
              ~chain:Mina_signature_kind.(Other_network "invalid")
3102
              ~fee_payer_idx:0 ~fee:minimum_fee ~nonce:0 ~zkapp_account_idx:1
3103
              ~ledger:(Option.value_exn test.txn_pool.best_tip_ledger)
×
3104
          in
3105
          match%map
3106
            Test.Resource_pool.Diff.verify test.txn_pool
×
3107
              (Envelope.Incoming.wrap
3108
                 ~data:
3109
                   [ User_command.forget_check
×
3110
                     @@ Zkapp_command
3111
                          (Zkapp_command.Valid.of_verifiable zkapp_command)
×
3112
                   ]
3113
                 ~sender:Envelope.Sender.Local )
3114
          with
3115
          | Error (Intf.Verification_error.Invalid e) ->
×
3116
              String.is_substring (Error.to_string_hum e) ~substring:"proof"
×
3117
          | _ ->
×
3118
              false )
3119

3120
    let%test_unit "transactions added before slot_tx_end are accepted" =
3121
      Thread_safe.block_on_async_exn (fun () ->
×
3122
          let curr_slot =
×
3123
            Mina_numbers.(
3124
              Global_slot_since_hard_fork.of_uint32
×
3125
              @@ Global_slot_since_genesis.to_uint32 @@ current_global_slot ())
×
3126
          in
3127
          let slot_tx_end =
3128
            Mina_numbers.Global_slot_since_hard_fork.(succ @@ succ curr_slot)
×
3129
          in
3130
          let%bind t = setup_test ~slot_tx_end () in
×
3131
          assert_pool_txs t [] ;
×
3132
          add_commands t independent_cmds >>| assert_pool_apply independent_cmds )
×
3133

3134
    let%test_unit "transactions added at slot_tx_end are rejected" =
3135
      Thread_safe.block_on_async_exn (fun () ->
×
3136
          let curr_slot =
×
3137
            Mina_numbers.(
3138
              Global_slot_since_hard_fork.of_uint32
×
3139
              @@ Global_slot_since_genesis.to_uint32 @@ current_global_slot ())
×
3140
          in
3141
          let%bind t = setup_test ~slot_tx_end:curr_slot () in
×
3142
          assert_pool_txs t [] ;
×
3143
          add_commands t independent_cmds >>| assert_pool_apply [] )
×
3144

3145
    let%test_unit "transactions added after slot_tx_end are rejected" =
3146
      Thread_safe.block_on_async_exn (fun () ->
×
3147
          let curr_slot =
×
3148
            Mina_numbers.(
3149
              Global_slot_since_hard_fork.of_uint32
×
3150
              @@ Global_slot_since_genesis.to_uint32 @@ current_global_slot ())
×
3151
          in
3152
          let slot_tx_end =
3153
            Option.value_exn
3154
            @@ Mina_numbers.(
3155
                 Global_slot_since_hard_fork.(
3156
                   sub curr_slot @@ Global_slot_span.of_int 1))
×
3157
          in
3158
          let%bind t = setup_test ~slot_tx_end () in
×
3159
          assert_pool_txs t [] ;
×
3160
          add_commands t independent_cmds >>| assert_pool_apply [] )
×
3161
  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