https://gitlab.com/tezos/tezos
Raw File
Tip revision: b0d5e96ce6ee5ccd0a2b260013c1770d895ea532 authored by Alain Mebsout on 04 January 2023, 13:28:17 UTC
Proto/Alpha: expose inbox internal structure with skip list
Tip revision: b0d5e96
mempool.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Nomadic Development. <contact@tezcore.com>             *)
(* Copyright (c) 2021-2022 Nomadic Labs, <contact@nomadic-labs.com>          *)
(* Copyright (c) 2022 TriliTech <contact@trili.tech>                         *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

open Protocol
open Alpha_context

type error_classification =
  [ `Branch_delayed of tztrace
  | `Branch_refused of tztrace
  | `Refused of tztrace
  | `Outdated of tztrace ]

type nanotez = Q.t

let nanotez_enc : nanotez Data_encoding.t =
  let open Data_encoding in
  def
    "nanotez"
    ~title:"A thousandth of a mutez"
    ~description:"One thousand nanotez make a mutez (1 tez = 1e9 nanotez)"
    (conv
       (fun q -> (q.Q.num, q.Q.den))
       (fun (num, den) -> {Q.num; den})
       (tup2 z z))

let manager_op_replacement_factor_enc : Q.t Data_encoding.t =
  let open Data_encoding in
  def
    "manager operation replacement factor"
    ~title:"A manager operation's replacement factor"
    ~description:"The fee and fee/gas ratio of an operation to replace another"
    (conv
       (fun q -> (q.Q.num, q.Q.den))
       (fun (num, den) -> {Q.num; den})
       (tup2 z z))

type config = {
  minimal_fees : Tez.t;
  minimal_nanotez_per_gas_unit : nanotez;
  minimal_nanotez_per_byte : nanotez;
  allow_script_failure : bool;
      (** If [true], this makes [post_filter_manager] unconditionally return
            [`Passed_postfilter filter_state], no matter the operation's
            success. *)
  clock_drift : Period.t option;
  replace_by_fee_factor : Q.t;
      (** This field determines the amount of additional fees (given as a
            factor of the declared fees) a manager should add to an operation
            in order to (eventually) replace an existing (prechecked) one
            in the mempool. Note that other criteria, such as the gas ratio,
            are also taken into account to decide whether to accept the
            replacement or not. *)
  max_prechecked_manager_operations : int;
      (** Maximal number of prechecked operations to keep. The mempool only
            keeps the [max_prechecked_manager_operations] operations with the
            highest fee/gas and fee/size ratios. *)
}

let default_minimal_fees =
  match Tez.of_mutez 100L with None -> assert false | Some t -> t

let default_minimal_nanotez_per_gas_unit = Q.of_int 100

let default_minimal_nanotez_per_byte = Q.of_int 1000

let quota = Main.validation_passes

let managers_index = 3 (* in Main.validation_passes *)

let managers_quota = Stdlib.List.nth quota managers_index

(* If the drift is not specified, it will be the duration of round zero.
   It allows only to spam with one future round.

   /!\ Warning /!\ : current plugin implementation implies that this drift
   cumulates with the accepted  drift regarding the current head's timestamp.
*)
let default_config =
  {
    minimal_fees = default_minimal_fees;
    minimal_nanotez_per_gas_unit = default_minimal_nanotez_per_gas_unit;
    minimal_nanotez_per_byte = default_minimal_nanotez_per_byte;
    allow_script_failure = true;
    clock_drift = None;
    replace_by_fee_factor =
      Q.make (Z.of_int 105) (Z.of_int 100)
      (* Default value of [replace_by_fee_factor] is set to 5% *);
    max_prechecked_manager_operations = 5_000;
  }

let config_encoding : config Data_encoding.t =
  let open Data_encoding in
  conv
    (fun {
           minimal_fees;
           minimal_nanotez_per_gas_unit;
           minimal_nanotez_per_byte;
           allow_script_failure;
           clock_drift;
           replace_by_fee_factor;
           max_prechecked_manager_operations;
         } ->
      ( minimal_fees,
        minimal_nanotez_per_gas_unit,
        minimal_nanotez_per_byte,
        allow_script_failure,
        clock_drift,
        replace_by_fee_factor,
        max_prechecked_manager_operations ))
    (fun ( minimal_fees,
           minimal_nanotez_per_gas_unit,
           minimal_nanotez_per_byte,
           allow_script_failure,
           clock_drift,
           replace_by_fee_factor,
           max_prechecked_manager_operations ) ->
      {
        minimal_fees;
        minimal_nanotez_per_gas_unit;
        minimal_nanotez_per_byte;
        allow_script_failure;
        clock_drift;
        replace_by_fee_factor;
        max_prechecked_manager_operations;
      })
    (obj7
       (dft "minimal_fees" Tez.encoding default_config.minimal_fees)
       (dft
          "minimal_nanotez_per_gas_unit"
          nanotez_enc
          default_config.minimal_nanotez_per_gas_unit)
       (dft
          "minimal_nanotez_per_byte"
          nanotez_enc
          default_config.minimal_nanotez_per_byte)
       (dft "allow_script_failure" bool default_config.allow_script_failure)
       (opt "clock_drift" Period.encoding)
       (dft
          "replace_by_fee_factor"
          manager_op_replacement_factor_enc
          default_config.replace_by_fee_factor)
       (dft
          "max_prechecked_manager_operations"
          int31
          default_config.max_prechecked_manager_operations))

(** An Alpha_context manager operation, packed so that the type is not
    parametrized by ['kind]. *)
type manager_op = Manager_op : 'kind Kind.manager operation -> manager_op

(** Information stored for each prechecked manager operation.

    Note that this record does not include the operation hash because
    it is instead used as key in the map that stores this information
    in the [state] below. *)
type manager_op_info = {
  manager_op : manager_op;
      (** Used when we want to remove the operation with
          {!Validate.remove_manager_operation}. *)
  fee : Tez.t;
  gas_limit : Fixed_point_repr.integral_tag Gas.Arith.t;
      (** Both [fee] and [gas_limit] are used to determine whether a new
          operation from the same manager should replace this one. *)
  weight : Q.t;
      (** Used to update [ops_prechecked] and [min_prechecked_op_weight]
          in [state] when appropriate. *)
}

type manager_op_weight = {
  operation_hash : Tezos_crypto.Operation_hash.t;
  weight : Q.t;
}

(** Build a {!manager_op_weight} from operation hash and {!manager_op_info}. *)
let mk_op_weight oph (info : manager_op_info) =
  {operation_hash = oph; weight = info.weight}

let compare_manager_op_weight op1 op2 =
  let c = Q.compare op1.weight op2.weight in
  if c <> 0 then c
  else Tezos_crypto.Operation_hash.compare op1.operation_hash op2.operation_hash

module ManagerOpWeightSet = Set.Make (struct
  type t = manager_op_weight

  (* Sort by weight *)
  let compare = compare_manager_op_weight
end)

(** Static information to store in the filter state. *)
type state_info = {
  grandparent_level_start : Timestamp.t;
  round_zero_duration : Period.t;
  proposal_round : Round.t;
  alpha_ctxt : Alpha_context.t;
      (** Protocol context at the initialization of the mempool filter.
          Note that it never gets updated. *)
}

type state = {
  state_info : state_info option;
  prechecked_manager_op_count : int;
      (** Number of prechecked manager operations.
          Invariants:
          - [prechecked_manager_op_count
               = Tezos_crypto.Operation_hash.Map.cardinal prechecked_manager_ops
               = ManagerOpWeightSet.cardinal prechecked_op_weights]
          - [prechecked_manager_op_count <= max_prechecked_manager_operations] *)
  prechecked_manager_ops : manager_op_info Tezos_crypto.Operation_hash.Map.t;
      (** All prechecked manager operations. See {!manager_op_info}. *)
  prechecked_op_weights : ManagerOpWeightSet.t;
      (** The {!manager_op_weight} of all prechecked manager operations. *)
  min_prechecked_op_weight : manager_op_weight option;
      (** The prechecked operation in [op_prechecked_managers], if any, with
            the minimal weight.
            Invariant:
            - [min_prechecked_op_weight = min { x | x \in prechecked_op_weights }] *)
}

let empty : state =
  {
    state_info = None;
    prechecked_manager_op_count = 0;
    prechecked_manager_ops = Tezos_crypto.Operation_hash.Map.empty;
    prechecked_op_weights = ManagerOpWeightSet.empty;
    min_prechecked_op_weight = None;
  }

let init config ?(validation_state : validation_state option) ~predecessor () =
  ignore config ;
  (match validation_state with
  | None -> return empty
  | Some validation_state ->
      let ctxt = Validate.get_initial_ctxt validation_state in
      let {
        Tezos_base.Block_header.fitness = predecessor_fitness;
        timestamp = predecessor_timestamp;
        _;
      } =
        predecessor.Tezos_base.Block_header.shell
      in
      Alpha_context.Fitness.predecessor_round_from_raw predecessor_fitness
      >>?= fun grandparent_round ->
      Alpha_context.Fitness.round_from_raw predecessor_fitness
      >>?= fun predecessor_round ->
      let round_durations = Constants.round_durations ctxt in
      let round_zero_duration =
        Round.round_duration round_durations Round.zero
      in
      Round.level_offset_of_round
        round_durations
        ~round:Round.(succ grandparent_round)
      >>?= fun proposal_level_offset ->
      Round.level_offset_of_round round_durations ~round:predecessor_round
      >>?= fun proposal_round_offset ->
      Period.(add proposal_level_offset proposal_round_offset)
      >>?= fun proposal_offset ->
      let state_info =
        {
          grandparent_level_start =
            Timestamp.(predecessor_timestamp - proposal_offset);
          round_zero_duration;
          proposal_round = predecessor_round;
          alpha_ctxt = ctxt;
        }
      in
      return {empty with state_info = Some state_info})
  >|= Environment.wrap_tzresult

let manager_prio p = `Low p

let consensus_prio = `High

let other_prio = `Medium

let on_flush config filter_state ?(validation_state : validation_state option)
    ~predecessor () =
  ignore (filter_state : state) ;
  init config ?validation_state ~predecessor ()

let get_manager_operation_gas_and_fee contents =
  let open Operation in
  let l = to_list (Contents_list contents) in
  List.fold_left
    (fun acc -> function
      | Contents (Manager_operation {fee; gas_limit; _}) -> (
          match acc with
          | Error _ as e -> e
          | Ok (total_fee, total_gas) -> (
              match Tez.(total_fee +? fee) with
              | Ok total_fee -> Ok (total_fee, Gas.Arith.add total_gas gas_limit)
              | Error _ as e -> e))
      | _ -> acc)
    (Ok (Tez.zero, Gas.Arith.zero))
    l

type Environment.Error_monad.error += Fees_too_low

let () =
  Environment.Error_monad.register_error_kind
    `Permanent
    ~id:"prefilter.fees_too_low"
    ~title:"Operation fees are too low"
    ~description:"Operation fees are too low"
    ~pp:(fun ppf () -> Format.fprintf ppf "Operation fees are too low")
    Data_encoding.unit
    (function Fees_too_low -> Some () | _ -> None)
    (fun () -> Fees_too_low)

type Environment.Error_monad.error +=
  | Manager_restriction of {oph : Tezos_crypto.Operation_hash.t; fee : Tez.t}

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"prefilter.manager_restriction"
    ~title:"Only one manager operation per manager per block allowed"
    ~description:"Only one manager operation per manager per block allowed"
    ~pp:(fun ppf (oph, fee) ->
      Format.fprintf
        ppf
        "Only one manager operation per manager per block allowed (found %a \
         with %atez fee. You may want to use --replace to provide adequate fee \
         and replace it)."
        Tezos_crypto.Operation_hash.pp
        oph
        Tez.pp
        fee)
    Data_encoding.(
      obj2
        (req "operation_hash" Tezos_crypto.Operation_hash.encoding)
        (req "operation_fee" Tez.encoding))
    (function Manager_restriction {oph; fee} -> Some (oph, fee) | _ -> None)
    (fun (oph, fee) -> Manager_restriction {oph; fee})

type Environment.Error_monad.error +=
  | Manager_operation_replaced of {
      old_hash : Tezos_crypto.Operation_hash.t;
      new_hash : Tezos_crypto.Operation_hash.t;
    }

let () =
  Environment.Error_monad.register_error_kind
    `Permanent
    ~id:"plugin.manager_operation_replaced"
    ~title:"Manager operation replaced"
    ~description:"The manager operation has been replaced"
    ~pp:(fun ppf (old_hash, new_hash) ->
      Format.fprintf
        ppf
        "The manager operation %a has been replaced with %a"
        Tezos_crypto.Operation_hash.pp
        old_hash
        Tezos_crypto.Operation_hash.pp
        new_hash)
    (Data_encoding.obj2
       (Data_encoding.req "old_hash" Tezos_crypto.Operation_hash.encoding)
       (Data_encoding.req "new_hash" Tezos_crypto.Operation_hash.encoding))
    (function
      | Manager_operation_replaced {old_hash; new_hash} ->
          Some (old_hash, new_hash)
      | _ -> None)
    (fun (old_hash, new_hash) ->
      Manager_operation_replaced {old_hash; new_hash})

type Environment.Error_monad.error += Fees_too_low_for_mempool of Tez.t

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"prefilter.fees_too_low_for_mempool"
    ~title:"Operation fees are too low to be considered in full mempool"
    ~description:"Operation fees are too low to be considered in full mempool"
    ~pp:(fun ppf required_fees ->
      Format.fprintf
        ppf
        "The mempool is full, the number of prechecked manager operations has \
         reached the limit max_prechecked_manager_operations set by the \
         filter. Increase operation fees to at least %atz for the operation to \
         be considered and propagated by THIS node. Note that the operations \
         with the minimum fees in the mempool risk being removed if better \
         ones are received."
        Tez.pp
        required_fees)
    Data_encoding.(obj1 (req "required_fees" Tez.encoding))
    (function
      | Fees_too_low_for_mempool required_fees -> Some required_fees | _ -> None)
    (fun required_fees -> Fees_too_low_for_mempool required_fees)

type Environment.Error_monad.error += Removed_fees_too_low_for_mempool

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"plugin.removed_fees_too_low_for_mempool"
    ~title:"Operation removed because fees are too low for full mempool"
    ~description:"Operation removed because fees are too low for full mempool"
    ~pp:(fun ppf () ->
      Format.fprintf
        ppf
        "The mempool is full, the number of prechecked manager operations has \
         reached the limit max_prechecked_manager_operations set by the \
         filter. Operation was removed because another operation with a better \
         fees/gas-size ratio was received and accepted by the mempool.")
    Data_encoding.unit
    (function Removed_fees_too_low_for_mempool -> Some () | _ -> None)
    (fun () -> Removed_fees_too_low_for_mempool)

(* TODO: https://gitlab.com/tezos/tezos/-/issues/2238
   Write unit tests for the feature 'replace-by-fee' and for other changes
   introduced by other MRs in the plugin. *)
(* In order to decide if the new operation can replace an old one from the
   same manager, we check if its fees (resp. fees/gas ratio) are greater than
   (or equal to) the old operations's fees (resp. fees/gas ratio), bumped by
   the factor [config.replace_by_fee_factor].
*)
let better_fees_and_ratio =
  let bump config q = Q.mul q config.replace_by_fee_factor in
  fun config old_gas old_fee new_gas new_fee ->
    let old_fee = Tez.to_mutez old_fee |> Z.of_int64 |> Q.of_bigint in
    let old_gas = Gas.Arith.integral_to_z old_gas |> Q.of_bigint in
    let new_fee = Tez.to_mutez new_fee |> Z.of_int64 |> Q.of_bigint in
    let new_gas = Gas.Arith.integral_to_z new_gas |> Q.of_bigint in
    let old_ratio = Q.div old_fee old_gas in
    let new_ratio = Q.div new_fee new_gas in
    Q.compare new_ratio (bump config old_ratio) >= 0
    && Q.compare new_fee (bump config old_fee) >= 0

let size_of_operation op =
  (WithExceptions.Option.get ~loc:__LOC__
  @@ Data_encoding.Binary.fixed_length
       Tezos_base.Operation.shell_header_encoding)
  + Data_encoding.Binary.length Operation.protocol_data_encoding op

(** Returns the weight and resources consumption of an operation. The weight
      corresponds to the one implemented by the baker, to decide which operations
      to put in a block first (the code is largely duplicated).
      See {!Tezos_baking_alpha.Operation_selection.weight_manager} *)
let weight_and_resources_manager_operation ~validation_state ?size ~fee ~gas op
    =
  let hard_gas_limit_per_block =
    Constants.hard_gas_limit_per_block
      (Validate.get_initial_ctxt validation_state)
  in
  let max_size = managers_quota.max_size in
  let size = match size with None -> size_of_operation op | Some s -> s in
  let size_f = Q.of_int size in
  let gas_f = Q.of_bigint (Gas.Arith.integral_to_z gas) in
  let fee_f = Q.of_int64 (Tez.to_mutez fee) in
  let size_ratio = Q.(size_f / Q.of_int max_size) in
  let gas_ratio =
    Q.(gas_f / Q.of_bigint (Gas.Arith.integral_to_z hard_gas_limit_per_block))
  in
  let resources = Q.max size_ratio gas_ratio in
  (Q.(fee_f / resources), resources)

(** Returns the weight of an operation, i.e. the fees w.r.t the gas and size
      consumption in the block. *)
let weight_manager_operation ~validation_state ?size ~fee ~gas op =
  let weight, _resources =
    weight_and_resources_manager_operation ~validation_state ?size ~fee ~gas op
  in
  weight

(** Return fee for an operation that consumes [op_resources] for its weight to
      be strictly greater than [min_weight]. *)
let required_fee_manager_operation_weight ~op_resources ~min_weight =
  let req_mutez_q = Q.((min_weight * op_resources) + Q.one) in
  Tez.of_mutez_exn @@ Q.to_int64 req_mutez_q

(** Check if an operation as a weight (fees w.r.t gas and size) large enough to
      be prechecked and return said weight. In the case where the prechecked
      mempool is full, return an error if the weight is too small, or return the
      operation to be replaced otherwise. *)
let check_minimal_weight ?validation_state config filter_state ~fee ~gas_limit
    op =
  match validation_state with
  | None -> `Weight_ok (`No_replace, [])
  | Some validation_state -> (
      let weight, op_resources =
        weight_and_resources_manager_operation
          ~validation_state
          ~fee
          ~gas:gas_limit
          op
      in
      if
        filter_state.prechecked_manager_op_count
        < config.max_prechecked_manager_operations
      then
        (* The precheck mempool is not full yet *)
        `Weight_ok (`No_replace, [weight])
      else
        match filter_state.min_prechecked_op_weight with
        | None ->
            (* The precheck mempool is empty *)
            `Weight_ok (`No_replace, [weight])
        | Some {weight = min_weight; operation_hash = min_oph} ->
            if Q.(weight > min_weight) then
              (* The operation has a weight greater than the minimal
                 prechecked operation, replace the latest with the new one *)
              `Weight_ok (`Replace min_oph, [weight])
            else
              (* Otherwise fail and give indication as to what to fee should
                 be for the operation to be prechecked *)
              let required_fee =
                required_fee_manager_operation_weight ~op_resources ~min_weight
              in
              `Fail
                (`Branch_delayed
                  [
                    Environment.wrap_tzerror
                      (Fees_too_low_for_mempool required_fee);
                  ]))

