diff --git a/manifest/main.ml b/manifest/main.ml index 05ca455d10395214e40d7aab67ddc72e33c3c187..dd80993fcb6d87f9a761fe579eb97bf3b7b37bb8 100644 --- a/manifest/main.ml +++ b/manifest/main.ml @@ -5473,8 +5473,13 @@ let hash = Protocol.hash let _plugin_tests = opt_map (both plugin test_helpers) @@ fun (plugin, test_helpers) -> only_if active @@ fun () -> - tests - ["test_conflict_handler"; "test_consensus_filter"] + tezt + [ + "helpers"; + "test_conflict_handler"; + "test_consensus_filter"; + "test_fee_needed_to_overtake"; + ] ~path:(path // "lib_plugin/test") ~with_macos_security_framework:true ~synopsis:"Tezos/Protocol: protocol plugin tests" diff --git a/opam/tezos-protocol-plugin-016-PtMumbai-tests.opam b/opam/tezos-protocol-plugin-016-PtMumbai-tests.opam index e2dcb3ab0c73471e057a6363c9a7ae3b64abdfc2..a7e8bcd2d917634d0120a692411244da1dd6cdf1 100644 --- a/opam/tezos-protocol-plugin-016-PtMumbai-tests.opam +++ b/opam/tezos-protocol-plugin-016-PtMumbai-tests.opam @@ -10,6 +10,7 @@ license: "MIT" depends: [ "dune" { >= "3.0" } "ocaml" { >= "4.14" } + "tezt" { with-test & >= "3.1.0" } "tezos-base" {with-test} "tezos-base-test-helpers" {with-test} "octez-alcotezt" {with-test} diff --git a/opam/tezos-protocol-plugin-017-PtNairob-tests.opam b/opam/tezos-protocol-plugin-017-PtNairob-tests.opam index 72206fa162b4ceb62d9cf67a136ec0c3984f4368..5f31ffa263466405fa961163b45861ea42c53b02 100644 --- a/opam/tezos-protocol-plugin-017-PtNairob-tests.opam +++ b/opam/tezos-protocol-plugin-017-PtNairob-tests.opam @@ -10,6 +10,7 @@ license: "MIT" depends: [ "dune" { >= "3.0" } "ocaml" { >= "4.14" } + "tezt" { with-test & >= "3.1.0" } "tezos-base" {with-test} "tezos-base-test-helpers" {with-test} "octez-alcotezt" {with-test} diff --git a/opam/tezos-protocol-plugin-alpha-tests.opam b/opam/tezos-protocol-plugin-alpha-tests.opam index 32a0ac49eb30fe5d70bd9152b4c2e5d4f25b01a5..b9b12cefccb7a3ec3f3ae2bfbd1cbd767d2889a9 100644 --- a/opam/tezos-protocol-plugin-alpha-tests.opam +++ b/opam/tezos-protocol-plugin-alpha-tests.opam @@ -10,6 +10,7 @@ license: "MIT" depends: [ "dune" { >= "3.0" } "ocaml" { >= "4.14" } + "tezt" { with-test & >= "3.1.0" } "tezos-base" {with-test} "tezos-base-test-helpers" {with-test} "octez-alcotezt" {with-test} diff --git a/src/lib_shell/shell_plugin.ml b/src/lib_shell/shell_plugin.ml index a99e0558232b0263142029e69fac2de22b753cba..7a7782a68a1b104b4fe63d52add2c57cee7d1e4f 100644 --- a/src/lib_shell/shell_plugin.ml +++ b/src/lib_shell/shell_plugin.ml @@ -55,6 +55,11 @@ module type FILTER = sig Lwt.t val conflict_handler : config -> Proto.Mempool.conflict_handler + + val fee_needed_to_overtake : + op_to_overtake:Proto.operation -> + candidate_op:Proto.operation -> + int64 option end end @@ -88,6 +93,8 @@ module No_filter (Proto : Registered_protocol.T) : if Proto.compare_operations existing_operation new_operation < 0 then `Replace else `Keep + + let fee_needed_to_overtake ~op_to_overtake:_ ~candidate_op:_ = None end end diff --git a/src/lib_shell/shell_plugin.mli b/src/lib_shell/shell_plugin.mli index 4ac984943cf60742a21babf9a72cd07b8a6a1619..465ffe3946b551dcbbe24cf6a888ec514b4475de 100644 --- a/src/lib_shell/shell_plugin.mli +++ b/src/lib_shell/shell_plugin.mli @@ -87,6 +87,21 @@ module type FILTER = sig (required by the protocol's operation comparison on which the implementation of this function relies). *) val conflict_handler : config -> Proto.Mempool.conflict_handler + + (** Compute the minimal fee (expressed in mutez) that [candidate_op] + would need to have in order to be strictly greater than + [op_to_overtake] according to {!Proto.compare_operations}. + + Return [None] when at least one operation is not a manager operation. + + Also return [None] if both operations are manager operations but + there was an error while computing the needed fee. However, + note that this cannot happen when both manager operations have + been successfully validated by the protocol. *) + val fee_needed_to_overtake : + op_to_overtake:Proto.operation -> + candidate_op:Proto.operation -> + int64 option end end diff --git a/src/proto_016_PtMumbai/lib_plugin/mempool.ml b/src/proto_016_PtMumbai/lib_plugin/mempool.ml index 7391d705d8f4a641f9fe29f375af1dbe7d369645..1bd7893fcbcf415b5fa5892821533aa704656fcf 100644 --- a/src/proto_016_PtMumbai/lib_plugin/mempool.ml +++ b/src/proto_016_PtMumbai/lib_plugin/mempool.ml @@ -216,7 +216,7 @@ let consensus_prio = `High let other_prio = `Medium -let get_manager_operation_gas_and_fee contents = +let compute_manager_contents_fee_and_gas_limit contents = let open Operation in let l = to_list (Contents_list contents) in List.fold_left @@ -309,7 +309,7 @@ let pre_filter_manager : then `Fees_ok else `Refused [Environment.wrap_tzerror Fees_too_low] in - match get_manager_operation_gas_and_fee op with + match compute_manager_contents_fee_and_gas_limit op with | Error err -> `Refused (Environment.wrap_tztrace err) | Ok (fee, gas_limit) -> ( match check_gas_and_fee fee gas_limit with @@ -560,6 +560,10 @@ let is_manager_operation op = | Some pass -> Compare.Int.equal pass Operation_repr.manager_pass | None -> false +(* Should not fail on a valid manager operation. *) +let compute_fee_and_gas_limit {protocol_data = Operation_data data; _} = + compute_manager_contents_fee_and_gas_limit data.contents + (** Determine whether the new manager operation is sufficiently better than the old manager operation to replace it. Sufficiently better means that the new operation's fee and fee/gas ratio are both @@ -596,14 +600,8 @@ let conflict_handler config : Mempool.conflict_handler = if is_manager_operation old_op && is_manager_operation new_op then let new_op_is_better = let open Result_syntax in - let {protocol_data = Operation_data old_protocol_data; _} = old_op in - let {protocol_data = Operation_data new_protocol_data; _} = new_op in - let* old_fee, old_gas_limit = - get_manager_operation_gas_and_fee old_protocol_data.contents - in - let* new_fee, new_gas_limit = - get_manager_operation_gas_and_fee new_protocol_data.contents - in + let* old_fee, old_gas_limit = compute_fee_and_gas_limit old_op in + let* new_fee, new_gas_limit = compute_fee_and_gas_limit new_op in return (better_fees_and_ratio config @@ -618,6 +616,27 @@ let conflict_handler config : Mempool.conflict_handler = else if Operation.compare existing_operation new_operation < 0 then `Replace else `Keep +let fee_needed_to_overtake ~op_to_overtake ~candidate_op = + if is_manager_operation candidate_op && is_manager_operation op_to_overtake + then + (let open Result_syntax in + let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in + let* target_fee, target_gas = compute_fee_and_gas_limit op_to_overtake in + if Gas.Arith.(target_gas = zero || candidate_gas = zero) then + (* This should not happen when both operations are valid. *) + Result.return_none + else + (* Compute the target ratio as in {!Operation_repr.weight_manager}. *) + let target_fee = Q.of_int64 (Tez.to_mutez target_fee) in + let target_gas = Q.of_bigint (Gas.Arith.integral_to_z target_gas) in + let target_ratio = Q.(target_fee / target_gas) in + (* Compute the minimal fee needed to have a strictly greater ratio. *) + let candidate_gas = Q.of_bigint (Gas.Arith.integral_to_z candidate_gas) in + Result.return_some + (Int64.succ Q.(to_int64 (target_ratio * candidate_gas)))) + |> Option.of_result |> Option.join + else None + module Internal_for_tests = struct let default_config_with_clock_drift clock_drift = {default_config with clock_drift} diff --git a/src/proto_016_PtMumbai/lib_plugin/mempool.mli b/src/proto_016_PtMumbai/lib_plugin/mempool.mli index 7562f4aedc0527430cbe973419eda5b47b14fb9d..4cafa6a597c0c8bab929820a8de29e9b7beac7d6 100644 --- a/src/proto_016_PtMumbai/lib_plugin/mempool.mli +++ b/src/proto_016_PtMumbai/lib_plugin/mempool.mli @@ -103,6 +103,22 @@ val pre_filter : able to call {!Protocol.Alpha_context.Operation.compare}). *) val conflict_handler : config -> Protocol.Mempool.conflict_handler +(** Compute the minimal fee (expressed in mutez) that [candidate_op] would + need to have in order to be strictly greater than [op_to_overtake] + according to {!Protocol.Alpha_context.Operation.compare}, when both + operations are manager operations. + + Return [None] when at least one operation is not a manager operation. + + Also return [None] if both operations are manager operations but + there was an error while computing the needed fee. However, note + that this cannot happen when both manager operations have been + successfully validated by the protocol. *) +val fee_needed_to_overtake : + op_to_overtake:Protocol.Alpha_context.packed_operation -> + candidate_op:Protocol.Alpha_context.packed_operation -> + int64 option + (** The following type, encoding, and default values are exported for [bin_sc_rollup_node/configuration.ml]. *) diff --git a/src/proto_016_PtMumbai/lib_plugin/test/dune b/src/proto_016_PtMumbai/lib_plugin/test/dune index 5b57bcb6dfec4392e6f5d9dfb05c998b20653130..31637da7d1db84a016b8515b2e94e1e60c052e82 100644 --- a/src/proto_016_PtMumbai/lib_plugin/test/dune +++ b/src/proto_016_PtMumbai/lib_plugin/test/dune @@ -1,9 +1,11 @@ ; This file was automatically generated, do not edit. ; Edit file manifest/main.ml instead. -(executables - (names test_conflict_handler test_consensus_filter) +(library + (name src_proto_016_PtMumbai_lib_plugin_test_tezt_lib) + (instrumentation (backend bisect_ppx)) (libraries + tezt.core tezos-base tezos-base-test-helpers tezos-base.unix @@ -16,11 +18,11 @@ tezos-protocol-016-PtMumbai tezos-protocol-016-PtMumbai.parameters tezos-016-PtMumbai-test-helpers) - (link_flags - (:standard) - (:include %{workspace_root}/macos-link-flags.sexp)) + (library_flags (:standard -linkall)) (flags (:standard) + -open Tezt_core + -open Tezt_core.Base -open Tezos_base.TzPervasives -open Tezos_base.TzPervasives.Error_monad.Legacy_monad_globals -open Tezos_base_test_helpers @@ -31,14 +33,30 @@ -open Tezos_protocol_016_PtMumbai -open Tezos_protocol_016_PtMumbai.Protocol -open Tezos_protocol_016_PtMumbai_parameters - -open Tezos_016_PtMumbai_test_helpers)) + -open Tezos_016_PtMumbai_test_helpers) + (modules + helpers + test_conflict_handler + test_consensus_filter + test_fee_needed_to_overtake)) + +(executable + (name main) + (instrumentation (backend bisect_ppx --bisect-sigterm)) + (libraries + src_proto_016_PtMumbai_lib_plugin_test_tezt_lib + tezt) + (link_flags + (:standard) + (:include %{workspace_root}/macos-link-flags.sexp)) + (modules main)) (rule (alias runtest) (package tezos-protocol-plugin-016-PtMumbai-tests) - (action (run %{dep:./test_conflict_handler.exe}))) + (enabled_if (<> false %{env:RUNTEZTALIAS=true})) + (action (run %{dep:./main.exe}))) (rule - (alias runtest) - (package tezos-protocol-plugin-016-PtMumbai-tests) - (action (run %{dep:./test_consensus_filter.exe}))) + (targets main.ml) + (action (with-stdout-to %{targets} (echo "let () = Tezt.Test.run ()")))) diff --git a/src/proto_016_PtMumbai/lib_plugin/test/helpers.ml b/src/proto_016_PtMumbai/lib_plugin/test/helpers.ml new file mode 100644 index 0000000000000000000000000000000000000000..0534f0ed77780e23349581e3cb1499b276970cea --- /dev/null +++ b/src/proto_016_PtMumbai/lib_plugin/test/helpers.ml @@ -0,0 +1,98 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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. *) +(* *) +(*****************************************************************************) + +(** One-element list containing the Tezt tag of the local protocol, + e.g. "alpha", "nairobi", "mumbai", etc. *) +let proto_tags = Alcotezt_utils.is_proto_test __FILE__ + +(** Register a plugin test, with file-specific tags and prefix in title. *) +let register_test ~__FILE__ ~file_title ~file_tags ~title ~additional_tags = + Test.register + ~__FILE__ + ~title:(sf "%s/plugin: %s: %s" Protocol.name file_title title) + ~tags:(proto_tags @ ("plugin" :: (file_tags @ additional_tags))) + +(** Generator for a packed operation preceded by its hash. *) +let oph_and_op_gen = QCheck2.Gen.map snd Operation_generator.generate_operation + +(** Generator for a packed non-manager operation. *) +let non_manager_operation_gen = + Operation_generator.generate_non_manager_operation + +(** Generator for a packed manager operation. *) +let manager_operation_gen = + let open QCheck2.Gen in + let* batch_size = int_range 1 Operation_generator.max_batch_size in + Operation_generator.generate_manager_operation batch_size + +(** Generator for a packed manager operation with the specified + total fee and gas limit. *) +let manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas = + let open Alpha_context in + let open QCheck2.Gen in + let rec set_fee_and_gas : + type kind. _ -> _ -> kind contents_list -> kind contents_list t = + fun desired_total_fee desired_total_gas -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in + return (Single (Manager_operation {data with fee; gas_limit})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> int_range 0 desired_total_fee + | `Zero -> return 0 + | `All -> return desired_total_fee + in + let* local_gas = int_range 0 desired_total_gas in + let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn local_gas in + let* tail = + set_fee_and_gas + (desired_total_fee - local_fee) + (desired_total_gas - local_gas) + tail + in + return (Cons (Manager_operation {data with fee; gas_limit}, tail)) + | Single _ -> + (* This function is only called on a manager operation. *) assert false + in + (* Generate a random manager operation. *) + let* batch_size = int_range 1 Operation_generator.max_batch_size in + let* op = Operation_generator.generate_manager_operation batch_size in + (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) + let {shell = _; protocol_data = Operation_data protocol_data} = op in + let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in + let protocol_data = {protocol_data with contents} in + let op = {op with protocol_data = Operation_data protocol_data} in + return (Operation.hash_packed op, op) + +(** Generate a packed manager operation with the specified total fee + and gas limit. *) +let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = + QCheck2.Gen.generate1 (manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas) diff --git a/src/proto_016_PtMumbai/lib_plugin/test/test_conflict_handler.ml b/src/proto_016_PtMumbai/lib_plugin/test/test_conflict_handler.ml index dbed73715fa8f7a302702386de69df6eb83c7558..acbaf7fdb6131d6f8eb45d707ed1fb01826e8aa3 100644 --- a/src/proto_016_PtMumbai/lib_plugin/test/test_conflict_handler.ml +++ b/src/proto_016_PtMumbai/lib_plugin/test/test_conflict_handler.ml @@ -31,6 +31,12 @@ Subject: Unit tests the Mempool.conflict_handler function of the plugin *) +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"conflict_handler" + ~file_tags:["mempool"; "conflict_handler"] + let pp_answer fmt = function | `Keep -> Format.fprintf fmt "Keep" | `Replace -> Format.fprintf fmt "Replace" @@ -53,13 +59,17 @@ let is_manager_op ((_ : Operation_hash.t), op) = (** Test that when the operations are not both manager operations, the conflict handler picks the higher operation according to [Operation.compare]. *) -let test_random_ops () = +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> let ops = - let open Operation_generator in - QCheck2.Gen.(generate ~n:100 (pair generate_operation generate_operation)) + QCheck2.Gen.( + generate ~n:100 (pair Helpers.oph_and_op_gen Helpers.oph_and_op_gen)) in List.iter - (fun ((_, op1), (_, op2)) -> + (fun (op1, op2) -> let answer = Plugin.Mempool.conflict_handler Plugin.Mempool.default_config @@ -70,8 +80,8 @@ let test_random_ops () = (* When both operations are manager operations, the result is complicated and depends on the [config]. Testing it here would mean basically reimplementing - [conflict_handler]. Instead, we test this case in - [test_manager_ops] below. *) + [conflict_handler]. Instead, we test this case in the + "manager operations" test below. *) () else if (* When there is at least one non-manager operation, the @@ -81,51 +91,7 @@ let test_random_ops () = then check_answer ~__LOC__ `Keep answer else check_answer ~__LOC__ `Replace answer) ops ; - return_unit - -(** Generator for a manager batch with the specified total fee and gas. *) -let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = - let open Alpha_context in - let open QCheck2.Gen in - let rec set_fee_and_gas : - type kind. _ -> _ -> kind contents_list -> kind contents_list t = - fun desired_total_fee desired_total_gas -> function - | Single (Manager_operation data) -> - let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in - return (Single (Manager_operation {data with fee; gas_limit})) - | Cons (Manager_operation data, tail) -> - let* local_fee = - (* We generate some corner cases where some individual - operations in the batch have zero fees. *) - let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in - match r with - | `Random -> int_range 0 desired_total_fee - | `Zero -> return 0 - | `All -> return desired_total_fee - in - let* local_gas = int_range 0 desired_total_gas in - let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn local_gas in - let* tail = - set_fee_and_gas - (desired_total_fee - local_fee) - (desired_total_gas - local_gas) - tail - in - return (Cons (Manager_operation {data with fee; gas_limit}, tail)) - | Single _ -> - (* This function is only called on a manager operation. *) assert false - in - (* Generate a random manager operation. *) - let* batch_size = int_range 1 Operation_generator.max_batch_size in - let* op = Operation_generator.generate_manager_operation batch_size in - (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) - let {shell = _; protocol_data = Operation_data protocol_data} = op in - let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in - let protocol_data = {protocol_data with contents} in - let op = {op with protocol_data = Operation_data protocol_data} in - return (Operation.hash_packed op, op) + unit let check_conflict_handler ~__LOC__ config ~old ~nw expected = let answer = @@ -138,11 +104,12 @@ let check_conflict_handler ~__LOC__ config ~old ~nw expected = (** Test the semantics of the conflict handler on manager operations, with either hand-picked or carefully generated fee and gas. *) -let test_manager_ops () = - let make_op ~fee_in_mutez ~gas = - QCheck2.Gen.generate1 - (generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas) - in +let () = + register_test + ~title:"manager operations" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let make_op = Helpers.generate_manager_op_with_fee_and_gas in (* Test operations with specific fee and gas, using the default configuration. This configuration replaces the old operation when @@ -209,7 +176,7 @@ let test_manager_ops () = let* fee_in_mutez = int_range 0 (fee_more5 - 1) in let* gas = int_range 0 max_gas in Format.eprintf "op_not_fee5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_fee = generate ~n:repeat generator_not_5more_fee in List.iter @@ -227,7 +194,7 @@ let test_manager_ops () = let fee_upper_bound = Q.to_int fee_for_5more_ratio - 1 in let* fee_in_mutez = int_range 0 (max 0 fee_upper_bound) in Format.eprintf "op_not_ratio5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_ratio = generate ~n:repeat generator_not_5more_ratio in List.iter @@ -250,26 +217,10 @@ let test_manager_ops () = let fee_lower_bound = max fee_more5 (Q.to_int fee_for_5more_ratio + 1) in let* fee_in_mutez = int_range fee_lower_bound max_fee in Format.eprintf "op_both_better: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_both_5more = generate ~n:repeat generator_both_5more in List.iter (fun nw -> check_conflict_handler ~__LOC__ default ~old ~nw `Replace) ops_both_5more ; - return_unit - -let () = - Alcotest_lwt.run - ~__FILE__ - Protocol.name - [ - ( "conflict_handler", - [ - Tztest.tztest - "Random operations (not both manager)" - `Quick - test_random_ops; - Tztest.tztest "Manager operations" `Quick test_manager_ops; - ] ); - ] - |> Lwt_main.run + Lwt.return_unit diff --git a/src/proto_016_PtMumbai/lib_plugin/test/test_fee_needed_to_overtake.ml b/src/proto_016_PtMumbai/lib_plugin/test/test_fee_needed_to_overtake.ml new file mode 100644 index 0000000000000000000000000000000000000000..c7db5cc5bbd024738ffd2d7d088a44bd8eba223f --- /dev/null +++ b/src/proto_016_PtMumbai/lib_plugin/test/test_fee_needed_to_overtake.ml @@ -0,0 +1,236 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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: Plugin.Mempool + Invocation: dune exec src/proto_016_PtMumbai/lib_plugin/test/main.exe \ + -- --file test_fee_needed_to_overtake.ml + Subject: Unit tests the Mempool.fee_needed_to_overtake + function of the plugin +*) + +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"fee_needed_to_overtake" + ~file_tags:["mempool"; "fee_needed_to_overtake"] + +let rec iter_neighbors f = function + | [] | [_] -> () + | x :: (y :: _ as l) -> + f x y ; + iter_neighbors f l + +let iter2_exn f l1 l2 = + match List.iter2 ~when_different_lengths:() f l1 l2 with + | Ok () -> () + | Error () -> + Test.fail + ~__LOC__ + "Lists have respective lengths %d and %d." + (List.length l1) + (List.length l2) + +(** Test that [fee_needed_to_overtake] returns [None] when at least + one argument is a non-manager operation. *) +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> + let n = (* Number of non-manager operations to generate *) 30 in + let non_manager_ops = + QCheck2.Gen.generate ~n Helpers.non_manager_operation_gen + in + (* Test with two non-manager operations. *) + let test op_to_overtake candidate_op = + assert ( + Option.is_none + (Plugin.Mempool.fee_needed_to_overtake ~op_to_overtake ~candidate_op)) + in + iter_neighbors test non_manager_ops ; + (* Test with one non-manager and one manager operation. *) + let manager_ops = QCheck2.Gen.generate ~n Helpers.manager_operation_gen in + let test_both op1 op2 = + test op1 op2 ; + test op2 op1 + in + iter2_exn test_both non_manager_ops manager_ops ; + unit + +(** Change the total fee of the packed operation [op] to [fee] (in mutez) + and its source to {!Signature.Public_key_hash.zero}. + + Precondition: [op] must be a manager operation. *) +let set_fee_and_source fee op = + let open Alpha_context in + let open QCheck2.Gen in + let source = Signature.Public_key_hash.zero in + let rec set_fee_contents_list_gen : + type kind. int64 -> kind contents_list -> kind contents_list t = + fun desired_total_fee (* in mutez *) -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn desired_total_fee in + return (Single (Manager_operation {data with fee; source})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> + let* n = int_range 0 (Int64.to_int desired_total_fee) in + return (Int64.of_int n) + | `Zero -> return 0L + | `All -> return desired_total_fee + in + let fee = Tez.of_mutez_exn local_fee in + let* tail = + set_fee_contents_list_gen (Int64.sub desired_total_fee local_fee) tail + in + return (Cons (Manager_operation {data with fee; source}, tail)) + | Single _ -> (* see precondition: manager operation *) assert false + in + let {shell = _; protocol_data = Operation_data data} = op in + let contents = generate1 (set_fee_contents_list_gen fee data.contents) in + {op with protocol_data = Operation_data {data with contents}} + +(** Return an [Operation_hash.t] that is distinct from [different_from]. *) +let different_oph ~different_from = + if Operation_hash.(different_from = zero) then ( + let new_hash = Operation_hash.hash_string ["1"] in + assert (Operation_hash.(new_hash <> zero)) ; + new_hash) + else Operation_hash.zero + +(** Check that {!Plugin.Mempool.fee_needed_to_overtake} correctly + returns the minimal fee with which [candidate_op] would be + guaranteed to be greater than [op_to_overtake]. + + Precondition: both operations are manager operations with respective + total fee and gas limit [fee_o], [gas_o] and [fee_c], [gas_c]. *) +let test_manager_ops (op_to_overtake, fee_o, gas_o) (candidate_op, fee_c, gas_c) + = + Log.debug + "Test op_to_overtake: {fee=%dmutez; gas=%d} and candidate_op: \ + {fee=%dmutez; gas=%d}" + fee_o + gas_o + fee_c + gas_c ; + let fee_needed = + WithExceptions.Option.get ~loc:__LOC__ + @@ Plugin.Mempool.fee_needed_to_overtake + ~op_to_overtake:(snd op_to_overtake) + ~candidate_op:(snd candidate_op) + in + Log.debug " --> fee_needed: %Ld" fee_needed ; + (* We need to ensure that in the operation comparisons below, the + hashes provided as first elements of the pairs are distinct. + Indeed, {!Alpha_context.Operation.compare} always returns 0 when + these hashes are equal, regardless of the operations themselves. *) + let fake_oph = different_oph ~different_from:(fst op_to_overtake) in + (* We also set the source to {!Signature.Public_key_hash.zero} in + the operation that will be compared to [op_to_overtake], so that + if their weights (fee/gas ratio) are equal, then the former is + smaller (see [Operation_repr.compare_manager_weight]). *) + let with_fee fee = (fake_oph, set_fee_and_source fee (snd candidate_op)) in + let fee_smaller = Int64.sub fee_needed 1L in + if Alpha_context.Operation.compare (with_fee fee_smaller) op_to_overtake > 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee smaller than \ + fee_needed should be smaller than or equal to op_to_overtake: \ + {fee=%dmutez; gas=%d}" + fee_smaller + gas_c + fee_o + gas_o ; + if Alpha_context.Operation.compare (with_fee fee_needed) op_to_overtake <= 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee_needed should be \ + greater than op_to_overtake: {fee=%dmutez; gas=%d}" + fee_needed + gas_c + fee_o + gas_o + +(** Test manager operations with hand-picked fee and gas. *) +let () = + register_test + ~title:"hand-picked fee and gas" + ~additional_tags:["manager"; "handpicked"] + @@ fun () -> + (* Various relative gas limits and fees: equal, off by one, + multiple/divisor, high ppcm, coprime, zero, one, much higher/lower, etc. *) + let fee_in_mutez_and_gas_list = + [ + (1000, 1000); + (500, 1000); + (1000, 1001); + (1000, 999); + (1000, 500); + (1000, 4000); + (1000, 1200); + (333, 777); + (11, 7); + (1000, 31); + (1000, 1); + (1, 100_000); + (1_000_000, 100_001); + (0, 10); + ] + in + let ops = + List.map + (fun (fee_in_mutez, gas) -> + let op = + Helpers.generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + in + (op, fee_in_mutez, gas)) + fee_in_mutez_and_gas_list + in + List.iter (fun op -> List.iter (test_manager_ops op) ops) ops ; + unit + +(** Test manager operations with random fee and gas. *) +let () = + register_test + ~title:"random fee and gas" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let gen = + let open QCheck2.Gen in + let* fee_in_mutez = int_range 0 100_000_000 in + let* gas = int_range 1 50_000_000 in + let* op = Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in + return (op, fee_in_mutez, gas) + in + iter_neighbors test_manager_ops (QCheck2.Gen.generate ~n:100 gen) ; + unit diff --git a/src/proto_016_PtMumbai/lib_protocol/test/helpers/operation_generator.ml b/src/proto_016_PtMumbai/lib_protocol/test/helpers/operation_generator.ml index 06f477c7a18c6575c361bbf43e82b2b9bd130961..661e1a35376f86acc9c240dbec989658543c08ef 100644 --- a/src/proto_016_PtMumbai/lib_protocol/test/helpers/operation_generator.ml +++ b/src/proto_016_PtMumbai/lib_protocol/test/helpers/operation_generator.ml @@ -47,6 +47,8 @@ let manager_pass = `PManager let all_passes = [`PConsensus; `PAnonymous; `PVote; `PManager] +let all_non_manager_passes = [`PConsensus; `PAnonymous; `PVote] + let consensus_kinds = [`KPreendorsement; `KEndorsement; `KDal_attestation] let anonymous_kinds = @@ -800,6 +802,24 @@ let generator_of ?source = function | `KSc_rollup_recover_bond -> generate_manager_operation ?source generate_sc_rollup_recover_bond +let generate_non_manager_operation = + let open QCheck2.Gen in + let* pass = oneofl all_non_manager_passes in + let* kind = oneofl (pass_to_operation_kinds pass) in + match kind with + | `KPreendorsement -> generate_operation generate_preendorsement + | `KEndorsement -> generate_operation generate_endorsement + | `KDal_attestation -> generate_operation generate_dal_attestation + | `KSeed_nonce_revelation -> generate_operation generate_seed_nonce_revelation + | `KVdf_revelation -> generate_operation generate_vdf_revelation + | `KDouble_endorsement -> generate_operation generate_double_endorsement + | `KDouble_preendorsement -> generate_operation generate_double_preendorsement + | `KDouble_baking -> generate_operation generate_double_baking + | `KActivate_account -> generate_operation generate_activate_account + | `KProposals -> generate_operation generate_proposals + | `KBallot -> generate_operation generate_ballot + | `KManager -> assert false + let generate_manager_operation batch_size = let open QCheck2.Gen in let* source = random_pkh in diff --git a/src/proto_017_PtNairob/lib_plugin/mempool.ml b/src/proto_017_PtNairob/lib_plugin/mempool.ml index 7391d705d8f4a641f9fe29f375af1dbe7d369645..1bd7893fcbcf415b5fa5892821533aa704656fcf 100644 --- a/src/proto_017_PtNairob/lib_plugin/mempool.ml +++ b/src/proto_017_PtNairob/lib_plugin/mempool.ml @@ -216,7 +216,7 @@ let consensus_prio = `High let other_prio = `Medium -let get_manager_operation_gas_and_fee contents = +let compute_manager_contents_fee_and_gas_limit contents = let open Operation in let l = to_list (Contents_list contents) in List.fold_left @@ -309,7 +309,7 @@ let pre_filter_manager : then `Fees_ok else `Refused [Environment.wrap_tzerror Fees_too_low] in - match get_manager_operation_gas_and_fee op with + match compute_manager_contents_fee_and_gas_limit op with | Error err -> `Refused (Environment.wrap_tztrace err) | Ok (fee, gas_limit) -> ( match check_gas_and_fee fee gas_limit with @@ -560,6 +560,10 @@ let is_manager_operation op = | Some pass -> Compare.Int.equal pass Operation_repr.manager_pass | None -> false +(* Should not fail on a valid manager operation. *) +let compute_fee_and_gas_limit {protocol_data = Operation_data data; _} = + compute_manager_contents_fee_and_gas_limit data.contents + (** Determine whether the new manager operation is sufficiently better than the old manager operation to replace it. Sufficiently better means that the new operation's fee and fee/gas ratio are both @@ -596,14 +600,8 @@ let conflict_handler config : Mempool.conflict_handler = if is_manager_operation old_op && is_manager_operation new_op then let new_op_is_better = let open Result_syntax in - let {protocol_data = Operation_data old_protocol_data; _} = old_op in - let {protocol_data = Operation_data new_protocol_data; _} = new_op in - let* old_fee, old_gas_limit = - get_manager_operation_gas_and_fee old_protocol_data.contents - in - let* new_fee, new_gas_limit = - get_manager_operation_gas_and_fee new_protocol_data.contents - in + let* old_fee, old_gas_limit = compute_fee_and_gas_limit old_op in + let* new_fee, new_gas_limit = compute_fee_and_gas_limit new_op in return (better_fees_and_ratio config @@ -618,6 +616,27 @@ let conflict_handler config : Mempool.conflict_handler = else if Operation.compare existing_operation new_operation < 0 then `Replace else `Keep +let fee_needed_to_overtake ~op_to_overtake ~candidate_op = + if is_manager_operation candidate_op && is_manager_operation op_to_overtake + then + (let open Result_syntax in + let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in + let* target_fee, target_gas = compute_fee_and_gas_limit op_to_overtake in + if Gas.Arith.(target_gas = zero || candidate_gas = zero) then + (* This should not happen when both operations are valid. *) + Result.return_none + else + (* Compute the target ratio as in {!Operation_repr.weight_manager}. *) + let target_fee = Q.of_int64 (Tez.to_mutez target_fee) in + let target_gas = Q.of_bigint (Gas.Arith.integral_to_z target_gas) in + let target_ratio = Q.(target_fee / target_gas) in + (* Compute the minimal fee needed to have a strictly greater ratio. *) + let candidate_gas = Q.of_bigint (Gas.Arith.integral_to_z candidate_gas) in + Result.return_some + (Int64.succ Q.(to_int64 (target_ratio * candidate_gas)))) + |> Option.of_result |> Option.join + else None + module Internal_for_tests = struct let default_config_with_clock_drift clock_drift = {default_config with clock_drift} diff --git a/src/proto_017_PtNairob/lib_plugin/mempool.mli b/src/proto_017_PtNairob/lib_plugin/mempool.mli index 7562f4aedc0527430cbe973419eda5b47b14fb9d..4cafa6a597c0c8bab929820a8de29e9b7beac7d6 100644 --- a/src/proto_017_PtNairob/lib_plugin/mempool.mli +++ b/src/proto_017_PtNairob/lib_plugin/mempool.mli @@ -103,6 +103,22 @@ val pre_filter : able to call {!Protocol.Alpha_context.Operation.compare}). *) val conflict_handler : config -> Protocol.Mempool.conflict_handler +(** Compute the minimal fee (expressed in mutez) that [candidate_op] would + need to have in order to be strictly greater than [op_to_overtake] + according to {!Protocol.Alpha_context.Operation.compare}, when both + operations are manager operations. + + Return [None] when at least one operation is not a manager operation. + + Also return [None] if both operations are manager operations but + there was an error while computing the needed fee. However, note + that this cannot happen when both manager operations have been + successfully validated by the protocol. *) +val fee_needed_to_overtake : + op_to_overtake:Protocol.Alpha_context.packed_operation -> + candidate_op:Protocol.Alpha_context.packed_operation -> + int64 option + (** The following type, encoding, and default values are exported for [bin_sc_rollup_node/configuration.ml]. *) diff --git a/src/proto_017_PtNairob/lib_plugin/test/dune b/src/proto_017_PtNairob/lib_plugin/test/dune index 1b6756e593131ff72f05379102b55598c5667138..9040099225015831ab4d7be6ac03b4385554e7d8 100644 --- a/src/proto_017_PtNairob/lib_plugin/test/dune +++ b/src/proto_017_PtNairob/lib_plugin/test/dune @@ -1,9 +1,11 @@ ; This file was automatically generated, do not edit. ; Edit file manifest/main.ml instead. -(executables - (names test_conflict_handler test_consensus_filter) +(library + (name src_proto_017_PtNairob_lib_plugin_test_tezt_lib) + (instrumentation (backend bisect_ppx)) (libraries + tezt.core tezos-base tezos-base-test-helpers tezos-base.unix @@ -16,11 +18,11 @@ tezos-protocol-017-PtNairob tezos-protocol-017-PtNairob.parameters tezos-017-PtNairob-test-helpers) - (link_flags - (:standard) - (:include %{workspace_root}/macos-link-flags.sexp)) + (library_flags (:standard -linkall)) (flags (:standard) + -open Tezt_core + -open Tezt_core.Base -open Tezos_base.TzPervasives -open Tezos_base.TzPervasives.Error_monad.Legacy_monad_globals -open Tezos_base_test_helpers @@ -31,14 +33,30 @@ -open Tezos_protocol_017_PtNairob -open Tezos_protocol_017_PtNairob.Protocol -open Tezos_protocol_017_PtNairob_parameters - -open Tezos_017_PtNairob_test_helpers)) + -open Tezos_017_PtNairob_test_helpers) + (modules + helpers + test_conflict_handler + test_consensus_filter + test_fee_needed_to_overtake)) + +(executable + (name main) + (instrumentation (backend bisect_ppx --bisect-sigterm)) + (libraries + src_proto_017_PtNairob_lib_plugin_test_tezt_lib + tezt) + (link_flags + (:standard) + (:include %{workspace_root}/macos-link-flags.sexp)) + (modules main)) (rule (alias runtest) (package tezos-protocol-plugin-017-PtNairob-tests) - (action (run %{dep:./test_conflict_handler.exe}))) + (enabled_if (<> false %{env:RUNTEZTALIAS=true})) + (action (run %{dep:./main.exe}))) (rule - (alias runtest) - (package tezos-protocol-plugin-017-PtNairob-tests) - (action (run %{dep:./test_consensus_filter.exe}))) + (targets main.ml) + (action (with-stdout-to %{targets} (echo "let () = Tezt.Test.run ()")))) diff --git a/src/proto_017_PtNairob/lib_plugin/test/helpers.ml b/src/proto_017_PtNairob/lib_plugin/test/helpers.ml new file mode 100644 index 0000000000000000000000000000000000000000..0534f0ed77780e23349581e3cb1499b276970cea --- /dev/null +++ b/src/proto_017_PtNairob/lib_plugin/test/helpers.ml @@ -0,0 +1,98 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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. *) +(* *) +(*****************************************************************************) + +(** One-element list containing the Tezt tag of the local protocol, + e.g. "alpha", "nairobi", "mumbai", etc. *) +let proto_tags = Alcotezt_utils.is_proto_test __FILE__ + +(** Register a plugin test, with file-specific tags and prefix in title. *) +let register_test ~__FILE__ ~file_title ~file_tags ~title ~additional_tags = + Test.register + ~__FILE__ + ~title:(sf "%s/plugin: %s: %s" Protocol.name file_title title) + ~tags:(proto_tags @ ("plugin" :: (file_tags @ additional_tags))) + +(** Generator for a packed operation preceded by its hash. *) +let oph_and_op_gen = QCheck2.Gen.map snd Operation_generator.generate_operation + +(** Generator for a packed non-manager operation. *) +let non_manager_operation_gen = + Operation_generator.generate_non_manager_operation + +(** Generator for a packed manager operation. *) +let manager_operation_gen = + let open QCheck2.Gen in + let* batch_size = int_range 1 Operation_generator.max_batch_size in + Operation_generator.generate_manager_operation batch_size + +(** Generator for a packed manager operation with the specified + total fee and gas limit. *) +let manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas = + let open Alpha_context in + let open QCheck2.Gen in + let rec set_fee_and_gas : + type kind. _ -> _ -> kind contents_list -> kind contents_list t = + fun desired_total_fee desired_total_gas -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in + return (Single (Manager_operation {data with fee; gas_limit})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> int_range 0 desired_total_fee + | `Zero -> return 0 + | `All -> return desired_total_fee + in + let* local_gas = int_range 0 desired_total_gas in + let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn local_gas in + let* tail = + set_fee_and_gas + (desired_total_fee - local_fee) + (desired_total_gas - local_gas) + tail + in + return (Cons (Manager_operation {data with fee; gas_limit}, tail)) + | Single _ -> + (* This function is only called on a manager operation. *) assert false + in + (* Generate a random manager operation. *) + let* batch_size = int_range 1 Operation_generator.max_batch_size in + let* op = Operation_generator.generate_manager_operation batch_size in + (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) + let {shell = _; protocol_data = Operation_data protocol_data} = op in + let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in + let protocol_data = {protocol_data with contents} in + let op = {op with protocol_data = Operation_data protocol_data} in + return (Operation.hash_packed op, op) + +(** Generate a packed manager operation with the specified total fee + and gas limit. *) +let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = + QCheck2.Gen.generate1 (manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas) diff --git a/src/proto_017_PtNairob/lib_plugin/test/test_conflict_handler.ml b/src/proto_017_PtNairob/lib_plugin/test/test_conflict_handler.ml index d53b599ea22647f77d43584e101790b9076705a8..3091894dd8e1e862752d398199d1e6ca895a0049 100644 --- a/src/proto_017_PtNairob/lib_plugin/test/test_conflict_handler.ml +++ b/src/proto_017_PtNairob/lib_plugin/test/test_conflict_handler.ml @@ -31,6 +31,12 @@ Subject: Unit tests the Mempool.conflict_handler function of the plugin *) +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"conflict_handler" + ~file_tags:["mempool"; "conflict_handler"] + let pp_answer fmt = function | `Keep -> Format.fprintf fmt "Keep" | `Replace -> Format.fprintf fmt "Replace" @@ -53,13 +59,17 @@ let is_manager_op ((_ : Operation_hash.t), op) = (** Test that when the operations are not both manager operations, the conflict handler picks the higher operation according to [Operation.compare]. *) -let test_random_ops () = +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> let ops = - let open Operation_generator in - QCheck2.Gen.(generate ~n:100 (pair generate_operation generate_operation)) + QCheck2.Gen.( + generate ~n:100 (pair Helpers.oph_and_op_gen Helpers.oph_and_op_gen)) in List.iter - (fun ((_, op1), (_, op2)) -> + (fun (op1, op2) -> let answer = Plugin.Mempool.conflict_handler Plugin.Mempool.default_config @@ -70,8 +80,8 @@ let test_random_ops () = (* When both operations are manager operations, the result is complicated and depends on the [config]. Testing it here would mean basically reimplementing - [conflict_handler]. Instead, we test this case in - [test_manager_ops] below. *) + [conflict_handler]. Instead, we test this case in the + "manager operations" test below. *) () else if (* When there is at least one non-manager operation, the @@ -81,51 +91,7 @@ let test_random_ops () = then check_answer ~__LOC__ `Keep answer else check_answer ~__LOC__ `Replace answer) ops ; - return_unit - -(** Generator for a manager batch with the specified total fee and gas. *) -let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = - let open Alpha_context in - let open QCheck2.Gen in - let rec set_fee_and_gas : - type kind. _ -> _ -> kind contents_list -> kind contents_list t = - fun desired_total_fee desired_total_gas -> function - | Single (Manager_operation data) -> - let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in - return (Single (Manager_operation {data with fee; gas_limit})) - | Cons (Manager_operation data, tail) -> - let* local_fee = - (* We generate some corner cases where some individual - operations in the batch have zero fees. *) - let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in - match r with - | `Random -> int_range 0 desired_total_fee - | `Zero -> return 0 - | `All -> return desired_total_fee - in - let* local_gas = int_range 0 desired_total_gas in - let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn local_gas in - let* tail = - set_fee_and_gas - (desired_total_fee - local_fee) - (desired_total_gas - local_gas) - tail - in - return (Cons (Manager_operation {data with fee; gas_limit}, tail)) - | Single _ -> - (* This function is only called on a manager operation. *) assert false - in - (* Generate a random manager operation. *) - let* batch_size = int_range 1 Operation_generator.max_batch_size in - let* op = Operation_generator.generate_manager_operation batch_size in - (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) - let {shell = _; protocol_data = Operation_data protocol_data} = op in - let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in - let protocol_data = {protocol_data with contents} in - let op = {op with protocol_data = Operation_data protocol_data} in - return (Operation.hash_packed op, op) + unit let check_conflict_handler ~__LOC__ config ~old ~nw expected = let answer = @@ -138,11 +104,12 @@ let check_conflict_handler ~__LOC__ config ~old ~nw expected = (** Test the semantics of the conflict handler on manager operations, with either hand-picked or carefully generated fee and gas. *) -let test_manager_ops () = - let make_op ~fee_in_mutez ~gas = - QCheck2.Gen.generate1 - (generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas) - in +let () = + register_test + ~title:"manager operations" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let make_op = Helpers.generate_manager_op_with_fee_and_gas in (* Test operations with specific fee and gas, using the default configuration. This configuration replaces the old operation when @@ -209,7 +176,7 @@ let test_manager_ops () = let* fee_in_mutez = int_range 0 (fee_more5 - 1) in let* gas = int_range 0 max_gas in Format.eprintf "op_not_fee5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_fee = generate ~n:repeat generator_not_5more_fee in List.iter @@ -227,7 +194,7 @@ let test_manager_ops () = let fee_upper_bound = Q.to_int fee_for_5more_ratio - 1 in let* fee_in_mutez = int_range 0 (max 0 fee_upper_bound) in Format.eprintf "op_not_ratio5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_ratio = generate ~n:repeat generator_not_5more_ratio in List.iter @@ -250,26 +217,10 @@ let test_manager_ops () = let fee_lower_bound = max fee_more5 (Q.to_int fee_for_5more_ratio + 1) in let* fee_in_mutez = int_range fee_lower_bound max_fee in Format.eprintf "op_both_better: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_both_5more = generate ~n:repeat generator_both_5more in List.iter (fun nw -> check_conflict_handler ~__LOC__ default ~old ~nw `Replace) ops_both_5more ; - return_unit - -let () = - Alcotest_lwt.run - ~__FILE__ - Protocol.name - [ - ( "conflict_handler", - [ - Tztest.tztest - "Random operations (not both manager)" - `Quick - test_random_ops; - Tztest.tztest "Manager operations" `Quick test_manager_ops; - ] ); - ] - |> Lwt_main.run + Lwt.return_unit diff --git a/src/proto_017_PtNairob/lib_plugin/test/test_fee_needed_to_overtake.ml b/src/proto_017_PtNairob/lib_plugin/test/test_fee_needed_to_overtake.ml new file mode 100644 index 0000000000000000000000000000000000000000..87fa27131a9b954a8586521457fa55cf04ee4d58 --- /dev/null +++ b/src/proto_017_PtNairob/lib_plugin/test/test_fee_needed_to_overtake.ml @@ -0,0 +1,236 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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: Plugin.Mempool + Invocation: dune exec src/proto_017_PtNairob/lib_plugin/test/main.exe \ + -- --file test_fee_needed_to_overtake.ml + Subject: Unit tests the Mempool.fee_needed_to_overtake + function of the plugin +*) + +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"fee_needed_to_overtake" + ~file_tags:["mempool"; "fee_needed_to_overtake"] + +let rec iter_neighbors f = function + | [] | [_] -> () + | x :: (y :: _ as l) -> + f x y ; + iter_neighbors f l + +let iter2_exn f l1 l2 = + match List.iter2 ~when_different_lengths:() f l1 l2 with + | Ok () -> () + | Error () -> + Test.fail + ~__LOC__ + "Lists have respective lengths %d and %d." + (List.length l1) + (List.length l2) + +(** Test that [fee_needed_to_overtake] returns [None] when at least + one argument is a non-manager operation. *) +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> + let n = (* Number of non-manager operations to generate *) 30 in + let non_manager_ops = + QCheck2.Gen.generate ~n Helpers.non_manager_operation_gen + in + (* Test with two non-manager operations. *) + let test op_to_overtake candidate_op = + assert ( + Option.is_none + (Plugin.Mempool.fee_needed_to_overtake ~op_to_overtake ~candidate_op)) + in + iter_neighbors test non_manager_ops ; + (* Test with one non-manager and one manager operation. *) + let manager_ops = QCheck2.Gen.generate ~n Helpers.manager_operation_gen in + let test_both op1 op2 = + test op1 op2 ; + test op2 op1 + in + iter2_exn test_both non_manager_ops manager_ops ; + unit + +(** Change the total fee of the packed operation [op] to [fee] (in mutez) + and its source to {!Signature.Public_key_hash.zero}. + + Precondition: [op] must be a manager operation. *) +let set_fee_and_source fee op = + let open Alpha_context in + let open QCheck2.Gen in + let source = Signature.Public_key_hash.zero in + let rec set_fee_contents_list_gen : + type kind. int64 -> kind contents_list -> kind contents_list t = + fun desired_total_fee (* in mutez *) -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn desired_total_fee in + return (Single (Manager_operation {data with fee; source})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> + let* n = int_range 0 (Int64.to_int desired_total_fee) in + return (Int64.of_int n) + | `Zero -> return 0L + | `All -> return desired_total_fee + in + let fee = Tez.of_mutez_exn local_fee in + let* tail = + set_fee_contents_list_gen (Int64.sub desired_total_fee local_fee) tail + in + return (Cons (Manager_operation {data with fee; source}, tail)) + | Single _ -> (* see precondition: manager operation *) assert false + in + let {shell = _; protocol_data = Operation_data data} = op in + let contents = generate1 (set_fee_contents_list_gen fee data.contents) in + {op with protocol_data = Operation_data {data with contents}} + +(** Return an [Operation_hash.t] that is distinct from [different_from]. *) +let different_oph ~different_from = + if Operation_hash.(different_from = zero) then ( + let new_hash = Operation_hash.hash_string ["1"] in + assert (Operation_hash.(new_hash <> zero)) ; + new_hash) + else Operation_hash.zero + +(** Check that {!Plugin.Mempool.fee_needed_to_overtake} correctly + returns the minimal fee with which [candidate_op] would be + guaranteed to be greater than [op_to_overtake]. + + Precondition: both operations are manager operations with respective + total fee and gas limit [fee_o], [gas_o] and [fee_c], [gas_c]. *) +let test_manager_ops (op_to_overtake, fee_o, gas_o) (candidate_op, fee_c, gas_c) + = + Log.debug + "Test op_to_overtake: {fee=%dmutez; gas=%d} and candidate_op: \ + {fee=%dmutez; gas=%d}" + fee_o + gas_o + fee_c + gas_c ; + let fee_needed = + WithExceptions.Option.get ~loc:__LOC__ + @@ Plugin.Mempool.fee_needed_to_overtake + ~op_to_overtake:(snd op_to_overtake) + ~candidate_op:(snd candidate_op) + in + Log.debug " --> fee_needed: %Ld" fee_needed ; + (* We need to ensure that in the operation comparisons below, the + hashes provided as first elements of the pairs are distinct. + Indeed, {!Alpha_context.Operation.compare} always returns 0 when + these hashes are equal, regardless of the operations themselves. *) + let fake_oph = different_oph ~different_from:(fst op_to_overtake) in + (* We also set the source to {!Signature.Public_key_hash.zero} in + the operation that will be compared to [op_to_overtake], so that + if their weights (fee/gas ratio) are equal, then the former is + smaller (see [Operation_repr.compare_manager_weight]). *) + let with_fee fee = (fake_oph, set_fee_and_source fee (snd candidate_op)) in + let fee_smaller = Int64.sub fee_needed 1L in + if Alpha_context.Operation.compare (with_fee fee_smaller) op_to_overtake > 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee smaller than \ + fee_needed should be smaller than or equal to op_to_overtake: \ + {fee=%dmutez; gas=%d}" + fee_smaller + gas_c + fee_o + gas_o ; + if Alpha_context.Operation.compare (with_fee fee_needed) op_to_overtake <= 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee_needed should be \ + greater than op_to_overtake: {fee=%dmutez; gas=%d}" + fee_needed + gas_c + fee_o + gas_o + +(** Test manager operations with hand-picked fee and gas. *) +let () = + register_test + ~title:"hand-picked fee and gas" + ~additional_tags:["manager"; "handpicked"] + @@ fun () -> + (* Various relative gas limits and fees: equal, off by one, + multiple/divisor, high ppcm, coprime, zero, one, much higher/lower, etc. *) + let fee_in_mutez_and_gas_list = + [ + (1000, 1000); + (500, 1000); + (1000, 1001); + (1000, 999); + (1000, 500); + (1000, 4000); + (1000, 1200); + (333, 777); + (11, 7); + (1000, 31); + (1000, 1); + (1, 100_000); + (1_000_000, 100_001); + (0, 10); + ] + in + let ops = + List.map + (fun (fee_in_mutez, gas) -> + let op = + Helpers.generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + in + (op, fee_in_mutez, gas)) + fee_in_mutez_and_gas_list + in + List.iter (fun op -> List.iter (test_manager_ops op) ops) ops ; + unit + +(** Test manager operations with random fee and gas. *) +let () = + register_test + ~title:"random fee and gas" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let gen = + let open QCheck2.Gen in + let* fee_in_mutez = int_range 0 100_000_000 in + let* gas = int_range 1 50_000_000 in + let* op = Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in + return (op, fee_in_mutez, gas) + in + iter_neighbors test_manager_ops (QCheck2.Gen.generate ~n:100 gen) ; + unit diff --git a/src/proto_017_PtNairob/lib_protocol/test/helpers/operation_generator.ml b/src/proto_017_PtNairob/lib_protocol/test/helpers/operation_generator.ml index 95e977fc9ee2aed4cafa86bdce718f82f0514827..ce00e9d4f8a848939794e342f38ace74174fd9bc 100644 --- a/src/proto_017_PtNairob/lib_protocol/test/helpers/operation_generator.ml +++ b/src/proto_017_PtNairob/lib_protocol/test/helpers/operation_generator.ml @@ -47,6 +47,8 @@ let manager_pass = `PManager let all_passes = [`PConsensus; `PAnonymous; `PVote; `PManager] +let all_non_manager_passes = [`PConsensus; `PAnonymous; `PVote] + let consensus_kinds = [`KPreendorsement; `KEndorsement; `KDal_attestation] let anonymous_kinds = @@ -638,6 +640,24 @@ let generator_of ?source = function | `KSc_rollup_recover_bond -> generate_manager_operation ?source generate_sc_rollup_recover_bond +let generate_non_manager_operation = + let open QCheck2.Gen in + let* pass = oneofl all_non_manager_passes in + let* kind = oneofl (pass_to_operation_kinds pass) in + match kind with + | `KPreendorsement -> generate_operation generate_preendorsement + | `KEndorsement -> generate_operation generate_endorsement + | `KDal_attestation -> generate_operation generate_dal_attestation + | `KSeed_nonce_revelation -> generate_operation generate_seed_nonce_revelation + | `KVdf_revelation -> generate_operation generate_vdf_revelation + | `KDouble_endorsement -> generate_operation generate_double_endorsement + | `KDouble_preendorsement -> generate_operation generate_double_preendorsement + | `KDouble_baking -> generate_operation generate_double_baking + | `KActivate_account -> generate_operation generate_activate_account + | `KProposals -> generate_operation generate_proposals + | `KBallot -> generate_operation generate_ballot + | `KManager -> assert false + let generate_manager_operation batch_size = let open QCheck2.Gen in let* source = random_pkh in diff --git a/src/proto_alpha/lib_plugin/mempool.ml b/src/proto_alpha/lib_plugin/mempool.ml index f078f3d64137af0956b8149b9a01b34de5f5bf6c..be6587427d3b93ad1c1748338a1c113c5f2b7fe7 100644 --- a/src/proto_alpha/lib_plugin/mempool.ml +++ b/src/proto_alpha/lib_plugin/mempool.ml @@ -216,7 +216,7 @@ let consensus_prio = `High let other_prio = `Medium -let get_manager_operation_gas_and_fee contents = +let compute_manager_contents_fee_and_gas_limit contents = let open Operation in let l = to_list (Contents_list contents) in List.fold_left @@ -311,7 +311,7 @@ let pre_filter_manager : then `Fees_ok else `Refused [Environment.wrap_tzerror Fees_too_low] in - match get_manager_operation_gas_and_fee op with + match compute_manager_contents_fee_and_gas_limit op with | Error err -> `Refused (Environment.wrap_tztrace err) | Ok (fee, gas_limit) -> ( match check_gas_and_fee fee gas_limit with @@ -562,6 +562,10 @@ let is_manager_operation op = | Some pass -> Compare.Int.equal pass Operation_repr.manager_pass | None -> false +(* Should not fail on a valid manager operation. *) +let compute_fee_and_gas_limit {protocol_data = Operation_data data; _} = + compute_manager_contents_fee_and_gas_limit data.contents + (** Determine whether the new manager operation is sufficiently better than the old manager operation to replace it. Sufficiently better means that the new operation's fee and fee/gas ratio are both @@ -598,14 +602,8 @@ let conflict_handler config : Mempool.conflict_handler = if is_manager_operation old_op && is_manager_operation new_op then let new_op_is_better = let open Result_syntax in - let {protocol_data = Operation_data old_protocol_data; _} = old_op in - let {protocol_data = Operation_data new_protocol_data; _} = new_op in - let* old_fee, old_gas_limit = - get_manager_operation_gas_and_fee old_protocol_data.contents - in - let* new_fee, new_gas_limit = - get_manager_operation_gas_and_fee new_protocol_data.contents - in + let* old_fee, old_gas_limit = compute_fee_and_gas_limit old_op in + let* new_fee, new_gas_limit = compute_fee_and_gas_limit new_op in return (better_fees_and_ratio config @@ -620,6 +618,27 @@ let conflict_handler config : Mempool.conflict_handler = else if Operation.compare existing_operation new_operation < 0 then `Replace else `Keep +let fee_needed_to_overtake ~op_to_overtake ~candidate_op = + if is_manager_operation candidate_op && is_manager_operation op_to_overtake + then + (let open Result_syntax in + let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in + let* target_fee, target_gas = compute_fee_and_gas_limit op_to_overtake in + if Gas.Arith.(target_gas = zero || candidate_gas = zero) then + (* This should not happen when both operations are valid. *) + Result.return_none + else + (* Compute the target ratio as in {!Operation_repr.weight_manager}. *) + let target_fee = Q.of_int64 (Tez.to_mutez target_fee) in + let target_gas = Q.of_bigint (Gas.Arith.integral_to_z target_gas) in + let target_ratio = Q.(target_fee / target_gas) in + (* Compute the minimal fee needed to have a strictly greater ratio. *) + let candidate_gas = Q.of_bigint (Gas.Arith.integral_to_z candidate_gas) in + Result.return_some + (Int64.succ Q.(to_int64 (target_ratio * candidate_gas)))) + |> Option.of_result |> Option.join + else None + module Internal_for_tests = struct let default_config_with_clock_drift clock_drift = {default_config with clock_drift} diff --git a/src/proto_alpha/lib_plugin/mempool.mli b/src/proto_alpha/lib_plugin/mempool.mli index 7562f4aedc0527430cbe973419eda5b47b14fb9d..4cafa6a597c0c8bab929820a8de29e9b7beac7d6 100644 --- a/src/proto_alpha/lib_plugin/mempool.mli +++ b/src/proto_alpha/lib_plugin/mempool.mli @@ -103,6 +103,22 @@ val pre_filter : able to call {!Protocol.Alpha_context.Operation.compare}). *) val conflict_handler : config -> Protocol.Mempool.conflict_handler +(** Compute the minimal fee (expressed in mutez) that [candidate_op] would + need to have in order to be strictly greater than [op_to_overtake] + according to {!Protocol.Alpha_context.Operation.compare}, when both + operations are manager operations. + + Return [None] when at least one operation is not a manager operation. + + Also return [None] if both operations are manager operations but + there was an error while computing the needed fee. However, note + that this cannot happen when both manager operations have been + successfully validated by the protocol. *) +val fee_needed_to_overtake : + op_to_overtake:Protocol.Alpha_context.packed_operation -> + candidate_op:Protocol.Alpha_context.packed_operation -> + int64 option + (** The following type, encoding, and default values are exported for [bin_sc_rollup_node/configuration.ml]. *) diff --git a/src/proto_alpha/lib_plugin/test/dune b/src/proto_alpha/lib_plugin/test/dune index 16c62fa130db18e639e363208f5a19483766e759..17d18334e7ed9c331235299188d0651db12d49d9 100644 --- a/src/proto_alpha/lib_plugin/test/dune +++ b/src/proto_alpha/lib_plugin/test/dune @@ -1,9 +1,11 @@ ; This file was automatically generated, do not edit. ; Edit file manifest/main.ml instead. -(executables - (names test_conflict_handler test_consensus_filter) +(library + (name src_proto_alpha_lib_plugin_test_tezt_lib) + (instrumentation (backend bisect_ppx)) (libraries + tezt.core tezos-base tezos-base-test-helpers tezos-base.unix @@ -16,11 +18,11 @@ tezos-protocol-alpha tezos-protocol-alpha.parameters tezos-alpha-test-helpers) - (link_flags - (:standard) - (:include %{workspace_root}/macos-link-flags.sexp)) + (library_flags (:standard -linkall)) (flags (:standard) + -open Tezt_core + -open Tezt_core.Base -open Tezos_base.TzPervasives -open Tezos_base.TzPervasives.Error_monad.Legacy_monad_globals -open Tezos_base_test_helpers @@ -31,14 +33,30 @@ -open Tezos_protocol_alpha -open Tezos_protocol_alpha.Protocol -open Tezos_protocol_alpha_parameters - -open Tezos_alpha_test_helpers)) + -open Tezos_alpha_test_helpers) + (modules + helpers + test_conflict_handler + test_consensus_filter + test_fee_needed_to_overtake)) + +(executable + (name main) + (instrumentation (backend bisect_ppx --bisect-sigterm)) + (libraries + src_proto_alpha_lib_plugin_test_tezt_lib + tezt) + (link_flags + (:standard) + (:include %{workspace_root}/macos-link-flags.sexp)) + (modules main)) (rule (alias runtest) (package tezos-protocol-plugin-alpha-tests) - (action (run %{dep:./test_conflict_handler.exe}))) + (enabled_if (<> false %{env:RUNTEZTALIAS=true})) + (action (run %{dep:./main.exe}))) (rule - (alias runtest) - (package tezos-protocol-plugin-alpha-tests) - (action (run %{dep:./test_consensus_filter.exe}))) + (targets main.ml) + (action (with-stdout-to %{targets} (echo "let () = Tezt.Test.run ()")))) diff --git a/src/proto_alpha/lib_plugin/test/helpers.ml b/src/proto_alpha/lib_plugin/test/helpers.ml new file mode 100644 index 0000000000000000000000000000000000000000..0534f0ed77780e23349581e3cb1499b276970cea --- /dev/null +++ b/src/proto_alpha/lib_plugin/test/helpers.ml @@ -0,0 +1,98 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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. *) +(* *) +(*****************************************************************************) + +(** One-element list containing the Tezt tag of the local protocol, + e.g. "alpha", "nairobi", "mumbai", etc. *) +let proto_tags = Alcotezt_utils.is_proto_test __FILE__ + +(** Register a plugin test, with file-specific tags and prefix in title. *) +let register_test ~__FILE__ ~file_title ~file_tags ~title ~additional_tags = + Test.register + ~__FILE__ + ~title:(sf "%s/plugin: %s: %s" Protocol.name file_title title) + ~tags:(proto_tags @ ("plugin" :: (file_tags @ additional_tags))) + +(** Generator for a packed operation preceded by its hash. *) +let oph_and_op_gen = QCheck2.Gen.map snd Operation_generator.generate_operation + +(** Generator for a packed non-manager operation. *) +let non_manager_operation_gen = + Operation_generator.generate_non_manager_operation + +(** Generator for a packed manager operation. *) +let manager_operation_gen = + let open QCheck2.Gen in + let* batch_size = int_range 1 Operation_generator.max_batch_size in + Operation_generator.generate_manager_operation batch_size + +(** Generator for a packed manager operation with the specified + total fee and gas limit. *) +let manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas = + let open Alpha_context in + let open QCheck2.Gen in + let rec set_fee_and_gas : + type kind. _ -> _ -> kind contents_list -> kind contents_list t = + fun desired_total_fee desired_total_gas -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in + return (Single (Manager_operation {data with fee; gas_limit})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> int_range 0 desired_total_fee + | `Zero -> return 0 + | `All -> return desired_total_fee + in + let* local_gas = int_range 0 desired_total_gas in + let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in + let gas_limit = Gas.Arith.integral_of_int_exn local_gas in + let* tail = + set_fee_and_gas + (desired_total_fee - local_fee) + (desired_total_gas - local_gas) + tail + in + return (Cons (Manager_operation {data with fee; gas_limit}, tail)) + | Single _ -> + (* This function is only called on a manager operation. *) assert false + in + (* Generate a random manager operation. *) + let* batch_size = int_range 1 Operation_generator.max_batch_size in + let* op = Operation_generator.generate_manager_operation batch_size in + (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) + let {shell = _; protocol_data = Operation_data protocol_data} = op in + let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in + let protocol_data = {protocol_data with contents} in + let op = {op with protocol_data = Operation_data protocol_data} in + return (Operation.hash_packed op, op) + +(** Generate a packed manager operation with the specified total fee + and gas limit. *) +let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = + QCheck2.Gen.generate1 (manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas) diff --git a/src/proto_alpha/lib_plugin/test/test_conflict_handler.ml b/src/proto_alpha/lib_plugin/test/test_conflict_handler.ml index 7593af3d8dc7b28ba86d154af08c88aa8d0b7b0f..cf042d3db7199b58a5ffaea254f383e036149af8 100644 --- a/src/proto_alpha/lib_plugin/test/test_conflict_handler.ml +++ b/src/proto_alpha/lib_plugin/test/test_conflict_handler.ml @@ -31,6 +31,12 @@ Subject: Unit tests the Mempool.conflict_handler function of the plugin *) +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"conflict_handler" + ~file_tags:["mempool"; "conflict_handler"] + let pp_answer fmt = function | `Keep -> Format.fprintf fmt "Keep" | `Replace -> Format.fprintf fmt "Replace" @@ -53,13 +59,17 @@ let is_manager_op ((_ : Operation_hash.t), op) = (** Test that when the operations are not both manager operations, the conflict handler picks the higher operation according to [Operation.compare]. *) -let test_random_ops () = +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> let ops = - let open Operation_generator in - QCheck2.Gen.(generate ~n:100 (pair generate_operation generate_operation)) + QCheck2.Gen.( + generate ~n:100 (pair Helpers.oph_and_op_gen Helpers.oph_and_op_gen)) in List.iter - (fun ((_, op1), (_, op2)) -> + (fun (op1, op2) -> let answer = Plugin.Mempool.conflict_handler Plugin.Mempool.default_config @@ -70,8 +80,8 @@ let test_random_ops () = (* When both operations are manager operations, the result is complicated and depends on the [config]. Testing it here would mean basically reimplementing - [conflict_handler]. Instead, we test this case in - [test_manager_ops] below. *) + [conflict_handler]. Instead, we test this case in the + "manager operations" test below. *) () else if (* When there is at least one non-manager operation, the @@ -81,51 +91,7 @@ let test_random_ops () = then check_answer ~__LOC__ `Keep answer else check_answer ~__LOC__ `Replace answer) ops ; - return_unit - -(** Generator for a manager batch with the specified total fee and gas. *) -let generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas = - let open Alpha_context in - let open QCheck2.Gen in - let rec set_fee_and_gas : - type kind. _ -> _ -> kind contents_list -> kind contents_list t = - fun desired_total_fee desired_total_gas -> function - | Single (Manager_operation data) -> - let fee = Tez.of_mutez_exn (Int64.of_int desired_total_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn desired_total_gas in - return (Single (Manager_operation {data with fee; gas_limit})) - | Cons (Manager_operation data, tail) -> - let* local_fee = - (* We generate some corner cases where some individual - operations in the batch have zero fees. *) - let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in - match r with - | `Random -> int_range 0 desired_total_fee - | `Zero -> return 0 - | `All -> return desired_total_fee - in - let* local_gas = int_range 0 desired_total_gas in - let fee = Tez.of_mutez_exn (Int64.of_int local_fee) in - let gas_limit = Gas.Arith.integral_of_int_exn local_gas in - let* tail = - set_fee_and_gas - (desired_total_fee - local_fee) - (desired_total_gas - local_gas) - tail - in - return (Cons (Manager_operation {data with fee; gas_limit}, tail)) - | Single _ -> - (* This function is only called on a manager operation. *) assert false - in - (* Generate a random manager operation. *) - let* batch_size = int_range 1 Operation_generator.max_batch_size in - let* op = Operation_generator.generate_manager_operation batch_size in - (* Modify its fee and gas to match the [fee_in_mutez] and [gas] inputs. *) - let {shell = _; protocol_data = Operation_data protocol_data} = op in - let* contents = set_fee_and_gas fee_in_mutez gas protocol_data.contents in - let protocol_data = {protocol_data with contents} in - let op = {op with protocol_data = Operation_data protocol_data} in - return (Operation.hash_packed op, op) + unit let check_conflict_handler ~__LOC__ config ~old ~nw expected = let answer = @@ -138,11 +104,12 @@ let check_conflict_handler ~__LOC__ config ~old ~nw expected = (** Test the semantics of the conflict handler on manager operations, with either hand-picked or carefully generated fee and gas. *) -let test_manager_ops () = - let make_op ~fee_in_mutez ~gas = - QCheck2.Gen.generate1 - (generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas) - in +let () = + register_test + ~title:"manager operations" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let make_op = Helpers.generate_manager_op_with_fee_and_gas in (* Test operations with specific fee and gas, using the default configuration. This configuration replaces the old operation when @@ -209,7 +176,7 @@ let test_manager_ops () = let* fee_in_mutez = int_range 0 (fee_more5 - 1) in let* gas = int_range 0 max_gas in Format.eprintf "op_not_fee5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_fee = generate ~n:repeat generator_not_5more_fee in List.iter @@ -227,7 +194,7 @@ let test_manager_ops () = let fee_upper_bound = Q.to_int fee_for_5more_ratio - 1 in let* fee_in_mutez = int_range 0 (max 0 fee_upper_bound) in Format.eprintf "op_not_ratio5: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_not_5more_ratio = generate ~n:repeat generator_not_5more_ratio in List.iter @@ -250,26 +217,10 @@ let test_manager_ops () = let fee_lower_bound = max fee_more5 (Q.to_int fee_for_5more_ratio + 1) in let* fee_in_mutez = int_range fee_lower_bound max_fee in Format.eprintf "op_both_better: fee = %d; gas = %d@." fee_in_mutez gas ; - generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in let ops_both_5more = generate ~n:repeat generator_both_5more in List.iter (fun nw -> check_conflict_handler ~__LOC__ default ~old ~nw `Replace) ops_both_5more ; - return_unit - -let () = - Alcotest_lwt.run - ~__FILE__ - Protocol.name - [ - ( "conflict_handler", - [ - Tztest.tztest - "Random operations (not both manager)" - `Quick - test_random_ops; - Tztest.tztest "Manager operations" `Quick test_manager_ops; - ] ); - ] - |> Lwt_main.run + Lwt.return_unit diff --git a/src/proto_alpha/lib_plugin/test/test_fee_needed_to_overtake.ml b/src/proto_alpha/lib_plugin/test/test_fee_needed_to_overtake.ml new file mode 100644 index 0000000000000000000000000000000000000000..91e2a99b38c9d807c486153995a81dbec6bc3d91 --- /dev/null +++ b/src/proto_alpha/lib_plugin/test/test_fee_needed_to_overtake.ml @@ -0,0 +1,236 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 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: Plugin.Mempool + Invocation: dune exec src/proto_alpha/lib_plugin/test/main.exe \ + -- --file test_fee_needed_to_overtake.ml + Subject: Unit tests the Mempool.fee_needed_to_overtake + function of the plugin +*) + +let register_test = + Helpers.register_test + ~__FILE__ + ~file_title:"fee_needed_to_overtake" + ~file_tags:["mempool"; "fee_needed_to_overtake"] + +let rec iter_neighbors f = function + | [] | [_] -> () + | x :: (y :: _ as l) -> + f x y ; + iter_neighbors f l + +let iter2_exn f l1 l2 = + match List.iter2 ~when_different_lengths:() f l1 l2 with + | Ok () -> () + | Error () -> + Test.fail + ~__LOC__ + "Lists have respective lengths %d and %d." + (List.length l1) + (List.length l2) + +(** Test that [fee_needed_to_overtake] returns [None] when at least + one argument is a non-manager operation. *) +let () = + register_test + ~title:"non-manager operations" + ~additional_tags:["nonmanager"; "random"] + @@ fun () -> + let n = (* Number of non-manager operations to generate *) 30 in + let non_manager_ops = + QCheck2.Gen.generate ~n Helpers.non_manager_operation_gen + in + (* Test with two non-manager operations. *) + let test op_to_overtake candidate_op = + assert ( + Option.is_none + (Plugin.Mempool.fee_needed_to_overtake ~op_to_overtake ~candidate_op)) + in + iter_neighbors test non_manager_ops ; + (* Test with one non-manager and one manager operation. *) + let manager_ops = QCheck2.Gen.generate ~n Helpers.manager_operation_gen in + let test_both op1 op2 = + test op1 op2 ; + test op2 op1 + in + iter2_exn test_both non_manager_ops manager_ops ; + unit + +(** Change the total fee of the packed operation [op] to [fee] (in mutez) + and its source to {!Signature.Public_key_hash.zero}. + + Precondition: [op] must be a manager operation. *) +let set_fee_and_source fee op = + let open Alpha_context in + let open QCheck2.Gen in + let source = Signature.Public_key_hash.zero in + let rec set_fee_contents_list_gen : + type kind. int64 -> kind contents_list -> kind contents_list t = + fun desired_total_fee (* in mutez *) -> function + | Single (Manager_operation data) -> + let fee = Tez.of_mutez_exn desired_total_fee in + return (Single (Manager_operation {data with fee; source})) + | Cons (Manager_operation data, tail) -> + let* local_fee = + (* We generate some corner cases where some individual + operations in the batch have zero fees. *) + let* r = frequencyl [(7, `Random); (2, `Zero); (1, `All)] in + match r with + | `Random -> + let* n = int_range 0 (Int64.to_int desired_total_fee) in + return (Int64.of_int n) + | `Zero -> return 0L + | `All -> return desired_total_fee + in + let fee = Tez.of_mutez_exn local_fee in + let* tail = + set_fee_contents_list_gen (Int64.sub desired_total_fee local_fee) tail + in + return (Cons (Manager_operation {data with fee; source}, tail)) + | Single _ -> (* see precondition: manager operation *) assert false + in + let {shell = _; protocol_data = Operation_data data} = op in + let contents = generate1 (set_fee_contents_list_gen fee data.contents) in + {op with protocol_data = Operation_data {data with contents}} + +(** Return an [Operation_hash.t] that is distinct from [different_from]. *) +let different_oph ~different_from = + if Operation_hash.(different_from = zero) then ( + let new_hash = Operation_hash.hash_string ["1"] in + assert (Operation_hash.(new_hash <> zero)) ; + new_hash) + else Operation_hash.zero + +(** Check that {!Plugin.Mempool.fee_needed_to_overtake} correctly + returns the minimal fee with which [candidate_op] would be + guaranteed to be greater than [op_to_overtake]. + + Precondition: both operations are manager operations with respective + total fee and gas limit [fee_o], [gas_o] and [fee_c], [gas_c]. *) +let test_manager_ops (op_to_overtake, fee_o, gas_o) (candidate_op, fee_c, gas_c) + = + Log.debug + "Test op_to_overtake: {fee=%dmutez; gas=%d} and candidate_op: \ + {fee=%dmutez; gas=%d}" + fee_o + gas_o + fee_c + gas_c ; + let fee_needed = + WithExceptions.Option.get ~loc:__LOC__ + @@ Plugin.Mempool.fee_needed_to_overtake + ~op_to_overtake:(snd op_to_overtake) + ~candidate_op:(snd candidate_op) + in + Log.debug " --> fee_needed: %Ld" fee_needed ; + (* We need to ensure that in the operation comparisons below, the + hashes provided as first elements of the pairs are distinct. + Indeed, {!Alpha_context.Operation.compare} always returns 0 when + these hashes are equal, regardless of the operations themselves. *) + let fake_oph = different_oph ~different_from:(fst op_to_overtake) in + (* We also set the source to {!Signature.Public_key_hash.zero} in + the operation that will be compared to [op_to_overtake], so that + if their weights (fee/gas ratio) are equal, then the former is + smaller (see [Operation_repr.compare_manager_weight]). *) + let with_fee fee = (fake_oph, set_fee_and_source fee (snd candidate_op)) in + let fee_smaller = Int64.sub fee_needed 1L in + if Alpha_context.Operation.compare (with_fee fee_smaller) op_to_overtake > 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee smaller than \ + fee_needed should be smaller than or equal to op_to_overtake: \ + {fee=%dmutez; gas=%d}" + fee_smaller + gas_c + fee_o + gas_o ; + if Alpha_context.Operation.compare (with_fee fee_needed) op_to_overtake <= 0 + then + Test.fail + ~__LOC__ + "Adjusted candidate_op: {fee=%Ldmutez; gas=%d} with fee_needed should be \ + greater than op_to_overtake: {fee=%dmutez; gas=%d}" + fee_needed + gas_c + fee_o + gas_o + +(** Test manager operations with hand-picked fee and gas. *) +let () = + register_test + ~title:"hand-picked fee and gas" + ~additional_tags:["manager"; "handpicked"] + @@ fun () -> + (* Various relative gas limits and fees: equal, off by one, + multiple/divisor, high ppcm, coprime, zero, one, much higher/lower, etc. *) + let fee_in_mutez_and_gas_list = + [ + (1000, 1000); + (500, 1000); + (1000, 1001); + (1000, 999); + (1000, 500); + (1000, 4000); + (1000, 1200); + (333, 777); + (11, 7); + (1000, 31); + (1000, 1); + (1, 100_000); + (1_000_000, 100_001); + (0, 10); + ] + in + let ops = + List.map + (fun (fee_in_mutez, gas) -> + let op = + Helpers.generate_manager_op_with_fee_and_gas ~fee_in_mutez ~gas + in + (op, fee_in_mutez, gas)) + fee_in_mutez_and_gas_list + in + List.iter (fun op -> List.iter (test_manager_ops op) ops) ops ; + unit + +(** Test manager operations with random fee and gas. *) +let () = + register_test + ~title:"random fee and gas" + ~additional_tags:["manager"; "random"] + @@ fun () -> + let gen = + let open QCheck2.Gen in + let* fee_in_mutez = int_range 0 100_000_000 in + let* gas = int_range 1 50_000_000 in + let* op = Helpers.manager_op_with_fee_and_gas_gen ~fee_in_mutez ~gas in + return (op, fee_in_mutez, gas) + in + iter_neighbors test_manager_ops (QCheck2.Gen.generate ~n:100 gen) ; + unit diff --git a/src/proto_alpha/lib_protocol/test/helpers/operation_generator.ml b/src/proto_alpha/lib_protocol/test/helpers/operation_generator.ml index 2c1dad6350f517b97d3c5f9a7ab9d03dd22ac092..7d72b97e370b7e14a17aaa33ceeff158a05eb265 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/operation_generator.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/operation_generator.ml @@ -47,6 +47,8 @@ let manager_pass = `PManager let all_passes = [`PConsensus; `PAnonymous; `PVote; `PManager] +let all_non_manager_passes = [`PConsensus; `PAnonymous; `PVote] + let consensus_kinds = [`KPreendorsement; `KEndorsement; `KDal_attestation] let anonymous_kinds = @@ -634,6 +636,24 @@ let generator_of ?source = function | `KSc_rollup_recover_bond -> generate_manager_operation ?source generate_sc_rollup_recover_bond +let generate_non_manager_operation = + let open QCheck2.Gen in + let* pass = oneofl all_non_manager_passes in + let* kind = oneofl (pass_to_operation_kinds pass) in + match kind with + | `KPreendorsement -> generate_operation generate_preendorsement + | `KEndorsement -> generate_operation generate_endorsement + | `KDal_attestation -> generate_operation generate_dal_attestation + | `KSeed_nonce_revelation -> generate_operation generate_seed_nonce_revelation + | `KVdf_revelation -> generate_operation generate_vdf_revelation + | `KDouble_endorsement -> generate_operation generate_double_endorsement + | `KDouble_preendorsement -> generate_operation generate_double_preendorsement + | `KDouble_baking -> generate_operation generate_double_baking + | `KActivate_account -> generate_operation generate_activate_account + | `KProposals -> generate_operation generate_proposals + | `KBallot -> generate_operation generate_ballot + | `KManager -> assert false + let generate_manager_operation batch_size = let open QCheck2.Gen in let* source = random_pkh in diff --git a/tezt/tests/dune b/tezt/tests/dune index 81eaee148e9b189f0fe7c2811f1c97611c865139..737b0f242c4f206359ed74d87b105c835d2881e6 100644 --- a/tezt/tests/dune +++ b/tezt/tests/dune @@ -27,6 +27,7 @@ src_proto_alpha_lib_protocol_test_integration_gas_tezt_lib src_proto_alpha_lib_protocol_test_integration_consensus_tezt_lib src_proto_alpha_lib_protocol_test_integration_tezt_lib + src_proto_alpha_lib_plugin_test_tezt_lib src_proto_alpha_lib_delegate_test_tezt_lib src_proto_alpha_lib_dal_test_tezt_lib src_proto_alpha_lib_dac_plugin_test_tezt_lib @@ -41,6 +42,7 @@ src_proto_017_PtNairob_lib_protocol_test_integration_gas_tezt_lib src_proto_017_PtNairob_lib_protocol_test_integration_consensus_tezt_lib src_proto_017_PtNairob_lib_protocol_test_integration_tezt_lib + src_proto_017_PtNairob_lib_plugin_test_tezt_lib src_proto_017_PtNairob_lib_delegate_test_tezt_lib src_proto_017_PtNairob_lib_dal_test_tezt_lib src_proto_017_PtNairob_lib_dac_plugin_test_tezt_lib @@ -55,6 +57,7 @@ src_proto_016_PtMumbai_lib_protocol_test_integration_gas_tezt_lib src_proto_016_PtMumbai_lib_protocol_test_integration_consensus_tezt_lib src_proto_016_PtMumbai_lib_protocol_test_integration_tezt_lib + src_proto_016_PtMumbai_lib_plugin_test_tezt_lib src_proto_016_PtMumbai_lib_delegate_test_tezt_lib src_proto_016_PtMumbai_lib_dal_test_tezt_lib src_proto_016_PtMumbai_lib_client_test_tezt_lib