diff --git a/docs/shell/validation.rst b/docs/shell/validation.rst index 16a7c8cc41dd31d7cdadff82acdc6725630bc57f..dd7033948b79a1bb159ed576bf2de3f38cbb9846 100644 --- a/docs/shell/validation.rst +++ b/docs/shell/validation.rst @@ -171,6 +171,15 @@ an operation with the same manager. This limitation was already present implicitely if you were using the `tezos-client` commands. Batches of operations can be used to get around this restriction. +To mitigate the limitation itself, a user can inject an operation with the same +manager and the same counter, but with a higher fee to replace an already existing +operation in the prevalidator. Only one of the two operations will be eventually +included in a block. To be able to replace the first operation, the fee and the +"fee/gas limit" ratio of the second one is supposed to be higher than the first's +by a factor (currently fixed to 5%). In case of successful replacement, the old +operation is re-classified as `\`Outdated`. + + Distributed DB -------------- .. _DDB_component: diff --git a/src/lib_shell/prevalidator.ml b/src/lib_shell/prevalidator.ml index bca5a25190eb52bacc4f5663692e27f9fcb1957a..c3144eec365d6312975d8e48245296e12be2d17b 100644 --- a/src/lib_shell/prevalidator.ml +++ b/src/lib_shell/prevalidator.ml @@ -220,6 +220,36 @@ type limits = { (* Minimal delay between two mempool advertisements *) let advertisement_delay = 0.1 +type error += + | Manager_operation_replaced of { + old_hash : Operation_hash.t; + new_hash : Operation_hash.t; + } + +let () = + register_error_kind + `Permanent + ~id:"prevalidator.manager_operation_replaced" + ~title:"Manager operation replaced" + ~description:"The manager operation has been replaced" + ~pp:(fun ppf (old_hash, new_hash) -> + Format.fprintf + ppf + "The manager operation %a has been replaced with %a" + Operation_hash.pp + old_hash + Operation_hash.pp + new_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}) + module Name = struct type t = Chain_id.t * Protocol_hash.t @@ -538,6 +568,40 @@ module Make ~head:(Store.Block.hash shell.predecessor) shell.mempool + let remove_from_advertisement oph = function + | `Pending mempool -> `Pending (Mempool.remove oph mempool) + | `None -> `None + + (* This function retrieves an old/replaced operation and reclassifies it as + [`Outdated]. Note that we don't need to re-flush the mempool, as this + function is only called in precheck mode. + + The operation is expected to be (a) parsable and (b) in the "prechecked" + class. So, we softly handle the situations where the operation is + unparsable or not found in any class in case this invariant is broken + for some reason. + *) + let reclassify_replaced_manager_op ~notifier old_hash new_hash shell = + shell.advertisement <- + remove_from_advertisement old_hash shell.advertisement ; + match Classification.remove old_hash shell.classification with + | Some (op, _class) -> ( + (* In this block, we add [old_hash] to the classification. This + does not break the "classes are disjoint" invariant, because we + just removed [old_hash] with [!Classification.remove] above. *) + match Prevalidation_t.parse op with + | Error errors -> + (* This should likely not happen, as we already parsed the op *) + handle ~notifier shell (`Unparsed (old_hash, op)) (`Refused errors) + | Ok op -> + let err = Manager_operation_replaced {old_hash; new_hash} in + handle ~notifier shell (`Parsed op) (`Outdated [err])) + | None -> + (* This case should not happen. *) + Distributed_db.Operation.clear_or_cancel + shell.parameters.chain_db + old_hash + let precheck ~disable_precheck ~filter_config ~filter_state ~validation_state oph (op : Filter.Proto.operation_data operation) = let validation_state = Prevalidation_t.validation_state validation_state in @@ -555,6 +619,10 @@ module Make (* The [precheck] optimization triggers: no need to call the protocol [apply_operation]. *) `Passed_precheck filter_state + | `Passed_precheck_with_replace (old_oph, filter_state) -> + (* Same as `Passed_precheck, but the operation whose hash is returned + should be reclassified to Outdated *) + `Passed_precheck_with_replace (old_oph, filter_state) | (`Branch_delayed _ | `Branch_refused _ | `Refused _ | `Outdated _) as errs -> (* Note that we don't need to distinguish some failure cases @@ -587,6 +655,11 @@ module Make handle ~notifier shell (`Parsed op) `Applied ; let new_mempool = Mempool.cons_valid op.hash mempool in Lwt.return (filter_state, validation_state, new_mempool) + | `Passed_precheck_with_replace (old_oph, filter_state) -> + reclassify_replaced_manager_op ~notifier old_oph oph shell ; + handle ~notifier shell (`Parsed op) `Applied ; + let new_mempool = Mempool.cons_valid op.hash mempool in + Lwt.return (filter_state, validation_state, new_mempool) | `Undecided -> ( Prevalidation_t.apply_operation validation_state op >>= function | Applied (new_validation_state, receipt) -> ( @@ -933,10 +1006,6 @@ module Make ~mempool shell.predecessor - let remove_from_advertisement oph = function - | `Pending mempool -> `Pending (Mempool.remove oph mempool) - | `None -> `None - let remove pv oph = Distributed_db.Operation.clear_or_cancel pv.shell.parameters.chain_db oph ; pv.shell.advertisement <- diff --git a/src/lib_shell/prevalidator_filters.ml b/src/lib_shell/prevalidator_filters.ml index d7c3d9798138e8e7c842e4a937711faf34dd4e0d..d3c4404f45b1b7f255619892fd4131180e005cb2 100644 --- a/src/lib_shell/prevalidator_filters.ml +++ b/src/lib_shell/prevalidator_filters.ml @@ -60,6 +60,7 @@ module type FILTER = sig Operation_hash.t -> Proto.operation_data -> [ `Passed_precheck of state + | `Passed_precheck_with_replace of Operation_hash.t * state | `Branch_delayed of tztrace | `Branch_refused of tztrace | `Refused of tztrace diff --git a/src/lib_shell/prevalidator_filters.mli b/src/lib_shell/prevalidator_filters.mli index 9d7e469632efe952e4d959428e2dc6b9350dd601..89de4d942108f9ebef4e67fae944aecd89bc9737 100644 --- a/src/lib_shell/prevalidator_filters.mli +++ b/src/lib_shell/prevalidator_filters.mli @@ -61,14 +61,17 @@ module type FILTER = sig [oph] from the state of the filter *) val remove : filter_state:state -> Operation_hash.t -> state - (** [precheck ~filter_state ~validation_state shell_header oph] should be - used to decide whether an operation can be propagated over the gossip + (** [precheck config ~filter_state ~validation_state shell_header oph] + should be used to decide whether an operation can be gossiped to the network without executing it. This is a wrapper around [Proto.precheck_manager] and [Proto.check_signature]. This function hereby has a similar return type. Returns [`Passed_precheck] if the operation was successfully - prechecked. If the function returns [`Undecided] it means that + prechecked. In case the operation is successfully prechecked + but replaces an already prechecked operation [old_oph], the + result [`Passed_precheck_with_replace old_oph] is returned. + If the function returns [`Undecided] it means that [apply_operation] should be called. This function takes a [state] as parameter and returns it updated if the @@ -81,6 +84,7 @@ module type FILTER = sig Operation_hash.t -> Proto.operation_data -> [ `Passed_precheck of state + | `Passed_precheck_with_replace of Operation_hash.t * state | `Branch_delayed of tztrace | `Branch_refused of tztrace | `Refused of tztrace diff --git a/src/proto_alpha/lib_plugin/plugin.ml b/src/proto_alpha/lib_plugin/plugin.ml index 4d6b0f63f084a4b4c076286066dd953ecbe978f0..60a9ec78170ea78c3dc2f9e15832c2f35ee1badc 100644 --- a/src/proto_alpha/lib_plugin/plugin.ml +++ b/src/proto_alpha/lib_plugin/plugin.ml @@ -81,6 +81,18 @@ module Mempool = struct (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; @@ -90,6 +102,13 @@ module Mempool = struct [`Passed_postfilter filter_state], no matter the operation's success. *) clock_drift : Period.t option; + replace_by_fee_factor : Q.t; + (** This field determines the amount of additional fees (given as a + factor of the declared fees) a manager should add to an operation + in order to (eventually) replace an existing (prechecked) one + in the mempool. Note that other criteria, such as the gas ratio, + are also taken into account to decide whether to accept the + replacement or not. *) } let default_minimal_fees = @@ -108,6 +127,9 @@ module Mempool = struct let config_encoding : config Data_encoding.t = let open Data_encoding in + (* 105/100 = 1.05%: This is the minumum fee increase ratio required between + an operation and another one it'd replace in the prevalidator. *) + let replace_factor = Q.make (Z.of_int 105) (Z.of_int 100) in conv (fun { minimal_fees; @@ -115,25 +137,29 @@ module Mempool = struct minimal_nanotez_per_byte; allow_script_failure; clock_drift; + replace_by_fee_factor; } -> ( minimal_fees, minimal_nanotez_per_gas_unit, minimal_nanotez_per_byte, allow_script_failure, - clock_drift )) + clock_drift, + replace_by_fee_factor )) (fun ( minimal_fees, minimal_nanotez_per_gas_unit, minimal_nanotez_per_byte, allow_script_failure, - clock_drift ) -> + clock_drift, + replace_by_fee_factor ) -> { minimal_fees; minimal_nanotez_per_gas_unit; minimal_nanotez_per_byte; allow_script_failure; clock_drift; + replace_by_fee_factor; }) - (obj5 + (obj6 (dft "minimal_fees" Tez.encoding default_minimal_fees) (dft "minimal_nanotez_per_gas_unit" @@ -144,7 +170,11 @@ module Mempool = struct nanotez_enc default_minimal_nanotez_per_byte) (dft "allow_script_failure" bool true) - (opt "clock_drift" Period.encoding)) + (opt "clock_drift" Period.encoding) + (dft + "replace_by_fee_factor" + manager_op_replacement_factor_enc + replace_factor)) let default_config = { @@ -153,14 +183,35 @@ module Mempool = struct minimal_nanotez_per_byte = default_minimal_nanotez_per_byte; allow_script_failure = true; clock_drift = None; + replace_by_fee_factor = + Q.make (Z.of_int 105) (Z.of_int 100) + (* Default value of [replace_by_fee_factor] is set to 5% *); } + type manager_gas_witness + + (* For each Prechecked manager operation (batched or not), we associate the + following information to its source: + - the operation's hash, needed in case the operation is replaced + afterwards, + - the total fee and gas_limit, needed to compare operations of the same + manager to decide which one has more fees w.r.t. announced gas limit + (modulo replace_by_fee_factor) + *) + type manager_op_info = { + operation_hash : Operation_hash.t; + gas_limit : manager_gas_witness Gas.Arith.t; + fee : Tez.t; + } + type state = { grandparent_level_start : Alpha_context.Timestamp.t option; round_zero_duration : Period.t option; - op_prechecked_managers : Signature.Public_key_hash.Set.t; - (** Set of all managers that are the source of manager operations - applied in the mempool. Each manager in the set should be accessible + op_prechecked_managers : manager_op_info Signature.Public_key_hash.Map.t; + (** All managers that are the source of manager operations + prechecked in the mempool. Each manager in the map is associated to + a record of type [manager_op_info] (See for record details above). + Each manager in the map should be accessible with an operation hash in [operation_hash_to_manager]. *) operation_hash_to_manager : Signature.Public_key_hash.t Operation_hash.Map.t; (** Map of operation hash to manager used to remove a manager from @@ -172,7 +223,7 @@ module Mempool = struct { grandparent_level_start = None; round_zero_duration = None; - op_prechecked_managers = Signature.Public_key_hash.Set.empty; + op_prechecked_managers = Signature.Public_key_hash.Map.empty; operation_hash_to_manager = Operation_hash.Map.empty; } @@ -229,7 +280,7 @@ module Mempool = struct { filter_state with op_prechecked_managers = - Signature.Public_key_hash.Set.remove + Signature.Public_key_hash.Map.remove source filter_state.op_prechecked_managers; operation_hash_to_manager = @@ -282,66 +333,89 @@ module Mempool = struct (function Manager_restriction -> Some () | _ -> None) (fun () -> Manager_restriction) - let check_manager_restriction filter_state source = - if - Signature.Public_key_hash.Set.mem + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2238 + Write unit tests for the feature 'replace-by-fee' and for other changes + introduced by other MRs in the plugin. *) + (* In order to decide if the new operation can replace an old one from the + same manager, we check if its fees (resp. fees/gas ratio) are greater than + (or equal to) the old operations's fees (resp. fees/gas ratio), bumped by + the factor [config.replace_by_fee_factor]. + *) + let better_fees_and_ratio = + let bump config q = Q.mul q config.replace_by_fee_factor in + fun config old_gas old_fee new_gas new_fee -> + let old_fee = Tez.to_mutez old_fee |> Z.of_int64 |> Q.of_bigint in + let old_gas = Gas.Arith.integral_to_z old_gas |> Q.of_bigint in + let new_fee = Tez.to_mutez new_fee |> Z.of_int64 |> Q.of_bigint in + let new_gas = Gas.Arith.integral_to_z new_gas |> Q.of_bigint in + let old_ratio = Q.div old_fee old_gas in + let new_ratio = Q.div new_fee new_gas in + Q.compare new_ratio (bump config old_ratio) >= 0 + && Q.compare new_fee (bump config old_fee) >= 0 + + let check_manager_restriction config filter_state source ~fee ~gas_limit = + match + Signature.Public_key_hash.Map.find source filter_state.op_prechecked_managers - then - (* Manager already seen: one manager per block limitation triggered. *) - Error (`Branch_delayed [Environment.wrap_tzerror Manager_restriction]) - else Ok () + with + | None -> `Fresh + | Some {operation_hash = old_hash; gas_limit = old_gas; fee = old_fee} -> + (* Manager already seen: one manager per block limitation triggered. + Can replace old operation if new operation's fees are better *) + if better_fees_and_ratio config old_gas old_fee gas_limit fee then + `Replace old_hash + else + `Fail (`Branch_delayed [Environment.wrap_tzerror Manager_restriction]) let pre_filter_manager : type t. config -> state -> - t Kind.manager contents_list -> public_key_hash -> int -> + t Kind.manager contents_list -> [ `Passed_prefilter | `Branch_refused of tztrace | `Branch_delayed of tztrace | `Refused of tztrace | `Outdated of tztrace ] = - fun config filter_state op source size -> - let check_gas_and_fee () = - match get_manager_operation_gas_and_fee op with - | Error err -> - let err = Environment.wrap_tztrace err in - `Refused err - | Ok (fee, gas) -> - 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) - 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 `Passed_prefilter - else `Refused [Environment.wrap_tzerror Fees_too_low] + fun config filter_state source size op -> + 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 `Passed_prefilter + else `Refused [Environment.wrap_tzerror Fees_too_low] in - match check_manager_restriction filter_state source with - | Error errs -> errs - | Ok () -> check_gas_and_fee () + match get_manager_operation_gas_and_fee op with + | Error err -> `Refused (Environment.wrap_tztrace err) + | Ok (fee, gas_limit) -> ( + match + check_manager_restriction config filter_state source ~fee ~gas_limit + with + | `Fail errs -> errs + | `Fresh | `Replace _ -> check_gas_and_fee fee gas_limit) type Environment.Error_monad.error += Outdated_endorsement @@ -615,36 +689,43 @@ module Mempool = struct | Single (Ballot _) -> Lwt.return @@ `Passed_prefilter | Single (Manager_operation {source; _}) as op -> - Lwt.return (pre_filter_manager config filter_state op source bytes) + Lwt.return @@ pre_filter_manager config filter_state source bytes op | Cons (Manager_operation {source; _}, _) as op -> - Lwt.return (pre_filter_manager config filter_state op source bytes) + Lwt.return @@ pre_filter_manager config filter_state source bytes op let precheck_manager : type t. + config -> state -> validation_state -> Tezos_base.Operation.shell_header -> t Kind.manager protocol_data -> + fee:Tez.t -> + gas_limit:manager_gas_witness Gas.Arith.t -> public_key_hash -> [> `Prechecked_manager + | `Prechecked_manager_with_replace of Operation_hash.t | `Branch_delayed of tztrace | `Branch_refused of tztrace | `Refused of tztrace | `Outdated of tztrace ] Lwt.t = - fun filter_state + fun config + filter_state validation_state shell ({contents; _} as protocol_data : t Kind.manager protocol_data) + ~fee + ~gas_limit source -> - let precheck_manager_and_check_signature () = + let precheck_manager_and_check_signature ~on_success = ( Main.precheck_manager validation_state contents >>=? fun () -> let (raw_operation : t Kind.manager operation) = Alpha_context.{shell; protocol_data} in Main.check_manager_signature validation_state contents raw_operation ) >|= function - | Ok () -> `Prechecked_manager + | Ok () -> on_success | Error err -> ( let err = Environment.wrap_tztrace err in match classify_trace err with @@ -653,17 +734,24 @@ module Mempool = struct | Temporary -> `Branch_delayed err | Outdated -> `Outdated err) in - match check_manager_restriction filter_state source with - | Error err -> Lwt.return err - | Ok () -> precheck_manager_and_check_signature () - - let add_manager_restriction filter_state oph source = + match + check_manager_restriction config filter_state source ~fee ~gas_limit + with + | `Fail err -> Lwt.return err + | `Fresh -> + precheck_manager_and_check_signature ~on_success:`Prechecked_manager + | `Replace old_oph -> + precheck_manager_and_check_signature + ~on_success:(`Prechecked_manager_with_replace old_oph) + + let add_manager_restriction filter_state oph info source = { filter_state with op_prechecked_managers = (* Manager not seen yet, record it for next ops *) - Signature.Public_key_hash.Set.add + Signature.Public_key_hash.Map.add source + info filter_state.op_prechecked_managers; operation_hash_to_manager = Operation_hash.Map.add oph source filter_state.operation_hash_to_manager @@ -678,37 +766,49 @@ module Mempool = struct Operation_hash.t -> Main.operation_data -> [ `Passed_precheck of state + | `Passed_precheck_with_replace of Operation_hash.t * state | `Branch_delayed of tztrace | `Branch_refused of tztrace | `Refused of tztrace | `Outdated of tztrace | `Undecided ] Lwt.t = - fun _ + fun config ~filter_state ~validation_state shell_header oph (Operation_data protocol_data) -> - let precheck_manager protocol_data source = - precheck_manager - filter_state - validation_state - shell_header - protocol_data - source - >|= function - | `Prechecked_manager -> - `Passed_precheck (add_manager_restriction filter_state oph source) - | (`Refused _ | `Branch_delayed _ | `Branch_refused _ | `Outdated _) as - errs -> - errs + let precheck_manager protocol_data source op = + match get_manager_operation_gas_and_fee op with + | Error err -> Lwt.return (`Refused (Environment.wrap_tztrace err)) + | Ok (fee, gas_limit) -> ( + let info = {operation_hash = oph; gas_limit; fee} in + precheck_manager + config + filter_state + validation_state + shell_header + protocol_data + source + ~fee + ~gas_limit + >|= function + | `Prechecked_manager -> + `Passed_precheck + (add_manager_restriction filter_state oph info source) + | `Prechecked_manager_with_replace old_oph -> + `Passed_precheck_with_replace + (old_oph, add_manager_restriction filter_state oph info source) + | (`Refused _ | `Branch_delayed _ | `Branch_refused _ | `Outdated _) + as errs -> + errs) in match protocol_data.contents with - | Single (Manager_operation {source; _}) -> - precheck_manager protocol_data source - | Cons (Manager_operation {source; _}, _) -> - precheck_manager protocol_data source + | Single (Manager_operation {source; _}) as op -> + precheck_manager protocol_data source op + | Cons (Manager_operation {source; _}, _) as op -> + precheck_manager protocol_data source op | Single _ -> Lwt.return `Undecided open Apply_results diff --git a/src/proto_alpha/lib_plugin/test/test_consensus_filter.ml b/src/proto_alpha/lib_plugin/test/test_consensus_filter.ml index d57472c1671b7b80ea4e7b678ed5582ded514d93..8e359c8ebf0bc7b91370aa5b8a89162498d0d536 100644 --- a/src/proto_alpha/lib_plugin/test/test_consensus_filter.ml +++ b/src/proto_alpha/lib_plugin/test/test_consensus_filter.ml @@ -37,6 +37,7 @@ let config drift_opt = Option.map (fun drift -> Period.of_seconds_exn (Int64.of_int drift)) drift_opt; + replace_by_fee_factor = Q.make (Z.of_int 105) (Z.of_int 100); } type Environment.Error_monad.error += Generation_failure diff --git a/tezt/_regressions/rpc/alpha.client.mempool.out b/tezt/_regressions/rpc/alpha.client.mempool.out index ac90d15755db8b3614ea2db3ba182288be1ace42..377a5f1b79e8ecb0a7c4461eb529c0f23abd6494 100644 --- a/tezt/_regressions/rpc/alpha.client.mempool.out +++ b/tezt/_regressions/rpc/alpha.client.mempool.out @@ -88,11 +88,13 @@ curl -s 'http://localhost:16385/chains/main/mempool/monitor_operations?applied=t ./tezos-client rpc get /chains/main/mempool/filter { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=true' { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=false' {} @@ -110,15 +112,18 @@ curl -s 'http://localhost:16385/chains/main/mempool/monitor_operations?applied=t "allow_script_failure": false }' { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get /chains/main/mempool/filter { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=true' { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=false' { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], @@ -129,30 +134,36 @@ curl -s 'http://localhost:16385/chains/main/mempool/monitor_operations?applied=t "allow_script_failure": true }' { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get /chains/main/mempool/filter { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=true' { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=false' { "minimal_fees": "200" } ./tezos-client rpc post /chains/main/mempool/filter with '{}' { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get /chains/main/mempool/filter { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=true' { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client rpc get '/chains/main/mempool/filter?include_default=false' {} diff --git a/tezt/_regressions/rpc/alpha.proxy.mempool.out b/tezt/_regressions/rpc/alpha.proxy.mempool.out index 81a476385688cc3ac4eac374753cd2a6c948cc4c..3b7eee21271fb51c5af7fc1e2d8c8278eea4f594 100644 --- a/tezt/_regressions/rpc/alpha.proxy.mempool.out +++ b/tezt/_regressions/rpc/alpha.proxy.mempool.out @@ -90,12 +90,14 @@ curl -s 'http://localhost:16385/chains/main/mempool/monitor_operations?applied=t ./tezos-client --mode proxy rpc get /chains/main/mempool/filter protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=true' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=false' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK @@ -115,17 +117,20 @@ protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaAL }' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get /chains/main/mempool/filter protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=true' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "50", "minimal_nanotez_per_gas_unit": [ "201", "5" ], - "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false } + "minimal_nanotez_per_byte": [ "56", "3" ], "allow_script_failure": false, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=false' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK @@ -138,17 +143,20 @@ protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaAL }' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get /chains/main/mempool/filter protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=true' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "200", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=false' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK @@ -157,17 +165,20 @@ protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaAL ./tezos-client --mode proxy rpc post /chains/main/mempool/filter with '{}' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get /chains/main/mempool/filter protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=true' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK { "minimal_fees": "100", "minimal_nanotez_per_gas_unit": [ "100", "1" ], - "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true } + "minimal_nanotez_per_byte": [ "1000", "1" ], "allow_script_failure": true, + "replace_by_fee_factor": [ "21", "20" ] } ./tezos-client --mode proxy rpc get '/chains/main/mempool/filter?include_default=false' protocol of proxy unspecified, using the node's protocol: ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK diff --git a/tezt/tests/main.ml b/tezt/tests/main.ml index d0ee9fb8996a8429c9c76b801874fea96e999c40..eecfd7af5c4efabdb739f2a1f0b3e81275b0bb15 100644 --- a/tezt/tests/main.ml +++ b/tezt/tests/main.ml @@ -106,6 +106,6 @@ let () = Tx_rollup.register ~protocols:[Alpha] ; Manager_operations.register ~protocols ; - + Replace_by_fees.register ~protocols:[Alpha] ; (* Test.run () should be the last statement, don't register afterwards! *) Test.run () diff --git a/tezt/tests/manager_operations.ml b/tezt/tests/manager_operations.ml index 081e4ef2378565d3714bfe921678eb78717675bc..873845150a19a076c2cb9cc8569b7ca93af738c3 100644 --- a/tezt/tests/manager_operations.ml +++ b/tezt/tests/manager_operations.ml @@ -588,7 +588,7 @@ module Memchecks = struct return oph) else return oph - let with_checks ~__LOC__ ~classification + let with_checks ~__LOC__ ?(bake = true) ~classification ?(classification_after_flush = classification) ~should_propagate ?(should_include = should_propagate) nodes inject = Log.subsection @@ -612,35 +612,37 @@ module Memchecks = struct "- Baking (should%s include operation %s)." (if should_include then "" else " not") oph ; - let* () = Helpers.bake_and_wait_block nodes.main in - Log.info "- Waiting for observer to see operation or block." ; - let* observer_result = wait_observer in - Log.info "- Checking mempool of main node." ; - let* mempool_after_baking = RPC.get_mempool client in - check_operation_classification - classification_after_flush - ~__LOC__ - mempool_after_baking - oph - ~explain:"after baking" ; - Log.info "- Checking that observer did not observe operation." ; - let* () = - check_op_in_block - ~__LOC__ - client - oph - ~should_include - ~explain:"newly baked" - and* () = - check_op_not_propagated + if not bake then return oph + else + let* () = Helpers.bake_and_wait_block nodes.main in + Log.info "- Waiting for observer to see operation or block." ; + let* observer_result = wait_observer in + Log.info "- Checking mempool of main node." ; + let* mempool_after_baking = RPC.get_mempool client in + check_operation_classification + classification_after_flush ~__LOC__ - nodes.observer + mempool_after_baking oph - observer_result - ~should_include - ~explain:(string_of_ext_classification classification) - in - return oph + ~explain:"after baking" ; + Log.info "- Checking that observer did not observe operation." ; + let* () = + check_op_in_block + ~__LOC__ + client + oph + ~should_include + ~explain:"newly baked" + and* () = + check_op_not_propagated + ~__LOC__ + nodes.observer + oph + observer_result + ~should_include + ~explain:(string_of_ext_classification classification) + in + return oph let with_refused_checks = with_checks ~classification:`Refused ~should_propagate:false diff --git a/tezt/tests/replace_by_fees.ml b/tezt/tests/replace_by_fees.ml new file mode 100644 index 0000000000000000000000000000000000000000..cf8a502813fdf2b0ecda0bcc60bf3d3ca4831762 --- /dev/null +++ b/tezt/tests/replace_by_fees.ml @@ -0,0 +1,526 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2021 Nomadic Labs *) +(* *) +(* 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. *) +(* *) +(*****************************************************************************) + +(* Testing + ------- + Component: Precheck + Invocation: dune exec tezt/tests/main.exe -- --file replace_by_fees.ml + Subject: Tests for the "replace by fees" feature in the prevelidator +*) + +(** This modules implement tests related to the manager operations replacement + in the mempool when precheck is activated *) + +open Tezt_tezos + +(* TODO: https://gitlab.com/tezos/tezos/-/issues/2208 + Factorize the Memchecks and Helpers modules and the node_client and + the two_nodes types in a separate file *) + +(* TODO: https://gitlab.com/tezos/tezos/-/issues/2208 + Refactor the tests as suggested in + https://gitlab.com/tezos/tezos/-/merge_requests/3968#note_753442465 once + helpers and checkers of manager_operations are refactored +*) +module Memchecks = Manager_operations.Memchecks +module Helpers = Manager_operations.Helpers + +type node_client = Manager_operations.node_client = { + node : Node.t; + client : Client.t; +} + +type two_nodes = Manager_operations.two_nodes = { + main : node_client; + observer : node_client; +} + +(* Some local helper and wapper functions *) +let check_applied ~__LOC__ nodes inject = + Memchecks.with_applied_checks + ~__LOC__ + nodes + ~expected_statuses:[] + ~bake:false + inject + +let check_branch_delayed ~__LOC__ nodes inject = + Memchecks.with_branch_delayed_checks ~__LOC__ ~bake:false nodes inject + +let check_refused ~__LOC__ nodes inject = + Memchecks.with_refused_checks ~__LOC__ ~bake:false nodes inject + +let op_is_applied ~__LOC__ nodes opH = + Lwt_list.iter_s + (fun client -> + let* mempool = RPC.get_mempool client in + Memchecks.check_operation_is_in_mempool ~__LOC__ `Applied mempool opH ; + unit) + [nodes.main.client; nodes.observer.client] + +let op_is_outdated ~__LOC__ nodes opH = + Lwt_list.iter_s + (fun client -> + let* mempool = RPC.get_mempool client in + Memchecks.check_operation_is_in_mempool ~__LOC__ `Outdated mempool opH ; + unit) + [nodes.main.client; nodes.observer.client] + +(* minimal extra fee replacement is set to 5% *) +let minimal_replacement_fee = + let factor = Q.make (Z.of_int 105) (Z.of_int 100) in + fun fee -> + let r = Q.mul (Q.of_int fee) factor in + (* rounding to upper value *) + Z.cdiv (Q.num r) (Q.den r) |> Z.to_int + +(* Default fee used in the tests of this module *) +let default_fee = 1000 + +(* Default fee bumped by replacement fees factor *) +let replacement_fee = minimal_replacement_fee default_fee + +(* Default gas limit used in the tests of this module *) +let default_gas = 1000 + +(* Default transferred amount used in the tests of this module *) +let default_amount = 1 + +(* Default sources of the tests in this module *) +let default_source = Constant.bootstrap1 + +(* A record type that contains all needed/variable informations in the tests + of this module *) +type op_info = { + fee : int; + gas : int; + amount : int; + (* if counter is none, the next counter will be used *) + get_counter : Client.t -> int option Lwt.t; +} + +(* Default op, with default gas and fees and no imposed counter *) +let default_op = + { + fee = default_fee; + gas = default_gas; + amount = default_amount; + get_counter = (fun _ -> Lwt.return_none); + } + +(* An op with replacement fees instead of default ones *) +let replacement_op = {default_op with fee = replacement_fee} + +(* Auxiliary function to get the current counter *) +let get_counter ?(contract = default_source) client = + let* counter = + RPC.Contracts.get_counter + ~contract_id:contract.Account.public_key_hash + client + in + Lwt.return @@ JSON.as_int counter + +(* Auxiliary function that constructs a batch transfer of the given size *) +let mk_batch client op_data size = + let* counter = op_data.get_counter client in + let* counter = + match counter with None -> get_counter client | Some c -> Lwt.return c + in + let rec batch_rec nb acc = + if nb < 0 then Lwt.return [] + else if nb = 0 then Lwt.return acc + else + let* op = + Operation.mk_transfer + ~source:default_source + ~counter:(counter + nb) + ~fee:op_data.fee + ~gas_limit:op_data.gas + ~amount:op_data.amount + ~dest:default_source + client + in + batch_rec (nb - 1) (op :: acc) + in + batch_rec size [] + +(* This is a generic function used to write most of the tests below. In + addition to the test's title the function takes: + - a first operation to inject [op1], and the checks to perform [incheck1] + while injecting, + - a second operation to inject [op2], the checks to perform [incheck2] + while injecting, and the checks [postcheck2] to perform on op1 and op2's + hashes after their injections, + - if provided, a third operation to inject [op3], the checks to perform + [incheck3] while injecting, and the checks [postcheck3] to perform on op1, + op2 and op3's hashes after their injections. + + The function does the following: + - construct a first (eventually batched) operation, forge and inject it, and + monitor its propagation through a main and an observer nodes using + function [incheck1], + - construct a second (eventually batched) operation with the same manager, + forge and inject it, and monitor its propagation through a main and an + observer nodes using function [incheck2], + - use function [postcheck2] to make some checks about the operations + once the second one is injected (eg. is the first operation applied or + removed ? ...) + - if a third operation and its checkers are provided: + + construct a third (eventually batched) operation with the same manager, + forge and inject it, and monitor its propagation through a main and an + observer nodes using function [incheck3], + + use function [postcheck3] to make some checks about the operations (eg. + to check which one is applied, which one is removed, ...) +*) +let replacement_test_helper ~title ~__LOC__ ~op1 ?(size1 = 1) ~op2 ?(size2 = 1) + ~incheck1 ~incheck2 ~postcheck2 ?op3 ?(size3 = 1) ?incheck3 ?postcheck3 () = + Protocol.register_test ~__FILE__ ~title ~tags:["replace"; "fee"; "manager"] + @@ fun protocol -> + let* nodes = Helpers.init ~protocol () in + let client = nodes.main.client in + let signer = default_source in + let* oph1 = + let* batch = mk_batch client op1 size1 in + incheck1 ~__LOC__ nodes @@ fun () -> + Operation.forge_and_inject_operation + ~async:true + ~force:true + ~protocol + ~batch + ~signer + client + in + let* oph2 = + let* batch = mk_batch client op2 size2 in + incheck2 ~__LOC__ nodes @@ fun () -> + Operation.forge_and_inject_operation + ~async:true + ~force:true + ~protocol + ~batch + ~signer + client + in + let* () = postcheck2 nodes oph1 oph2 in + match (op3, incheck3, postcheck3) with + | (None, None, None) -> unit + | (Some op3, Some incheck3, Some postcheck3) -> + let* oph3 = + let* batch = mk_batch client op3 size3 in + incheck3 ~__LOC__ nodes @@ fun () -> + Operation.forge_and_inject_operation + ~async:true + ~force:true + ~protocol + ~batch + ~signer + client + in + postcheck3 nodes oph1 oph2 oph3 + | _ -> Test.fail ~__LOC__ "Test is illformed" + +(* 2nd operation is identical (same hash) to the first one. *) +let identical_operations = + replacement_test_helper + ~__LOC__ + ~title:"Injecting the same operation twice" + ~op1:default_op + ~op2:default_op + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun _nodes h1 h2 -> + assert (h1 = h2) ; + unit) + () + +(* 2nd operation's gas and fees are equal and not the hash. *) +let same_gas_and_fees_but_different_ops = + replacement_test_helper + ~__LOC__ + ~title:"Inject two different operations with same gas and fee" + ~op1:default_op + ~op2:{default_op with amount = default_amount + 1} + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation just below replacement threshold. *) +let replacement_fees_below_threshold = + replacement_test_helper + ~__LOC__ + ~title:"Second op's fees are below threshold by 1 mutez" + ~op1:default_op + ~op2:{replacement_op with fee = replacement_op.fee - 1} + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation equal to replacement threshold. *) +let replacement_fees_equal_to_threshold = + replacement_test_helper + ~__LOC__ + ~title:"Second op's fees are equal to replacement fees threshold" + ~op1:default_op + ~op2:replacement_op + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation just above replacement threshold. *) +let replacement_fees_above_threshold = + replacement_test_helper + ~__LOC__ + ~title:"Second op's fees are above replacement fees threshold by 1 mutez" + ~op1:default_op + ~op2:{replacement_op with fee = replacement_op.fee + 1} + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation just above replacement threshold of the + first operation, but fees of the 3rd operation are below replacement + threshold of the second operation. *) +let third_operation_fees_below_replacement_threshold = + let op2 = {replacement_op with fee = replacement_op.fee + 1} in + replacement_test_helper + ~__LOC__ + ~title:"Replace a replacement requires bumping fees again" + ~op1:default_op + ~op2 + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + ~op3:{default_op with fee = minimal_replacement_fee op2.fee - 1} + ~incheck3:check_branch_delayed + ~postcheck3:(fun nodes _h1 h2 _h3 -> op_is_applied ~__LOC__ nodes h2) + () + +(* Fees of 2nd operation just above replacement threshold of the + first operation, and fees of the 3rd operation are equal to replacement + threshold of the second operation. *) +let third_operation_fees_equal_to_replacement_threshold = + let op2 = {replacement_op with fee = replacement_op.fee + 1} in + replacement_test_helper + ~__LOC__ + ~title:"Replace a replacement op with enough fees" + ~op1:default_op + ~op2 + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + ~op3:{op2 with fee = minimal_replacement_fee op2.fee} + ~incheck3:check_applied + ~postcheck3:(fun nodes _h1 h2 _h3 -> op_is_outdated ~__LOC__ nodes h2) + () + +(* Fees of 2nd operation equal to replacement threshold, but the gas is also + increased by 1 unit. *) +let replacement_fees_equal_to_threshold_but_gas_increased = + replacement_test_helper + ~__LOC__ + ~title:"Second op's fees are equal to replacement fees. But gas increased" + ~op1:default_op + ~op2:{replacement_op with gas = replacement_op.gas + 1} + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation just below replacement threshold. Even if the gas + limit of the second operation is decreased by 1, replacement will fail, + because fees are not sufficient. *) +let replacement_fees_below_threshold_even_if_gas_is_decreased = + let op2 = replacement_op in + replacement_test_helper + ~__LOC__ + ~title: + "Second op's gas descreased by 1, but still no enough replacement fees" + ~op1:default_op + ~op2:{op2 with fee = op2.fee - 1; gas = op2.gas - 1} + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* The ratio fee/gas is far better than the first one, but the current + implemented policy doesn't allow to decrease amount of fees when replacing + and operation. *) +let fees_of_second_op_below_fees_of_first_one = + let op1 = {default_op with fee = 50_000; gas = 100_000} in + (* The ratio fee/gas is more important, but fee is lower to replace *) + let op2 = {op1 with fee = op1.fee; gas = op1.gas / 100} in + replacement_test_helper + ~__LOC__ + ~title:"Op2's gas/fee is more important, but fees are not higher than max" + ~op1 + ~op2 + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* The second operation has much more fees than the first one, and a + better fee/gas ratio. But its counter is in the future. It cannot be + applied, and thus replace the first operation *) +let cannot_replace_with_an_op_having_diffrent_counter = + let get_counter client = + let* counter = get_counter client in + (* counter in the future*) + Lwt.return_some @@ (counter + 100) + in + let op2 = {default_op with fee = 100 * default_op.fee; get_counter} in + replacement_test_helper + ~__LOC__ + ~title:"Much more fees in second op, but counter in the future" + ~op1:default_op + ~op2 + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* The first operation is not applied. So the second operation doesn't need to + have fees at least equal to "replacement fees threshold" to be applied. *) +let cannot_replace_a_non_applied_operation = + let get_counter client = + let* counter = get_counter client in + (* counter in the future*) + Lwt.return_some @@ (counter + 100) + in + let op1 = {default_op with get_counter} in + replacement_test_helper + ~__LOC__ + ~title:"First operation not applied, second one applied with default fees" + ~op1 + ~op2:default_op + ~incheck1:check_branch_delayed + ~incheck2:check_applied + ~postcheck2:(fun _nodes _h1 _h2 -> unit) + () + +(* Sum of fees of the second operation is ok, but gas doubled. So ratio is not + good to make the replacement *) +let replace_simple_op_with_a_batched_low_fees = + replacement_test_helper + ~__LOC__ + ~title:"Gas limit doubled in batch. More fees needed" + ~op1:default_op + ~op2:{default_op with fee = (replacement_fee / 2) + 1} + ~size2:2 + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Sum of fees of the second operation is ok, ans gas is ok in the whole batch. + So, replacement is ok. *) +let replace_simple_op_with_a_batched = + replacement_test_helper + ~__LOC__ + ~title:"2nd operation's gas limit is constant. Replacement possible." + ~op1:default_op + ~op2: + {default_op with gas = default_gas / 2; fee = (replacement_fee / 2) + 1} + ~size2:2 + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + () + +(* Replacing a batched operation not possible due to low fees *) +let replace_batched_op_with_simple_one_low_fees = + replacement_test_helper + ~__LOC__ + ~title:"Replacing a batched operation not possible due to low fees" + ~op1:default_op + ~size1:2 + ~op2:replacement_op + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Replacing a batched op is possible if enough fees are provided *) +let replace_batched_op_with_simple_one = + replacement_test_helper + ~__LOC__ + ~title:"Replacing a batched op is possible if enough fees are provided" + ~op1:{default_op with fee = default_op.fee / 2} + ~size1:2 + ~op2:replacement_op + ~incheck1:check_applied + ~incheck2:check_applied + ~postcheck2:(fun nodes h1 _h2 -> op_is_outdated ~__LOC__ nodes h1) + () + +(* Fees of 2nd operation are bigger than the account's supply. *) +let low_balance_to_pay_fees = + replacement_test_helper + ~__LOC__ + ~title:"Low balance - second op's fees are equal to max_int64-1" + ~op1:default_op + ~op2:{default_op with fee = max_int} + ~size2:2 + ~incheck1:check_applied + ~incheck2:check_branch_delayed + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +(* Sum of fees of the 2nd operation overflow on int64. *) +let sum_fees_overflow = + replacement_test_helper + ~__LOC__ + ~title:"Sum of fees of the 2nd operation overflow on int64" + ~op1:default_op + ~op2:{default_op with fee = max_int} + ~size2:10 + ~incheck1:check_applied + ~incheck2:check_refused + ~postcheck2:(fun nodes h1 _h2 -> op_is_applied ~__LOC__ nodes h1) + () + +let register ~protocols = + identical_operations ~protocols ; + same_gas_and_fees_but_different_ops ~protocols ; + replacement_fees_below_threshold ~protocols ; + replacement_fees_equal_to_threshold ~protocols ; + replacement_fees_above_threshold ~protocols ; + third_operation_fees_below_replacement_threshold ~protocols ; + third_operation_fees_equal_to_replacement_threshold ~protocols ; + replacement_fees_equal_to_threshold_but_gas_increased ~protocols ; + replacement_fees_below_threshold_even_if_gas_is_decreased ~protocols ; + fees_of_second_op_below_fees_of_first_one ~protocols ; + cannot_replace_with_an_op_having_diffrent_counter ~protocols ; + cannot_replace_a_non_applied_operation ~protocols ; + replace_simple_op_with_a_batched_low_fees ~protocols ; + replace_simple_op_with_a_batched ~protocols ; + replace_batched_op_with_simple_one_low_fees ~protocols ; + replace_batched_op_with_simple_one ~protocols ; + low_balance_to_pay_fees ~protocols ; + sum_fees_overflow ~protocols