let pre_filter_manager :
    type t.
    config ->
    state ->
    validation_state_before:validation_state option ->
    Operation.packed_protocol_data ->
    t Kind.manager contents_list ->
    [ `Passed_prefilter of Q.t list
    | `Branch_refused of tztrace
    | `Branch_delayed of tztrace
    | `Refused of tztrace
    | `Outdated of tztrace ] =
 fun config filter_state ~validation_state_before packed_op op ->
  let size = size_of_operation packed_op in
  let check_gas_and_fee fee gas_limit =
    let fees_in_nanotez =
      Q.mul (Q.of_int64 (Tez.to_mutez fee)) (Q.of_int 1000)
    in
    let minimal_fees_in_nanotez =
      Q.mul (Q.of_int64 (Tez.to_mutez config.minimal_fees)) (Q.of_int 1000)
    in
    let minimal_fees_for_gas_in_nanotez =
      Q.mul
        config.minimal_nanotez_per_gas_unit
        (Q.of_bigint @@ Gas.Arith.integral_to_z gas_limit)
    in
    let minimal_fees_for_size_in_nanotez =
      Q.mul config.minimal_nanotez_per_byte (Q.of_int size)
    in
    if
      Q.compare
        fees_in_nanotez
        (Q.add
           minimal_fees_in_nanotez
           (Q.add
              minimal_fees_for_gas_in_nanotez
              minimal_fees_for_size_in_nanotez))
      >= 0
    then `Fees_ok
    else `Refused [Environment.wrap_tzerror Fees_too_low]
  in
  match get_manager_operation_gas_and_fee op with
  | Error err -> `Refused (Environment.wrap_tztrace err)
  | Ok (fee, gas_limit) -> (
      match check_gas_and_fee fee gas_limit with
      | `Refused _ as err -> err
      | `Fees_ok -> (
          match
            check_minimal_weight
              ?validation_state:validation_state_before
              config
              filter_state
              ~fee
              ~gas_limit
              packed_op
          with
          | `Fail errs -> errs
          | `Weight_ok (_, weight) -> `Passed_prefilter weight))

type Environment.Error_monad.error += Wrong_operation

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"prefilter.wrong_operation"
    ~title:"Wrong operation"
    ~description:"Failing_noop operations are not accepted in the mempool."
    ~pp:(fun ppf () ->
      Format.fprintf
        ppf
        "Failing_noop operations are not accepted in the mempool")
    Data_encoding.unit
    (function Wrong_operation -> Some () | _ -> None)
    (fun () -> Wrong_operation)

type Environment.Error_monad.error += Consensus_operation_in_far_future

let () =
  Environment.Error_monad.register_error_kind
    `Branch
    ~id:"prefilter.Consensus_operation_in_far_future"
    ~title:"Consensus operation in far future"
    ~description:"Consensus operation too far in the future are not accepted."
    ~pp:(fun ppf () ->
      Format.fprintf
        ppf
        "Consensus operation too far in the future are not accepted.")
    Data_encoding.unit
    (function Consensus_operation_in_far_future -> Some () | _ -> None)
    (fun () -> Consensus_operation_in_far_future)

(** {2 consensus operation filtering}

     In Tenderbake, we increased a lot the number of consensus
      operations, therefore it seems necessary to be able to filter consensus
     operations that could be produced by a Byzantine baker mis-using
     its right to produce operations in future rounds or levels.

      We consider the situation where the head is at level [h_l],
     round [h_r], and with timestamp [h_ts], with the predecessor of the head
     being at round [hp_r].
      We receive at a time [now] a consensus operation for level [op_l] and
     round [op_r].

       A consensus operation is considered too far in the future, and therefore filtered,
      if the earliest possible starting time of its round is greater than the
      current time plus a safety margin of [config.clock_drift].

      To consider potential level 2 reorgs, we first compute the expected
      timestamp of round zero at previous level [hp0_ts],

      All ops at level p_l and round r' such that time(r') is greater than (now + drift) are
     deemed too far in the future:

                  h_r                          op_ts    now+drift     (h_l,r')
     hp0_ts h_0   h_l                            |        |              |
        +----+-----+---------+-------------------+--+-----+--------------+-----------
             |     |         |                   |  |     |              |
             |    h_ts     h_r end time          | now    |        earliest expected
             |     |                             |        |        time of round r'
             |<----op_r rounds duration -------->|        |
                   |
                   |<--------------- operations kept ---->|<-rejected----------...
                   |
                   |<-----------operations considered by the filter -----------...

    For an operation on a proposal at the next level, we consider the minimum
    starting time of the operation's round, obtained by assuming that the proposal
    at the next level was built on top of a proposal at round 0 for the current
    level, itself based on a proposal at round 0 of previous level.
    Operations on proposal with higher levels are treated similarly.

    All ops at the next level and round r' such that timestamp(r') > now+drift
    are deemed too far in the future.

                r=0     r=1   h_r      now     now+drift   (h_l+1,r')
   hp0_ts h_0   h_l           h_l       |          |          |
      +----+---- |-------+----+---------+----------+----------+----------
           |     |       |    |                               |
           |     t0      |   h_ts                      earliest expected
           |     |       |    |                         time of round r'
           |<--- |    earliest|                               |
                 |  next level|                               |
                 |       |<---------------------------------->|
                                  round_offset(r')

  *)

(** At a given level a consensus operation is acceptable if its earliest
      expected timestamp, [op_earliest_ts] is below the current clock with an
      accepted drift for the clock given by a configuration.  *)
let acceptable ~drift ~op_earliest_ts ~now_timestamp =
  Timestamp.(
    now_timestamp +? drift >|? fun now_drifted -> op_earliest_ts <= now_drifted)

(** Check that an operation with the given [op_round], at level [op_level]
      is likely to be correct, meaning it could have been produced before
      now (+ the safety margin from configuration).

      Given an operation at level greater or equal than/to the current level, we
      compute the expected timestamp of the operation's round. If the operation
      is at a greater level, we assume that it is based on the proposal at round
      zero of the current level.

      All operations whose (level, round) is lower than or equal to the current
      head are deemed valid.
      Note that in case where their is a high drift in the computer clock, they
      might not have been considered valid by comparing their expected timestamp
      to the clock.

      This is a stricter than necessary filter as it will reject operations that
      could be valid in the current timeframe if the proposal they endorse is
      built over a predecessor of the current proposal that would be of lower
      round than the current one.

      What can we do that would be smarter: get current head's predecessor round
      and timestamp to compute the timestamp t0 of a predecessor that would have
      been proposed at round 0.

      Timestamp of round at current level for an alternative head that would be
      based on such proposal would be computed based on t0.
      For level higher than current head, compute the round's earliest timestamp
      if all proposal passed at round 0 starting from t0.
  *)
let acceptable_op ~config ~round_durations ~round_zero_duration ~proposal_level
    ~proposal_round ~proposal_timestamp
    ~(proposal_predecessor_level_start : Timestamp.t) ~op_level ~op_round
    ~now_timestamp =
  if
    Raw_level.(succ op_level < proposal_level)
    || (op_level = proposal_level && op_round <= proposal_round)
  then
    (* Past and current round operations are not in the future *)
    (* This case could be handled directly in `pre_filter_far_future_consensus_ops`
       for a (slightly) better performance. *)
    Ok true
  else
    (* If, by some tolerance on local clock drift, the timestamp of the
       current head is itself in the future, we use this time instead of
       now_timestamp *)
    let now_timestamp = Timestamp.(max now_timestamp proposal_timestamp) in
    (* Computing when the current level started. *)
    let drift = Option.value ~default:round_zero_duration config.clock_drift in
    (* We compute the earliest timestamp possible [op_earliest_ts] for the
       operation's (level,round), as if all proposals were accepted at round 0
       since the previous level. *)
    (* Invariant: [op_level + 1 >= proposal_level] *)
    let level_offset = Raw_level.(diff (succ op_level) proposal_level) in
    Period.mult level_offset round_zero_duration >>? fun time_shift ->
    Timestamp.(proposal_predecessor_level_start +? time_shift)
    >>? fun earliest_op_level_start ->
    (* computing the operations's round start from it's earliest
       possible level start *)
    Round.timestamp_of_another_round_same_level
      round_durations
      ~current_round:Round.zero
      ~current_timestamp:earliest_op_level_start
      ~considered_round:op_round
    >>? fun op_earliest_ts ->
    (* We finally check that the expected time of the operation is
       acceptable *)
    acceptable ~drift ~op_earliest_ts ~now_timestamp

let pre_filter_far_future_consensus_ops config ~filter_state
    ({level = op_level; round = op_round; _} : consensus_content) : bool Lwt.t =
  match filter_state.state_info with
  | None -> Lwt.return_true
  | Some state_info -> (
      (let proposal_timestamp = Timestamp.predecessor state_info.alpha_ctxt in
       let now_timestamp = Time.System.now () |> Time.System.to_protocol in
       let Level.{level; _} = Level.current state_info.alpha_ctxt in
       let proposal_level =
         match Raw_level.pred level with
         | None ->
             (* mempool level is set to the successor of the current head *)
             assert false
         | Some proposal_level -> proposal_level
       in
       let round_durations = Constants.round_durations state_info.alpha_ctxt in
       Lwt.return
       @@ acceptable_op
            ~config
            ~round_durations
            ~round_zero_duration:state_info.round_zero_duration
            ~proposal_level
            ~proposal_round:state_info.proposal_round
            ~proposal_timestamp
            ~proposal_predecessor_level_start:state_info.grandparent_level_start
            ~op_level
            ~op_round
            ~now_timestamp)
      >>= function
      | Ok b -> Lwt.return b
      | _ -> Lwt.return_false)

(** A quasi infinite amount of "valid" (pre)endorsements could be
      sent by a committee member, one for each possible round number.

      This filter rejects (pre)endorsements that refer to a round
      that could not have been reached within the time span between
      the last head's timestamp and the current local clock.

      We add [config.clock_drift] time as a safety margin.
  *)
let pre_filter config ~(filter_state : state) ?validation_state_before
    ({shell = _; protocol_data = Operation_data {contents; _} as op} :
      Main.operation) =
  let prefilter_manager_op manager_op =
    Lwt.return
    @@
    match
      pre_filter_manager
        config
        filter_state
        ~validation_state_before
        op
        manager_op
    with
    | `Passed_prefilter prio -> `Passed_prefilter (manager_prio prio)
    | (`Branch_refused _ | `Branch_delayed _ | `Refused _ | `Outdated _) as err
      ->
        err
  in
  match contents with
  | Single (Failing_noop _) ->
      Lwt.return (`Refused [Environment.wrap_tzerror Wrong_operation])
  | Single (Preendorsement consensus_content)
  | Single (Endorsement consensus_content) ->
      pre_filter_far_future_consensus_ops ~filter_state config consensus_content
      >>= fun keep ->
      if keep then Lwt.return @@ `Passed_prefilter consensus_prio
      else
        Lwt.return
          (`Branch_refused
            [Environment.wrap_tzerror Consensus_operation_in_far_future])
  | Single (Dal_slot_availability _)
  | Single (Seed_nonce_revelation _)
  | Single (Double_preendorsement_evidence _)
  | Single (Double_endorsement_evidence _)
  | Single (Double_baking_evidence _)
  | Single (Activate_account _)
  | Single (Proposals _)
  | Single (Vdf_revelation _)
  | Single (Drain_delegate _)
  | Single (Ballot _) ->
      Lwt.return @@ `Passed_prefilter other_prio
  | Single (Manager_operation _) as op -> prefilter_manager_op op
  | Cons (Manager_operation _, _) as op -> prefilter_manager_op op

(** Call the protocol's {!Validate.validate_operation} and
    return either:

    - the updated {!validation_state} when the validation is
      successful, or

    - the protocol error trace converted to an [error trace], together
      with the corresponding {!error_classification}.

    The signature check is skipped when the operation has previously
    been validated successfully, ie. [nb_successful_prechecks > 0]. *)
let proto_validate_operation validation_state oph ~nb_successful_prechecks
    (operation : packed_operation) :
    (validation_state, error trace * error_classification) result Lwt.t =
  let open Lwt_result_syntax in
  let*! res =
    Validate.validate_operation
      ~check_signature:(nb_successful_prechecks <= 0)
      validation_state
      oph
      operation
  in
  match res with
  | Ok validation_state -> return validation_state
  | Error tztrace ->
      let err = Environment.wrap_tztrace tztrace in
      let error_classification =
        match classify_trace err with
        | Branch -> `Branch_refused err
        | Permanent -> `Refused err
        | Temporary -> `Branch_delayed err
        | Outdated -> `Outdated err
      in
      fail (err, error_classification)

(** Call the protocol's {!Validate.validate_operation} on a
    manager operation and return:

    - [`Success] containing the updated [validation_state] when the
      validation is successful;

    - [`Conflict] containing the hash of the conflicting operation,
      and the {!error_classification} corresponding to the protocol error
      trace, when the validation fails because of the
      one-manager-operation-per-manager-per-block restriction;

    - an error containing the relevant {!error_classification} when
      the validation fails with any other protocol error.

    The signature check is skipped when the operation has previously
    been validated successfully, ie. [nb_successful_prechecks > 0]. *)
let proto_validate_manager_operation validation_state oph
    ~nb_successful_prechecks
    (operation : 'a Kind.manager Alpha_context.operation) :
    ( [> `Success of validation_state
      | `Conflict of Tezos_crypto.Operation_hash.t * error_classification ],
      error_classification )
    result
    Lwt.t =
  let open Lwt_result_syntax in
  let*! res =
    proto_validate_operation
      validation_state
      oph
      ~nb_successful_prechecks
      (Operation.pack operation)
  in
  match res with
  | Ok validation_state -> return (`Success validation_state)
  | Error (err, error_classification) -> (
      match err with
      | Environment.Ecoproto_error
          (Validate_errors.Manager.Manager_restriction
            {
              source = _manager;
              conflict = Operation_conflict {existing; new_operation = _};
            })
        :: _ ->
          return (`Conflict (existing, error_classification))
      | _ -> fail error_classification)

(** Remove a manager operation from the protocol's [validation_state]. *)
let remove_from_validation_state validation_state (Manager_op op) =
  let operation_state =
    Validate.remove_operation validation_state.Validate.operation_state op
  in
  {validation_state with operation_state}

(** Call the protocol validation on a manager operation and handle
    potential conflicts: if either the 1M restriction is triggered or
    the mempool exceeds the maximal number of prechecked operations,
    then this function is responsible for either discarding the new
    operation, or removing an old operation to free up space for the
    new operation.

    Return the updated protocol [validation_state] and, when
    applicable, the replaced operation accompanied by its new
    classification.

    Note that this function does not handle the update of the
    [filter_state]. *)
let validate_manager_operation_and_handle_conflicts config filter_state
    validation_state oph ~nb_successful_prechecks fee gas_limit
    (operation : 'manager_kind Kind.manager operation) :
    ( validation_state
      * [ `No_replace
        | `Replace of Tezos_crypto.Operation_hash.t * error_classification ],
      error_classification )
    result
    Lwt.t =
  let open Lwt_result_syntax in
  let* proto_validation_outcome =
    proto_validate_manager_operation
      validation_state
      oph
      ~nb_successful_prechecks
      operation
  in
  match proto_validation_outcome with
  | `Success validation_state -> (
      (* The operation has been successfully validated and there is no
         1M conflict. We now need to ensure that the mempool does not
         exceed its maximal number of prechecked manager operations. *)
      match
        check_minimal_weight
          ~validation_state
          config
          filter_state
          ~fee
          ~gas_limit
          (Operation_data operation.protocol_data)
      with
      | `Weight_ok (`No_replace, _weight) ->
          (* The mempool is not full: no need to replace any operation. *)
          return (validation_state, `No_replace)
      | `Weight_ok (`Replace min_weight_oph, _weight) -> (
          (* The mempool is full yet the new operation has enough weight
             to be included: the old operation with the lowest weight is
             reclassified as [Branch_delayed]. *)
          (* TODO: https://gitlab.com/tezos/tezos/-/issues/2347 The
             branch_delayed ring is bounded to 1000, so we may loose
             operations. We can probably do better. *)
          match
            Tezos_crypto.Operation_hash.Map.find
              min_weight_oph
              filter_state.prechecked_manager_ops
          with
          | None ->
              (* This only occurs for a [Drain_delegate]
                 operation: it has a higher priority than a manager
                 therefore we keep the drain delegate *)
              return (validation_state, `No_replace)
          | Some {manager_op; _} ->
              let validation_state =
                remove_from_validation_state validation_state manager_op
              in
              let replace_err =
                Environment.wrap_tzerror Removed_fees_too_low_for_mempool
              in
              let replacement =
                `Replace (min_weight_oph, `Branch_delayed [replace_err])
              in
              return (validation_state, replacement))
      | `Fail err ->
          (* The mempool is full and the weight of the new operation is
             too low: raise the error returned by {!check_minimal_weight}. *)
          fail err)
  | `Conflict (old_oph, _proto_error) -> (
      (* The protocol [validation_state] already contains an operation
         from the same manager. We look at the fees and gas limits of
         both operations to decide whether to replace the old one. *)
      match
        Tezos_crypto.Operation_hash.Map.find
          old_oph
          filter_state.prechecked_manager_ops
      with
      | None ->
          (* This only occurs for a [Drain_delegate] operation: it has
             a higher priority than a manager therefore we keep the
             drain delegate *)
          return (validation_state, `No_replace)
      | Some old_info ->
          if
            better_fees_and_ratio
              config
              old_info.gas_limit
              old_info.fee
              gas_limit
              fee
          then
            (* The new operation is better and replaces the old one from
               the same manager. Note that there is no need to check the
               number of prechecked operations in the mempool
               here. Indeed, the removal of the old operation frees up a
               spot in the mempool anyway. *)
            let validation_state =
              remove_from_validation_state validation_state old_info.manager_op
            in
            let* proto_validation_outcome2 =
              proto_validate_manager_operation
                validation_state
                oph
                ~nb_successful_prechecks
                operation
            in
            match proto_validation_outcome2 with
            | `Success validation_state ->
                let replace_err =
                  Environment.wrap_tzerror
                    (Manager_operation_replaced
                       {old_hash = old_oph; new_hash = oph})
                in
                let replacement = `Replace (old_oph, `Outdated [replace_err]) in
                return (validation_state, replacement)
            | `Conflict (_oph, conflict_proto_error) ->
                (* This should not happen: a manager operation should not
                   conflict with multiple operations. *)
                fail conflict_proto_error
          else
            (* The new operation is not interesting enough so it is rejected. *)
            let err = Manager_restriction {oph = old_oph; fee = old_info.fee} in
            fail (`Branch_delayed [Environment.wrap_tzerror err]))

(** Remove a manager operation hash from the filter state.
    Do nothing if the operation was not in the state. *)
let remove ~filter_state oph =
  match
    Tezos_crypto.Operation_hash.Map.find oph filter_state.prechecked_manager_ops
  with
  | None ->
      (* Not present in the filter_state: nothing to do. *)
      filter_state
  | Some info ->
      let prechecked_manager_ops =
        Tezos_crypto.Operation_hash.Map.remove
          oph
          filter_state.prechecked_manager_ops
      in
      let prechecked_manager_op_count =
        filter_state.prechecked_manager_op_count - 1
      in
      let prechecked_op_weights =
        ManagerOpWeightSet.remove
          (mk_op_weight oph info)
          filter_state.prechecked_op_weights
      in
      let min_prechecked_op_weight =
        match filter_state.min_prechecked_op_weight with
        | None -> None
        | Some min_op_weight ->
            if
              Tezos_crypto.Operation_hash.equal min_op_weight.operation_hash oph
            then ManagerOpWeightSet.min_elt prechecked_op_weights
            else Some min_op_weight
      in
      {
        filter_state with
        prechecked_manager_op_count;
        prechecked_manager_ops;
        prechecked_op_weights;
        min_prechecked_op_weight;
      }

(** Add a manager operation hash and information to the filter state.
    Do nothing if the operation is already present in the state. *)
let add_manager_op filter_state oph info replacement =
  let filter_state =
    match replacement with
    | `No_replace -> filter_state
    | `Replace (oph, _classification) -> remove ~filter_state oph
  in
  if Tezos_crypto.Operation_hash.Map.mem oph filter_state.prechecked_manager_ops
  then (* Already present in the filter_state: nothing to do. *)
    filter_state
  else
    let prechecked_manager_op_count =
      filter_state.prechecked_manager_op_count + 1
    in
    let prechecked_manager_ops =
      Tezos_crypto.Operation_hash.Map.add
        oph
        info
        filter_state.prechecked_manager_ops
    in
    let op_weight = mk_op_weight oph info in
    let prechecked_op_weights =
      ManagerOpWeightSet.add op_weight filter_state.prechecked_op_weights
    in
    let min_prechecked_op_weight =
      match filter_state.min_prechecked_op_weight with
      | Some old_min when compare_manager_op_weight old_min op_weight <= 0 ->
          Some old_min
      | _ -> Some op_weight
    in
    {
      filter_state with
      prechecked_manager_op_count;
      prechecked_manager_ops;
      prechecked_op_weights;
      min_prechecked_op_weight;
    }

(** Call {!validate_manager_operation_and_handle_conflicts} then
    update the [filter_state] by adding the newly validated operation,
    and removing the replaced one one when applicable.

    Return either the updated [filter_state], updated
    [validation_state], and operation replacement, or an error
    containing the appropriate classification. *)
let precheck_manager_result config filter_state validation_state oph
    ~nb_successful_prechecks (operation : 'manager_kind Kind.manager operation)
    :
    ( state
      * validation_state
      * [ `No_replace
        | `Replace of Tezos_crypto.Operation_hash.t * error_classification ],
      error_classification )
    result
    Lwt.t =
  let open Lwt_result_syntax in
  let*? fee, gas_limit =
    Result.map_error
      (fun err -> `Refused (Environment.wrap_tztrace err))
      (get_manager_operation_gas_and_fee operation.protocol_data.contents)
  in
  let* validation_state, replacement =
    validate_manager_operation_and_handle_conflicts
      config
      filter_state
      validation_state
      oph
      ~nb_successful_prechecks
      fee
      gas_limit
      operation
  in
  let weight =
    weight_manager_operation
      ~validation_state
      ~fee
      ~gas:gas_limit
      (Operation_data operation.protocol_data)
  in
  let info = {manager_op = Manager_op operation; gas_limit; fee; weight} in
  let filter_state = add_manager_op filter_state oph info replacement in
  return (filter_state, validation_state, replacement)

(** Call {!precheck_manager_result} then convert its error monad
    result into the appropriate return type for [precheck]. *)
let precheck_manager config filter_state validation_state oph
    ~nb_successful_prechecks operation :
    [> `Passed_precheck of
       state
       * validation_state
       * [ `No_replace
         | `Replace of Tezos_crypto.Operation_hash.t * error_classification ]
    | error_classification ]
    Lwt.t =
  precheck_manager_result
    config
    filter_state
    validation_state
    oph
    ~nb_successful_prechecks
    operation
  >>= function
  | Ok (filter_state, validation_state, replacement) ->
      Lwt.return
        (`Passed_precheck (filter_state, validation_state, replacement))
  | Error
      ((`Refused _ | `Branch_delayed _ | `Branch_refused _ | `Outdated _) as
      err) ->
      Lwt.return err

(** Call the protocol's {!Validate.validate_operation}. If
    successful, return the updated [validation_state], the unchanged
    [filter_state], and no operation replacement. Otherwise, return the
    classification associated with the protocol error. Note that when
    there is a conflict with a previously validated operation, the new
    operation is always discarded. As it does not allow for any fee
    market, this function is designed for non-manager operations. *)
let precheck_non_manager filter_state validation_state oph
    ~nb_successful_prechecks operation =
  proto_validate_operation
    validation_state
    oph
    ~nb_successful_prechecks
    operation
  >>= function
  | Ok validation_state ->
      Lwt.return
        (`Passed_precheck (filter_state, validation_state, `No_replace))
  | Error
      ( _err,
        ((`Refused _ | `Branch_delayed _ | `Branch_refused _ | `Outdated _) as
        error_classification) ) ->
      Lwt.return error_classification

(* Now that [precheck] uses {!Validate.validate_operation}
   for every kind of operation, it must never return
   [`Undecided]. Indeed, this would cause the prevalidator to call
   {!Apply.apply_operation}, which relies on updates to the alpha
   context to detect incompatible operations, whereas
   [validate_operation] only updates the
   {!Validate.validate_operation_state}. Therefore, it would
   be possible for the mempool to accept conflicting operations. *)
let precheck :
    config ->
    filter_state:state ->
    validation_state:validation_state ->
    Tezos_crypto.Operation_hash.t ->
    Main.operation ->
    nb_successful_prechecks:int ->
    [ `Passed_precheck of
      state
      * validation_state
      * [ `No_replace
        | `Replace of Tezos_crypto.Operation_hash.t * error_classification ]
    | `Undecided
    | error_classification ]
    Lwt.t =
 fun config
     ~filter_state
     ~validation_state
     oph
     operation
     ~nb_successful_prechecks ->
  let {protocol_data = Operation_data protocol_data; _} = operation in
  let call_precheck_manager (protocol_data : _ Kind.manager protocol_data) =
    precheck_manager
      config
      filter_state
      validation_state
      oph
      ~nb_successful_prechecks
      {shell = operation.shell; protocol_data}
  in
  match protocol_data.contents with
  | Single (Manager_operation _) -> call_precheck_manager protocol_data
  | Cons (Manager_operation _, _) -> call_precheck_manager protocol_data
  | Single _ ->
      precheck_non_manager
        filter_state
        validation_state
        oph
        ~nb_successful_prechecks
        operation

open Apply_results

type Environment.Error_monad.error += Skipped_operation

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"postfilter.skipped_operation"
    ~title:"The operation has been skipped by the protocol"
    ~description:"The operation has been skipped by the protocol"
    ~pp:(fun ppf () ->
      Format.fprintf ppf "The operation has been skipped by the protocol")
    Data_encoding.unit
    (function Skipped_operation -> Some () | _ -> None)
    (fun () -> Skipped_operation)

type Environment.Error_monad.error += Backtracked_operation

let () =
  Environment.Error_monad.register_error_kind
    `Temporary
    ~id:"postfilter.backtracked_operation"
    ~title:"The operation has been backtracked by the protocol"
    ~description:"The operation has been backtracked by the protocol"
    ~pp:(fun ppf () ->
      Format.fprintf ppf "The operation has been backtracked by the protocol")
    Data_encoding.unit
    (function Backtracked_operation -> Some () | _ -> None)
    (fun () -> Backtracked_operation)

let rec post_filter_manager :
    type t.
    Alpha_context.t ->
    state ->
    t Kind.manager contents_result_list ->
    config ->
    [`Passed_postfilter of state | `Refused of tztrace] =
 fun ctxt filter_state result config ->
  (* TODO: https://gitlab.com/tezos/tezos/-/issues/2181
     This function should be unit tested.
     The errors that can be raised if allow_script_failure is enable should
     be tested. *)
  match result with
  | Single_result (Manager_operation_result {operation_result; _}) -> (
      let check_allow_script_failure errs =
        if config.allow_script_failure then `Passed_postfilter filter_state
        else `Refused errs
      in
      match operation_result with
      | Applied _ -> `Passed_postfilter filter_state
      | Skipped _ ->
          check_allow_script_failure
            [Environment.wrap_tzerror Skipped_operation]
      | Failed (_, errors) ->
          check_allow_script_failure (Environment.wrap_tztrace errors)
      | Backtracked (_, errors) ->
          check_allow_script_failure
            (match errors with
            | Some e -> Environment.wrap_tztrace e
            | None -> [Environment.wrap_tzerror Backtracked_operation]))
  | Cons_result (Manager_operation_result res, rest) -> (
      post_filter_manager
        ctxt
        filter_state
        (Single_result (Manager_operation_result res))
        config
      |> function
      | `Passed_postfilter filter_state ->
          post_filter_manager ctxt filter_state rest config
      | `Refused _ as errs -> errs)

let post_filter config ~(filter_state : state) ~validation_state_before:_
    ~validation_state_after (_op, receipt) =
  match receipt with
  | No_operation_metadata -> assert false (* only for multipass validator *)
  | Operation_metadata {contents} -> (
      let handle_manager result =
        let ctxt = Validate.get_initial_ctxt validation_state_after in
        Lwt.return (post_filter_manager ctxt filter_state result config)
      in
      match contents with
      | Single_result (Preendorsement_result _)
      | Single_result (Endorsement_result _)
      | Single_result (Dal_slot_availability_result _)
      | Single_result (Seed_nonce_revelation_result _)
      | Single_result (Double_preendorsement_evidence_result _)
      | Single_result (Double_endorsement_evidence_result _)
      | Single_result (Double_baking_evidence_result _)
      | Single_result (Activate_account_result _)
      | Single_result Proposals_result
      | Single_result (Vdf_revelation_result _)
      | Single_result (Drain_delegate_result _)
      | Single_result Ballot_result ->
          Lwt.return (`Passed_postfilter filter_state)
      | Single_result (Manager_operation_result _) as result ->
          handle_manager result
      | Cons_result (Manager_operation_result _, _) as result ->
          handle_manager result)
back to top