123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143(*****************************************************************************)(* *)(* 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_contexttypeerror_classification=[`Branch_delayedoftztrace|`Branch_refusedoftztrace|`Refusedoftztrace|`Outdatedoftztrace]typenanotez=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;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.toption;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. *)}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;allow_script_failure=true;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% *);max_prechecked_manager_operations=5_000;}letconfig_encoding:configData_encoding.t=letopenData_encodinginconv(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.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)(dft"allow_script_failure"booldefault_config.allow_script_failure)(opt"clock_drift"Period.encoding)(dft"replace_by_fee_factor"manager_op_replacement_factor_encdefault_config.replace_by_fee_factor)(dft"max_prechecked_manager_operations"int31default_config.max_prechecked_manager_operations))(** An Alpha_context manager operation, packed so that the type is not
parametrized by ['kind]. *)typemanager_op=Manager_op:'kindKind.manageroperation->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. *)typemanager_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_tagGas.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. *)}typemanager_op_weight={operation_hash:Operation_hash.t;weight:Q.t}(** Build a {!manager_op_weight} from operation hash and {!manager_op_info}. *)letmk_op_weightoph(info:manager_op_info)={operation_hash=oph;weight=info.weight}letcompare_manager_op_weightop1op2=letc=Q.compareop1.weightop2.weightinifc<>0thencelseOperation_hash.compareop1.operation_hashop2.operation_hashmoduleManagerOpWeightSet=Set.Make(structtypet=manager_op_weight(* Sort by weight *)letcompare=compare_manager_op_weightend)(** Static information to store in the filter state. *)typestate_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;}(** State that tracks validated manager operations. *)typeops_state={prechecked_manager_op_count:int;(** Number of prechecked manager operations.
Invariants:
- [prechecked_manager_op_count
= 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_infoOperation_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_weightoption;(** 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 }] *)}typestate={state_info:state_info;ops_state:ops_state}letempty_ops_state={prechecked_manager_op_count=0;prechecked_manager_ops=Operation_hash.Map.empty;prechecked_op_weights=ManagerOpWeightSet.empty;min_prechecked_op_weight=None;}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)inletstate_info={head;round_durations;hard_gas_limit_per_block;head_round;round_zero_duration;grandparent_level_start;}inreturn{state_info;ops_state=empty_ops_state}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_state~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_state.state_info.round_durationsold_state.state_info.hard_gas_limit_per_blockletmanager_priop=`Lowpletconsensus_prio=`Highletother_prio=`Mediumletget_manager_operation_gas_and_feecontents=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)typeEnvironment.Error_monad.error+=|Manager_restrictionof{oph: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:(funppf(oph,fee)->Format.fprintfppf"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)."Operation_hash.ppophTez.ppfee)Data_encoding.(obj2(req"operation_hash"Operation_hash.encoding)(req"operation_fee"Tez.encoding))(functionManager_restriction{oph;fee}->Some(oph,fee)|_->None)(fun(oph,fee)->Manager_restriction{oph;fee})typeEnvironment.Error_monad.error+=|Manager_operation_replacedof{old_hash:Operation_hash.t;new_hash: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:(funppf(old_hash,new_hash)->Format.fprintfppf"The manager operation %a has been replaced with %a"Operation_hash.ppold_hashOperation_hash.ppnew_hash)(Data_encoding.obj2(Data_encoding.req"old_hash"Operation_hash.encoding)(Data_encoding.req"new_hash"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})typeEnvironment.Error_monad.error+=Fees_too_low_for_mempoolofTez.tlet()=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:(funppfrequired_fees->Format.fprintfppf"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.pprequired_fees)Data_encoding.(obj1(req"required_fees"Tez.encoding))(function|Fees_too_low_for_mempoolrequired_fees->Somerequired_fees|_->None)(funrequired_fees->Fees_too_low_for_mempoolrequired_fees)typeEnvironment.Error_monad.error+=Removed_fees_too_low_for_mempoollet()=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:(funppf()->Format.fprintfppf"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(functionRemoved_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].
*)letbetter_fees_and_ratio=letbumpconfigq=Q.mulqconfig.replace_by_fee_factorinfunconfigold_gasold_feenew_gasnew_fee->letold_fee=Tez.to_mutezold_fee|>Z.of_int64|>Q.of_bigintinletold_gas=Gas.Arith.integral_to_zold_gas|>Q.of_bigintinletnew_fee=Tez.to_muteznew_fee|>Z.of_int64|>Q.of_bigintinletnew_gas=Gas.Arith.integral_to_znew_gas|>Q.of_bigintinletold_ratio=Q.divold_feeold_gasinletnew_ratio=Q.divnew_feenew_gasinQ.comparenew_ratio(bumpconfigold_ratio)>=0&&Q.comparenew_fee(bumpconfigold_fee)>=0letsize_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)(** Return fee for an operation that consumes [op_resources] for its weight to
be strictly greater than [min_weight]. *)letrequired_fee_manager_operation_weight~op_resources~min_weight=letreq_mutez_q=Q.((min_weight*op_resources)+Q.one)inTez.of_mutez_exn@@Q.to_int64req_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. *)letcheck_minimal_weightconfigstate~fee~gas_limitop=letweight,op_resources=weight_and_resources_manager_operation~hard_gas_limit_per_block:state.state_info.hard_gas_limit_per_block~fee~gas:gas_limitopinifstate.ops_state.prechecked_manager_op_count<config.max_prechecked_manager_operationsthen(* The precheck mempool is not full yet *)`Weight_ok(`No_replace,[weight])elsematchstate.ops_state.min_prechecked_op_weightwith|None->(* The precheck mempool is empty *)`Weight_ok(`No_replace,[weight])|Some{weight=min_weight;operation_hash=min_oph}->ifQ.(weight>min_weight)then(* The operation has a weight greater than the minimal
prechecked operation, replace the latest with the new one *)`Weight_ok(`Replacemin_oph,[weight])else(* Otherwise fail and give indication as to what to fee should
be for the operation to be prechecked *)letrequired_fee=required_fee_manager_operation_weight~op_resources~min_weightin`Fail(`Branch_delayed[Environment.wrap_tzerror(Fees_too_low_for_mempoolrequired_fee)])letoutput_encoding=letopenData_encodinginobj3(req"outbox_level"Environment.Bounded.Non_negative_int32.encoding)(req"message_index"n)(req"message"Variable.string)letoutput_proof_encoding=letopenData_encodinginobj3(req"output_proof"Tezos_context_helpers.Context.Proof_encoding.Merkle_proof_encoding.V2.Tree2.tree_proof_encoding)(req"output_proof_state"Sc_rollup.State_hash.encoding)(req"output_proof_output"output_encoding)moduleTree=structopenEnvironmentincludeContext.Treetypetree=Context.treetypet=Context.ttypekey=stringlisttypevalue=bytesendmoduleWasm_machine=Environment.Wasm_2_0_0.Make(Tree)letdiscard_wasm_output_proof_earlyoutput_proofoutbox_levelmessage_indexoutput=letopenLwt_syntaxinlet+result=Environment.Context.verify_tree_proofoutput_proof(funtree->let*output=Wasm_machine.get_output{outbox_level;message_index}treeinreturn(tree,output))inmatchresultwith|Ok(_,Someexpected_output)->not(expected_output=output)|_->falseletkinded_hash_to_state_hash=function|`Valuehash|`Nodehash->Sc_rollup.State_hash.context_hash_to_state_hashhashletis_invalid_op:typet.tmanager_operation->boolLwt.t=letopenLwt_syntaxinfunction|Sc_rollup_execute_outbox_message{rollup=_;cemented_commitment=_;output_proof}->(matchData_encoding.Binary.of_string_optoutput_proof_encodingoutput_proofwith|None->return_true|Some(output_proof,output_proof_state,(outbox_level,message_index,output))->let*discard_wasm_proof=discard_wasm_output_proof_earlyoutput_proofoutbox_levelmessage_indexoutputinletstate_is_correct=Sc_rollup.State_hash.equaloutput_proof_state(kinded_hash_to_state_hashoutput_proof.Environment.Context.Proof.before)inletis_invalid=(notstate_is_correct)||discard_wasm_proofinreturnis_invalid)|_->return_falseletreccontains_invalid_op:typet.tKind.managercontents_list->boolLwt.t=function|Single(Manager_operation{operation;_})->is_invalid_opoperation|Cons(Manager_operation{operation;_},rest)->letopenLwt_syntaxinlet*is_invalid=is_invalid_opoperationinifnotis_invalidthencontains_invalid_oprestelsereturn_trueletsyntactic_check({shell=_;protocol_data=Operation_data{contents;_}}:Main.operation)=letopenLwt_syntaxinmatchcontentswith|Single(Failing_noop_)|Single(Preendorsement_)|Single(Endorsement_)|Single(Dal_attestation_)|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`Well_formed|Single(Manager_operation_)asop->let*is_invalid=contains_invalid_opopinifis_invalidthenreturn`Ill_formedelsereturn`Well_formed|Cons(Manager_operation_,_)asop->let*is_invalid=contains_invalid_opopinifis_invalidthenreturn`Ill_formedelsereturn`Well_formedletpre_filter_manager:typet.config->state->Operation.packed_protocol_data->tKind.managercontents_list->[`Passed_prefilterofQ.tlist|`Branch_refusedoftztrace|`Branch_delayedoftztrace|`Refusedoftztrace|`Outdatedoftztrace]=funconfigfilter_statepacked_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]inmatchget_manager_operation_gas_and_feeopwith|Errorerr->`Refused(Environment.wrap_tztraceerr)|Ok(fee,gas_limit)->(matchcheck_gas_and_feefeegas_limitwith|`Refused_aserr->err|`Fees_ok->(matchcheck_minimal_weightconfigfilter_state~fee~gas_limitpacked_opwith|`Failerrs->errs|`Weight_ok(_,weight)->`Passed_prefilterweight))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=Timestamp.(now_timestamp+?drift>|?funnow_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.
*)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=ifRaw_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)inPeriod.multlevel_offsetround_zero_duration>>?funtime_shift->Timestamp.(proposal_predecessor_level_start+?time_shift)>>?funearliest_op_level_start->(* computing the operations's round start from it's earliest
possible level start *)Round.timestamp_of_another_round_same_levelround_durations~current_round:Round.zero~current_timestamp:earliest_op_level_start~considered_round:op_round>>?funop_earliest_ts->(* We finally check that the expected time of the operation is
acceptable *)acceptable~drift~op_earliest_ts~now_timestampletpre_filter_far_future_consensus_opsconfig~filter_state({level=op_level;round=op_round;_}:consensus_content):boolLwt.t=letres=letopenResult_syntaxinletnow_timestamp=Time.System.now()|>Time.System.to_protocolinlet*proposal_level=Raw_level.of_int32filter_state.state_info.head.levelinacceptable_op~config~round_durations:filter_state.state_info.round_durations~round_zero_duration:filter_state.state_info.round_zero_duration~proposal_level~proposal_round:filter_state.state_info.head_round~proposal_timestamp:filter_state.state_info.head.timestamp~proposal_predecessor_level_start:filter_state.state_info.grandparent_level_start~op_level~op_round~now_timestampinmatchreswithOkb->Lwt.returnb|Error_->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.
*)letpre_filterconfig~filter_state({shell=_;protocol_data=Operation_data{contents;_}asop}:Main.operation)=letprefilter_manager_opmanager_op=Lwt.return@@matchpre_filter_managerconfigfilter_stateopmanager_opwith|`Passed_prefilterprio->`Passed_prefilter(manager_prioprio)|(`Branch_refused_|`Branch_delayed_|`Refused_|`Outdated_)aserr->errinmatchcontentswith|Single(Failing_noop_)->Lwt.return(`Refused[Environment.wrap_tzerrorWrong_operation])|Single(Preendorsementconsensus_content)|Single(Endorsementconsensus_content)->pre_filter_far_future_consensus_ops~filter_stateconfigconsensus_content>>=funkeep->ifkeepthenLwt.return@@`Passed_prefilterconsensus_prioelseLwt.return(`Branch_refused[Environment.wrap_tzerrorConsensus_operation_in_far_future])|Single(Dal_attestation_)|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_prefilterother_prio|Single(Manager_operation_)asop->prefilter_manager_opop|Cons(Manager_operation_,_)asop->prefilter_manager_opop(** Remove a manager operation hash from the ops_state.
Do nothing if the operation was not in the state. *)letremove_operationops_stateoph=matchOperation_hash.Map.findophops_state.prechecked_manager_opswith|None->(* Not present in the ops_state: nothing to do. *)ops_state|Someinfo->letprechecked_manager_ops=Operation_hash.Map.removeophops_state.prechecked_manager_opsinletprechecked_manager_op_count=ops_state.prechecked_manager_op_count-1inletprechecked_op_weights=ManagerOpWeightSet.remove(mk_op_weightophinfo)ops_state.prechecked_op_weightsinletmin_prechecked_op_weight=matchops_state.min_prechecked_op_weightwith|None->None|Somemin_op_weight->ifOperation_hash.equalmin_op_weight.operation_hashophthenManagerOpWeightSet.min_eltprechecked_op_weightselseSomemin_op_weightin{prechecked_manager_op_count;prechecked_manager_ops;prechecked_op_weights;min_prechecked_op_weight;}(** Remove a manager operation hash from the ops_state.
Do nothing if the operation was not in the state. *)letremove~filter_stateoph={filter_statewithops_state=remove_operationfilter_state.ops_stateoph}(** Add a manager operation hash and information to the filter state.
Do nothing if the operation is already present in the state. *)letadd_manager_opops_stateophinforeplacement=letops_state=matchreplacementwith|`No_replace->ops_state|`Replace(oph,_classification)->remove_operationops_stateophinifOperation_hash.Map.memophops_state.prechecked_manager_opsthen(* Already present in the ops_state: nothing to do. *)ops_stateelseletprechecked_manager_op_count=ops_state.prechecked_manager_op_count+1inletprechecked_manager_ops=Operation_hash.Map.addophinfoops_state.prechecked_manager_opsinletop_weight=mk_op_weightophinfoinletprechecked_op_weights=ManagerOpWeightSet.addop_weightops_state.prechecked_op_weightsinletmin_prechecked_op_weight=matchops_state.min_prechecked_op_weightwith|Someold_minwhencompare_manager_op_weightold_minop_weight<=0->Someold_min|_->Someop_weightin{prechecked_manager_op_count;prechecked_manager_ops;prechecked_op_weights;min_prechecked_op_weight;}letadd_manager_op_and_enforce_mempool_boundconfigfilter_stateoph(op:'manager_kindKind.manageroperation)=letopenLwt_result_syntaxinlet*?fee,gas_limit=Result.map_error(funerr->`Refused(Environment.wrap_tztraceerr))(get_manager_operation_gas_and_feeop.protocol_data.contents)inlet*replacement,weight=matchcheck_minimal_weightconfigfilter_state~fee~gas_limit(Operation_dataop.protocol_data)with|`Weight_ok(`No_replace,weight)->(* The mempool is not full: no need to replace any operation. *)return(`No_replace,weight)|`Weight_ok(`Replacemin_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. *)letreplace_err=Environment.wrap_tzerrorRemoved_fees_too_low_for_mempoolinletreplacement=`Replace(min_weight_oph,`Branch_delayed[replace_err])inreturn(replacement,weight)|`Failerr->(* The mempool is full and the weight of the new operation is
too low: raise the error returned by {!check_minimal_weight}. *)failerrinletweight=matchweightwith[x]->x|_->assertfalseinletinfo={manager_op=Manager_opop;gas_limit;fee;weight}inletops_state=add_manager_opfilter_state.ops_stateophinforeplacementinreturn({filter_statewithops_state},replacement)(** If the provided operation is a manager operation, add it to the
filter_state. If the mempool is full, either return an error if the
operation does not have enough weight to be included, or return the
operation with minimal weight that gets removed to make room.
Do nothing on non-manager operations.
If [replace] is provided, then it is removed from [filter_state]
before processing [op]. (If [replace] is a non-manager operation,
this does nothing since it was never in [filter_state] to begin with.)
Note that when this happens, the mempool can no longer be full after
the operation has been removed, so this function always returns
[`No_replace].
This function is designed to be called by the shell each time a
new operation has been validated by the protocol. It will be
removed in the future once the shell is able to bound the number of
operations in the mempool by itself. *)letadd_operation_and_enforce_mempool_bound?replaceconfigfilter_state(oph,op)=letfilter_state=matchreplacewith|Somereplace_oph->(* If the operation to replace is not a manager operation, then
it cannot be present in the [filter_state]. But then,
[remove] does nothing anyway. *)remove~filter_statereplace_oph|None->filter_stateinlet{protocol_data=Operation_dataprotocol_data;_}=opinletcall_managerprotocol_data=add_manager_op_and_enforce_mempool_boundconfigfilter_stateoph{shell=op.shell;protocol_data}inmatchprotocol_data.contentswith|Single(Manager_operation_)->call_managerprotocol_data|Cons(Manager_operation_,_)->call_managerprotocol_data|Single_->return(filter_state,`No_replace)letis_manager_operationop=matchOperation.acceptable_passopwith|Somepass->Compare.Int.equalpassOperation_repr.manager_pass|None->false(** [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=fun~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=letopenResult_syntaxinlet{protocol_data=Operation_dataold_protocol_data;_}=old_opinlet{protocol_data=Operation_datanew_protocol_data;_}=new_opinlet*old_fee,old_gas_limit=get_manager_operation_gas_and_feeold_protocol_data.contentsinlet*new_fee,new_gas_limit=get_manager_operation_gas_and_feenew_protocol_data.contentsinreturn(better_fees_and_ratioconfigold_gas_limitold_feenew_gas_limitnew_fee)inmatchnew_op_is_betterwith|Okbwhenb->`Replace|Ok_|Error_->`KeepelseifOperation.compareexisting_operationnew_operation<0then`Replaceelse`Keep