123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793(*****************************************************************************)(* *)(* 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. *)(* *)(*****************************************************************************)openProtocolopenAlpha_contexttypenanotez=Q.tletnanotez_enc:nanotezData_encoding.t=letopenData_encodingindef"nanotez"~title:"A thousandth of a mutez"~description:"One thousand nanotez make a mutez (1 tez = 1e9 nanotez)"(conv(funq->(q.Q.num,q.Q.den))(fun(num,den)->{Q.num;den})(tup2zz))letmanager_op_replacement_factor_enc:Q.tData_encoding.t=letopenData_encodingindef"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(funq->(q.Q.num,q.Q.den))(fun(num,den)->{Q.num;den})(tup2zz))typeconfig={minimal_fees:Tez.t;minimal_nanotez_per_gas_unit:nanotez;minimal_nanotez_per_byte:nanotez;clock_drift:Period.toption;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. *)}letdefault_minimal_fees=matchTez.of_mutez100LwithNone->assertfalse|Somet->tletdefault_minimal_nanotez_per_gas_unit=Q.of_int100letdefault_minimal_nanotez_per_byte=Q.of_int1000letmanagers_quota=Stdlib.List.nthMain.validation_passesOperation_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.
*)letdefault_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_int105)(Z.of_int100)(* Default value of [replace_by_fee_factor] is set to 5% *);}letconfig_encoding:configData_encoding.t=letopenData_encodinginconv(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.encodingdefault_config.minimal_fees)(dft"minimal_nanotez_per_gas_unit"nanotez_encdefault_config.minimal_nanotez_per_gas_unit)(dft"minimal_nanotez_per_byte"nanotez_encdefault_config.minimal_nanotez_per_byte)(opt"clock_drift"Period.encoding)(dft"replace_by_fee_factor"manager_op_replacement_factor_encdefault_config.replace_by_fee_factor))(** Static information to store in the filter state. *)typeinfo={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;}letinit_state_prototzresult~headround_durationshard_gas_limit_per_block=letopenLwt_result_syntaxinlet*?head_round=Alpha_context.Fitness.round_from_rawhead.Tezos_base.Block_header.fitnessinletround_zero_duration=Round.round_durationround_durationsRound.zeroinlet*?grandparent_round=Alpha_context.Fitness.predecessor_round_from_rawhead.fitnessinlet*?proposal_level_offset=Round.level_offset_of_roundround_durations~round:Round.(succgrandparent_round)inlet*?proposal_round_offset=Round.level_offset_of_roundround_durations~round:head_roundinlet*?proposal_offset=Period.(addproposal_level_offsetproposal_round_offset)inletgrandparent_level_start=Timestamp.(head.timestamp-proposal_offset)inreturn{head;round_durations;hard_gas_limit_per_block;head_round;round_zero_duration;grandparent_level_start;}letinit_state~headround_durationshard_gas_limit_per_block=Lwt.mapEnvironment.wrap_tzresult(init_state_prototzresult~headround_durationshard_gas_limit_per_block)letinitcontext~(head:Tezos_base.Block_header.shell_header)=letopenLwt_result_syntaxinlet*(ctxt,(_:Receipt.balance_updates),(_:Migration.origination_resultlist))=preparecontext~level:(Int32.succhead.level)~predecessor_timestamp:head.timestamp~timestamp:head.timestamp|>Lwt.mapEnvironment.wrap_tzresultinletround_durations=Constants.round_durationsctxtinlethard_gas_limit_per_block=Constants.hard_gas_limit_per_blockctxtininit_state~headround_durationshard_gas_limit_per_blockletflushold_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~headold_info.round_durationsold_info.hard_gas_limit_per_blockletmanager_priop=`Lowpletconsensus_prio=`Highletother_prio=`Mediumletcompute_manager_contents_fee_and_gas_limitcontents=letopenOperationinletl=to_list(Contents_listcontents)inList.fold_left(funacc->function|Contents(Manager_operation{fee;gas_limit;_})->(matchaccwith|Error_ase->e|Ok(total_fee,total_gas)->(matchTez.(total_fee+?fee)with|Oktotal_fee->Ok(total_fee,Gas.Arith.addtotal_gasgas_limit)|Error_ase->e))|_->acc)(Ok(Tez.zero,Gas.Arith.zero))ltypeEnvironment.Error_monad.error+=Fees_too_lowlet()=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:(funppf()->Format.fprintfppf"Operation fees are too low")Data_encoding.unit(functionFees_too_low->Some()|_->None)(fun()->Fees_too_low)letsize_of_operationop=(WithExceptions.Option.get~loc:__LOC__@@Data_encoding.Binary.fixed_lengthTezos_base.Operation.shell_header_encoding)+Data_encoding.Binary.lengthOperation.protocol_data_encodingop(** 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} *)letweight_and_resources_manager_operation~hard_gas_limit_per_block?size~fee~gasop=letmax_size=managers_quota.max_sizeinletsize=matchsizewithNone->size_of_operationop|Somes->sinletsize_f=Q.of_intsizeinletgas_f=Q.of_bigint(Gas.Arith.integral_to_zgas)inletfee_f=Q.of_int64(Tez.to_mutezfee)inletsize_ratio=Q.(size_f/Q.of_intmax_size)inletgas_ratio=Q.(gas_f/Q.of_bigint(Gas.Arith.integral_to_zhard_gas_limit_per_block))inletresources=Q.maxsize_ratiogas_ratioin(Q.(fee_f/resources),resources)letpre_filter_manager:typet.info->config->Operation.packed_protocol_data->tKind.managercontents_list->[`Passed_prefilterofQ.tlist|`Branch_refusedoftztrace|`Branch_delayedoftztrace|`Refusedoftztrace|`Outdatedoftztrace]=funinfoconfigpacked_opop->letsize=size_of_operationpacked_opinletcheck_gas_and_feefeegas_limit=letfees_in_nanotez=Q.mul(Q.of_int64(Tez.to_mutezfee))(Q.of_int1000)inletminimal_fees_in_nanotez=Q.mul(Q.of_int64(Tez.to_mutezconfig.minimal_fees))(Q.of_int1000)inletminimal_fees_for_gas_in_nanotez=Q.mulconfig.minimal_nanotez_per_gas_unit(Q.of_bigint@@Gas.Arith.integral_to_zgas_limit)inletminimal_fees_for_size_in_nanotez=Q.mulconfig.minimal_nanotez_per_byte(Q.of_intsize)inifQ.comparefees_in_nanotez(Q.addminimal_fees_in_nanotez(Q.addminimal_fees_for_gas_in_nanotezminimal_fees_for_size_in_nanotez))>=0then`Fees_okelse`Refused[Environment.wrap_tzerrorFees_too_low]inmatchcompute_manager_contents_fee_and_gas_limitopwith|Errorerr->`Refused(Environment.wrap_tztraceerr)|Ok(fee,gas_limit)->(matchcheck_gas_and_feefeegas_limitwith|`Refused_aserr->err|`Fees_ok->letweight,_op_resources=weight_and_resources_manager_operation~hard_gas_limit_per_block:info.hard_gas_limit_per_block~fee~gas:gas_limitpacked_opin`Passed_prefilter[weight])typeEnvironment.Error_monad.error+=Wrong_operationlet()=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:(funppf()->Format.fprintfppf"Failing_noop operations are not accepted in the mempool")Data_encoding.unit(functionWrong_operation->Some()|_->None)(fun()->Wrong_operation)typeEnvironment.Error_monad.error+=Consensus_operation_in_far_futurelet()=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:(funppf()->Format.fprintfppf"Consensus operation too far in the future are not accepted.")Data_encoding.unit(functionConsensus_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. *)letacceptable~drift~op_earliest_ts~now_timestamp=letopenResult_syntaxinTimestamp.(let+now_drifted=now_timestamp+?driftinop_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.
*)letacceptable_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=letopenResult_syntaxinifRaw_level.(succop_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. *)Oktrueelse(* 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 *)letnow_timestamp=Timestamp.(maxnow_timestampproposal_timestamp)in(* Computing when the current level started. *)letdrift=Option.value~default:round_zero_durationconfig.clock_driftin(* 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] *)letlevel_offset=Raw_level.(diff(succop_level)proposal_level)inlet*time_shift=Period.multlevel_offsetround_zero_durationinlet*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_levelround_durations~current_round:Round.zero~current_timestamp:earliest_op_level_start~considered_round:op_roundin(* We finally check that the expected time of the operation is
acceptable *)acceptable~drift~op_earliest_ts~now_timestamptypelevel_and_round={level:Raw_level.t;round:Round.t}letpre_filter_far_future_consensus_opsinfoconfig({level=op_level;round=op_round}:level_and_round):boolLwt.t=letopenResult_syntaxinletres=letnow_timestamp=Time.System.now()|>Time.System.to_protocolinlet*proposal_level=Raw_level.of_int32info.head.levelinacceptable_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_timestampinmatchreswithOkb->Lwt.returnb|Error_->Lwt.return_falseletprefilter_consensus_operationinfoconfiglevel_and_round=letopenLwt_syntaxinlet*keep=pre_filter_far_future_consensus_opsinfoconfiglevel_and_roundinifkeepthenreturn(`Passed_prefilterconsensus_prio)elsereturn(`Branch_refused[Environment.wrap_tzerrorConsensus_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.
*)letpre_filterinfoconfig({shell=_;protocol_data=Operation_data{contents;_}asop}:Main.operation)=letopenLwt_syntaxinletprefilter_manager_opmanager_op=return@@matchpre_filter_managerinfoconfigopmanager_opwith|`Passed_prefilterprio->`Passed_prefilter(manager_prioprio)|(`Branch_refused_|`Branch_delayed_|`Refused_|`Outdated_)aserr->errinmatchcontentswith|Single(Failing_noop_)->return(`Refused[Environment.wrap_tzerrorWrong_operation])|Single(Preattestationconsensus_content)|Single(Attestation{consensus_content;dal_content=_})->letlevel_and_round:level_and_round={level=consensus_content.level;round=consensus_content.round}inprefilter_consensus_operationinfoconfiglevel_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_prefilterother_prio)|Single(Manager_operation_)asop->prefilter_manager_opop|Cons(Manager_operation_,_)asop->prefilter_manager_opopletsyntactic_check_=Lwt.return`Well_formedletis_manager_operationop=matchOperation.acceptable_passopwith|Somepass->Compare.Int.equalpassOperation_repr.manager_pass|None->false(* Should not fail on a valid manager operation. *)letcompute_fee_and_gas_limit{protocol_data=Operation_datadata;_}=compute_manager_contents_fee_and_gas_limitdata.contentsletgas_as_qgas=Gas.Arith.integral_to_zgas|>Q.of_bigintletfee_and_ratio_as_qfeegas=letfee=Tez.to_mutezfee|>Z.of_int64|>Q.of_bigintinletgas=gas_as_qgasinletratio=Q.divfeegasin(fee,ratio)letbumped_fee_and_ratio_as_qconfigfeegas=letbump=Q.mulconfig.replace_by_fee_factorinletfee,ratio=fee_and_ratio_as_qfeegasin(bumpfee,bumpratio)(** 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]. *)letbetter_fees_and_ratioconfigold_gasold_feenew_gasnew_fee=letbumped_old_fee,bumped_old_ratio=bumped_fee_and_ratio_as_qconfigold_feeold_gasinletnew_fee,new_ratio=fee_and_ratio_as_qnew_feenew_gasinQ.comparenew_feebumped_old_fee>=0&&Q.comparenew_ratiobumped_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}). *)letconflict_handlerconfig:Mempool.conflict_handler=letopenResult_syntaxinfun~existing_operation~new_operation->let(_:Operation_hash.t),old_op=existing_operationinlet(_:Operation_hash.t),new_op=new_operationinifis_manager_operationold_op&&is_manager_operationnew_opthenletnew_op_is_better=let*old_fee,old_gas_limit=compute_fee_and_gas_limitold_opinlet*new_fee,new_gas_limit=compute_fee_and_gas_limitnew_opinreturn(better_fees_and_ratioconfigold_gas_limitold_feenew_gas_limitnew_fee)inmatchnew_op_is_betterwith|Okbwhenb->`Replace|Ok_|Error_->`KeepelseifOperation.compareexisting_operationnew_operation<0then`Replaceelse`Keepletint64_ceil_of_qq=letn=Q.to_int64qinifQ.(equalq(of_int64n))thennelseInt64.succn(* 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. *)letfee_needed_to_replace_by_feeconfig~op_to_replace~candidate_op=letopenResult_syntaxinifis_manager_operationcandidate_op&&is_manager_operationop_to_replacethen(let*_fee,candidate_gas=compute_fee_and_gas_limitcandidate_opinlet*old_fee,old_gas=compute_fee_and_gas_limitop_to_replaceinifGas.Arith.(old_gas=zero||candidate_gas=zero)then(* This should not happen when both operations are valid. *)return_noneelseletcandidate_gas=gas_as_qcandidate_gasinletbumped_old_fee,bumped_old_ratio=bumped_fee_and_ratio_as_qconfigold_feeold_gasin(* 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].) *)letfee_needed_for_fee=int64_ceil_of_qbumped_old_feeinletfee_needed_for_ratio=int64_ceil_of_qQ.(bumped_old_ratio*candidate_gas)inreturn_some(maxfee_needed_for_feefee_needed_for_ratio))|>Option.of_result|>Option.joinelseNoneletfind_manager{shell=_;protocol_data=Operation_data{contents;_}}=matchcontentswith|Single(Manager_operation{source;_})->Somesource|Cons(Manager_operation{source;_},_)->Somesource|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. *)moduleConflict_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. *)typet=packed_operationSignature.Public_key_hash.Map.tletempty=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. *)letupdateconflict_map~new_operation~replacements=letconflict_map=List.fold_left(funconflict_mapop->matchfind_manageropwith|Somemanager->Signature.Public_key_hash.Map.removemanagerconflict_map|None->(* Non-manager operation: ignore it. *)conflict_map)conflict_mapreplacementsinmatchfind_managernew_operationwith|Somemanager->Signature.Public_key_hash.Map.addmanagernew_operationconflict_map|None->(* Non-manager operation: ignore it. *)conflict_mapletfee_needed_to_replace_by_feeconfig~candidate_op~conflict_map=matchfind_managercandidate_opwith|None->(* Non-manager operation. *)None|Somemanager->(matchSignature.Public_key_hash.Map.findmanagerconflict_mapwith|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|Someop_to_replace->fee_needed_to_replace_by_feeconfig~candidate_op~op_to_replace)endletfee_needed_to_overtake~op_to_overtake~candidate_op=letopenResult_syntaxinifis_manager_operationcandidate_op&&is_manager_operationop_to_overtakethen(let*_fee,candidate_gas=compute_fee_and_gas_limitcandidate_opinlet*target_fee,target_gas=compute_fee_and_gas_limitop_to_overtakeinifGas.Arith.(target_gas=zero||candidate_gas=zero)then(* This should not happen when both operations are valid. *)return_noneelse(* 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}. *)lettarget_fee=Q.of_int64(Tez.to_muteztarget_fee)inlettarget_gas=Q.of_bigint(Gas.Arith.integral_to_ztarget_gas)inlettarget_ratio=Q.(target_fee/target_gas)in(* Compute the minimal fee needed to have a strictly greater ratio. *)letcandidate_gas=Q.of_bigint(Gas.Arith.integral_to_zcandidate_gas)inreturn_some(Int64.succQ.(to_int64(target_ratio*candidate_gas))))|>Option.of_result|>Option.joinelseNonemoduleInternal_for_tests=structletdefault_config_with_clock_driftclock_drift={default_configwithclock_drift}letdefault_config_with_replace_factorreplace_by_fee_factor={default_configwithreplace_by_fee_factor}letget_clock_drift{clock_drift;_}=clock_driftletacceptable_op=acceptable_opletfee_needed_to_replace_by_fee=fee_needed_to_replace_by_feeend