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 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;
clock_drift : Period.t option;
replace_by_fee_factor : Q.t;
(** Factor by which the fee and fee/gas ratio of an old operation in
the mempool are both multiplied to determine the values that a new
operation must exceed in order to replace the old operation. See
the [better_fees_and_ratio] function further below. *)
}
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 managers_quota =
Stdlib.List.nth Main.validation_passes Operation_repr.manager_pass
(* 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;
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% *);
}
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;
clock_drift;
replace_by_fee_factor;
} ->
( minimal_fees,
minimal_nanotez_per_gas_unit,
minimal_nanotez_per_byte,
clock_drift,
replace_by_fee_factor ))
(fun ( minimal_fees,
minimal_nanotez_per_gas_unit,
minimal_nanotez_per_byte,
clock_drift,
replace_by_fee_factor ) ->
{
minimal_fees;
minimal_nanotez_per_gas_unit;
minimal_nanotez_per_byte;
clock_drift;
replace_by_fee_factor;
})
(obj5
(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)
(opt "clock_drift" Period.encoding)
(dft
"replace_by_fee_factor"
manager_op_replacement_factor_enc
default_config.replace_by_fee_factor))
(** Static information to store in the filter state. *)
type info = {
head : Block_header.shell_header;
round_durations : Round.round_durations;
hard_gas_limit_per_block : Gas.Arith.integral;
head_round : Round.t;
round_zero_duration : Period.t;
grandparent_level_start : Timestamp.t;
}
let init_state_prototzresult ~head round_durations hard_gas_limit_per_block =
let open Lwt_result_syntax in
let*? head_round =
Alpha_context.Fitness.round_from_raw head.Tezos_base.Block_header.fitness
in
let round_zero_duration = Round.round_duration round_durations Round.zero in
let*? grandparent_round =
Alpha_context.Fitness.predecessor_round_from_raw head.fitness
in
let*? proposal_level_offset =
Round.level_offset_of_round
round_durations
~round:Round.(succ grandparent_round)
in
let*? proposal_round_offset =
Round.level_offset_of_round round_durations ~round:head_round
in
let*? proposal_offset =
Period.(add proposal_level_offset proposal_round_offset)
in
let grandparent_level_start = Timestamp.(head.timestamp - proposal_offset) in
return
{
head;
round_durations;
hard_gas_limit_per_block;
head_round;
round_zero_duration;
grandparent_level_start;
}
let init_state ~head round_durations hard_gas_limit_per_block =
Lwt.map
Environment.wrap_tzresult
(init_state_prototzresult ~head round_durations hard_gas_limit_per_block)
let init context ~(head : Tezos_base.Block_header.shell_header) =
let open Lwt_result_syntax in
let* ( ctxt,
(_ : Receipt.balance_updates),
(_ : Migration.origination_result list) ) =
prepare
context
~level:(Int32.succ head.level)
~predecessor_timestamp:head.timestamp
~timestamp:head.timestamp
|> Lwt.map Environment.wrap_tzresult
in
let round_durations = Constants.round_durations ctxt in
let hard_gas_limit_per_block = Constants.hard_gas_limit_per_block ctxt in
init_state ~head round_durations hard_gas_limit_per_block
let flush old_info ~head =
(* To avoid the need to prepare a context as in [init], we retrieve
the [round_durations] from the previous state. Indeed, they are
only determined by the [minimal_block_delay] and
[delay_increment_per_round] constants (see
{!Raw_context.prepare}), and all the constants remain unchanged
during the lifetime of a protocol. As to
[hard_gas_limit_per_block], it is directly a protocol
constant. *)
init_state ~head old_info.round_durations old_info.hard_gas_limit_per_block
let manager_prio p = `Low p
let consensus_prio = `High
let other_prio = `Medium
let compute_manager_contents_fee_and_gas_limit 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)
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 ~hard_gas_limit_per_block ?size ~fee
~gas op =
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)
let pre_filter_manager :
type t.
info ->
config ->
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 info config 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 compute_manager_contents_fee_and_gas_limit 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 ->
let weight, _op_resources =
weight_and_resources_manager_operation
~hard_gas_limit_per_block:info.hard_gas_limit_per_block
~fee
~gas:gas_limit
packed_op
in
`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 =
let open Result_syntax in
Timestamp.(
let+ now_drifted = now_timestamp +? drift in
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 attest 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 =
let open Result_syntax in
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
let* time_shift = Period.mult level_offset round_zero_duration in
let* earliest_op_level_start =
Timestamp.(proposal_predecessor_level_start +? time_shift)
in
(* computing the operations's round start from it's earliest
possible level start *)
let* op_earliest_ts =
Round.timestamp_of_another_round_same_level
round_durations
~current_round:Round.zero
~current_timestamp:earliest_op_level_start
~considered_round:op_round
in
(* We finally check that the expected time of the operation is
acceptable *)
acceptable ~drift ~op_earliest_ts ~now_timestamp
type level_and_round = {level : Raw_level.t; round : Round.t}
let pre_filter_far_future_consensus_ops info config
({level = op_level; round = op_round} : level_and_round) : bool Lwt.t =
let open Result_syntax in
let res =
let now_timestamp = Time.System.now () |> Time.System.to_protocol in
let* proposal_level = Raw_level.of_int32 info.head.level in
acceptable_op
~config
~round_durations:info.round_durations
~round_zero_duration:info.round_zero_duration
~proposal_level
~proposal_round:info.head_round
~proposal_timestamp:info.head.timestamp
~proposal_predecessor_level_start:info.grandparent_level_start
~op_level
~op_round
~now_timestamp
in
match res with Ok b -> Lwt.return b | Error _ -> Lwt.return_false
let prefilter_consensus_operation info config level_and_round =
let open Lwt_syntax in
let* keep = pre_filter_far_future_consensus_ops info config level_and_round in
if keep then return (`Passed_prefilter consensus_prio)
else
return
(`Branch_refused
[Environment.wrap_tzerror Consensus_operation_in_far_future])
(** A quasi infinite amount of "valid" (pre)attestations could be
sent by a committee member, one for each possible round number.
This filter rejects (pre)attestations 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 info config
({shell = _; protocol_data = Operation_data {contents; _} as op} :
Main.operation) =
let open Lwt_syntax in
let prefilter_manager_op manager_op =
return
@@
match pre_filter_manager info config 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 _) ->
return (`Refused [Environment.wrap_tzerror Wrong_operation])
| Single (Preattestation consensus_content)
| Single (Attestation {consensus_content; dal_content = _}) ->
let level_and_round : level_and_round =
{level = consensus_content.level; round = consensus_content.round}
in
prefilter_consensus_operation info config level_and_round
| Single (Seed_nonce_revelation _)
| Single (Double_preattestation_evidence _)
| Single (Double_attestation_evidence _)
| Single (Double_baking_evidence _)
| Single (Activate_account _)
| Single (Proposals _)
| Single (Vdf_revelation _)
| Single (Drain_delegate _)
| Single (Ballot _) ->
return (`Passed_prefilter other_prio)
| Single (Manager_operation _) as op -> prefilter_manager_op op
| Cons (Manager_operation _, _) as op -> prefilter_manager_op op
let syntactic_check _ = Lwt.return `Well_formed
let is_manager_operation op =
match Operation.acceptable_pass op with
| Some pass -> Compare.Int.equal pass Operation_repr.manager_pass
| None -> false
(* Should not fail on a valid manager operation. *)
let compute_fee_and_gas_limit {protocol_data = Operation_data data; _} =
compute_manager_contents_fee_and_gas_limit data.contents
let gas_as_q gas = Gas.Arith.integral_to_z gas |> Q.of_bigint
let fee_and_ratio_as_q fee gas =
let fee = Tez.to_mutez fee |> Z.of_int64 |> Q.of_bigint in
let gas = gas_as_q gas in
let ratio = Q.div fee gas in
(fee, ratio)
let bumped_fee_and_ratio_as_q config fee gas =
let bump = Q.mul config.replace_by_fee_factor in
let fee, ratio = fee_and_ratio_as_q fee gas in
(bump fee, bump ratio)
(** Determine whether the new manager operation is sufficiently better
than the old manager operation to replace it. Sufficiently better
means that the new operation's fee and fee/gas ratio are both
greater than or equal to the old operation's same metrics bumped by
the factor [config.replace_by_fee_factor]. *)
let better_fees_and_ratio config old_gas old_fee new_gas new_fee =
let bumped_old_fee, bumped_old_ratio =
bumped_fee_and_ratio_as_q config old_fee old_gas
in
let new_fee, new_ratio = fee_and_ratio_as_q new_fee new_gas in
Q.compare new_fee bumped_old_fee >= 0
&& Q.compare new_ratio bumped_old_ratio >= 0
(** [conflict_handler config] returns a conflict handler for
{!Mempool.add_operation} (see {!Mempool.conflict_handler}).
- For non-manager operations, we select the greater operation
according to {!Operation.compare}.
- A manager operation is replaced only when the new operation's
fee and fee/gas ratio both exceed the old operation's by at least a
factor of [config.replace_by_fee_factor] (see {!better_fees_and_ratio}).
Precondition: both operations must be individually valid (because
of the call to {!Operation.compare}). *)
let conflict_handler config : Mempool.conflict_handler =
let open Result_syntax in
fun ~existing_operation ~new_operation ->
let (_ : Operation_hash.t), old_op = existing_operation in
let (_ : Operation_hash.t), new_op = new_operation in
if is_manager_operation old_op && is_manager_operation new_op then
let new_op_is_better =
let* old_fee, old_gas_limit = compute_fee_and_gas_limit old_op in
let* new_fee, new_gas_limit = compute_fee_and_gas_limit new_op in
return
(better_fees_and_ratio
config
old_gas_limit
old_fee
new_gas_limit
new_fee)
in
match new_op_is_better with
| Ok b when b -> `Replace
| Ok _ | Error _ -> `Keep
else if Operation.compare existing_operation new_operation < 0 then `Replace
else `Keep
let int64_ceil_of_q q =
let n = Q.to_int64 q in
if Q.(equal q (of_int64 n)) then n else Int64.succ n
(* Compute the minimal fee (expressed in mutez) that [candidate_op]
would need to have in order for the {!conflict_handler} to let it
replace [op_to_replace], when both operations are manager
operations.
As specified in {!conflict_handler}, this means that [candidate_op]
with the returned fee needs to have both its fee and its fee/gas
ratio exceed (or match) [op_to_replace]'s same metrics bumped by
the {!config}'s [replace_by_fee_factor].
Return [None] when at least one operation is not a manager
operation.
Also return [None] if both operations are manager operations but
there was an error while computing the needed fee. However, note
that this cannot happen when both manager operations have been
successfully validated by the protocol. *)
let fee_needed_to_replace_by_fee config ~op_to_replace ~candidate_op =
let open Result_syntax in
if is_manager_operation candidate_op && is_manager_operation op_to_replace
then
(let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in
let* old_fee, old_gas = compute_fee_and_gas_limit op_to_replace in
if Gas.Arith.(old_gas = zero || candidate_gas = zero) then
(* This should not happen when both operations are valid. *)
return_none
else
let candidate_gas = gas_as_q candidate_gas in
let bumped_old_fee, bumped_old_ratio =
bumped_fee_and_ratio_as_q config old_fee old_gas
in
(* The new operation needs to exceed both the bumped fee and the
bumped ratio to make {!better_fees_and_ratio} return [true].
(Having fee or ratio equal to its bumped counterpart is ok too,
hence the [ceil] in [int64_ceil_of_q].) *)
let fee_needed_for_fee = int64_ceil_of_q bumped_old_fee in
let fee_needed_for_ratio =
int64_ceil_of_q Q.(bumped_old_ratio * candidate_gas)
in
return_some (max fee_needed_for_fee fee_needed_for_ratio))
|> Option.of_result |> Option.join
else None
let find_manager {shell = _; protocol_data = Operation_data {contents; _}} =
match contents with
| Single (Manager_operation {source; _}) -> Some source
| Cons (Manager_operation {source; _}, _) -> Some source
| Single
( Preattestation _ | Attestation _ | Proposals _ | Ballot _
| Seed_nonce_revelation _ | Vdf_revelation _ | Double_baking_evidence _
| Double_preattestation_evidence _ | Double_attestation_evidence _
| Activate_account _ | Drain_delegate _ | Failing_noop _ ) ->
None
(* The purpose of this module is to offer a version of
[fee_needed_to_replace_by_fee] where the caller doesn't need to
provide the [op_to_replace]. Instead, it needs to call
[Conflict_map.update] every time a new operation is added to the
mempool. This setup prevents the mempool plugin from exposing the
notion of manager operations and their source. *)
module Conflict_map = struct
(* The state consists in a map of all validated manager operations,
indexed by their source.
Note that the protocol already enforces that there is at most one
operation per source.
The state only tracks manager operations because other kinds of
operations don't have fees that we might adjust to change the
outcome of the {!conflict_handler}, so
[fee_needed_to_replace_by_fee] will always return [None] when
they are involved anyway. *)
type t = packed_operation Signature.Public_key_hash.Map.t
let empty = Signature.Public_key_hash.Map.empty
(* Remove all the [replacements] from the state, then add
[new_operation]. Non-manager operations are ignored.
It is important to remove before adding: otherwise, we would
remove the newly added operation when it has the same manager as
one of the replacements. *)
let update conflict_map ~new_operation ~replacements =
let conflict_map =
List.fold_left
(fun conflict_map op ->
match find_manager op with
| Some manager ->
Signature.Public_key_hash.Map.remove manager conflict_map
| None -> (* Non-manager operation: ignore it. *) conflict_map)
conflict_map
replacements
in
match find_manager new_operation with
| Some manager ->
Signature.Public_key_hash.Map.add manager new_operation conflict_map
| None -> (* Non-manager operation: ignore it. *) conflict_map
let fee_needed_to_replace_by_fee config ~candidate_op ~conflict_map =
match find_manager candidate_op with
| None -> (* Non-manager operation. *) None
| Some manager -> (
match Signature.Public_key_hash.Map.find manager conflict_map with
| None ->
(* This can only happen when the pre-existing conflicting
operation is a [Drain_delegate], which cannot be replaced by a
manager operation regardless of the latter's fee. *)
None
| Some op_to_replace ->
fee_needed_to_replace_by_fee config ~candidate_op ~op_to_replace)
end
let fee_needed_to_overtake ~op_to_overtake ~candidate_op =
let open Result_syntax in
if is_manager_operation candidate_op && is_manager_operation op_to_overtake
then
(let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in
let* target_fee, target_gas = compute_fee_and_gas_limit op_to_overtake in
if Gas.Arith.(target_gas = zero || candidate_gas = zero) then
(* This should not happen when both operations are valid. *)
return_none
else
(* Compute the target ratio as in {!Operation_repr.weight_manager}.
We purposefully don't use {!fee_and_ratio_as_q} because the code
here needs to stay in sync with {!Operation_repr.weight_manager}
rather than {!better_fees_and_ratio}. *)
let target_fee = Q.of_int64 (Tez.to_mutez target_fee) in
let target_gas = Q.of_bigint (Gas.Arith.integral_to_z target_gas) in
let target_ratio = Q.(target_fee / target_gas) in
(* Compute the minimal fee needed to have a strictly greater ratio. *)
let candidate_gas =
Q.of_bigint (Gas.Arith.integral_to_z candidate_gas)
in
return_some (Int64.succ Q.(to_int64 (target_ratio * candidate_gas))))
|> Option.of_result |> Option.join
else None
module Internal_for_tests = struct
let default_config_with_clock_drift clock_drift =
{default_config with clock_drift}
let default_config_with_replace_factor replace_by_fee_factor =
{default_config with replace_by_fee_factor}
let get_clock_drift {clock_drift; _} = clock_drift
let acceptable_op = acceptable_op
let fee_needed_to_replace_by_fee = fee_needed_to_replace_by_fee
end