diff --git a/tezt/lib_tezos/RPC.ml b/tezt/lib_tezos/RPC.ml index d6e0d42b047d053713ca4dd0dca81c142992ae98..6f791891e5a52156e6274f340a722a0d9e6c4b94 100644 --- a/tezt/lib_tezos/RPC.ml +++ b/tezt/lib_tezos/RPC.ml @@ -191,8 +191,8 @@ let post_private_injection_operation ?(async = false) data = ~data Fun.id -let post_run_operation ?(chain = "main") ?(block = "head") ?(async = false) data - = +let post_chain_block_helpers_scripts_run_operation ?(chain = "main") + ?(block = "head") ?(async = false) data = make POST ["chains"; chain; "blocks"; block; "helpers"; "scripts"; "run_operation"] diff --git a/tezt/lib_tezos/RPC.mli b/tezt/lib_tezos/RPC.mli index add8900a5eb1def3368cfd0e3295f8b58e44abd9..403b24a357ef39258572b90ab6305b4b598a4dbb 100644 --- a/tezt/lib_tezos/RPC.mli +++ b/tezt/lib_tezos/RPC.mli @@ -222,11 +222,16 @@ val post_injection_operation : ?async:bool -> JSON.u -> JSON.t t (** RPC: [POST /private/injection/operation] *) val post_private_injection_operation : ?async:bool -> JSON.u -> JSON.t t -(** RPC: [POST /chains/[chain]/blocks/[block]/helpers/scripts/run_operation] +(** RPC: [POST /chains//blocks//helpers/scripts/run_operation] + + Tries to validate and apply the operation represented by the given + json, directly on top of the [block]. Only skips signature + checks. If successful, returns the operation together with the + metadata produced by its application. [chain] defaults to ["main"]. [block] defaults to ["head"]. *) -val post_run_operation : +val post_chain_block_helpers_scripts_run_operation : ?chain:string -> ?block:string -> ?async:bool -> JSON.u -> JSON.t t (** RPC: [GET /chains/[chain]/chain_id] diff --git a/tezt/lib_tezos/RPC_legacy.ml b/tezt/lib_tezos/RPC_legacy.ml index 1adb473033d63cfdf7e5c0e9b45a9badc4683665..d5d95001a4a5b63cb04190c398bf76aed13568de 100644 --- a/tezt/lib_tezos/RPC_legacy.ml +++ b/tezt/lib_tezos/RPC_legacy.ml @@ -195,13 +195,6 @@ let post_forge_operations ?endpoint ?hooks ?(chain = "main") ?(block = "head") in Client.rpc ?endpoint ?hooks ~data POST path client -let post_run_operation ?endpoint ?hooks ?(chain = "main") ?(block = "head") - ~data client = - let path = - ["chains"; chain; "blocks"; block; "helpers"; "scripts"; "run_operation"] - in - Client.rpc ?endpoint ?hooks ~data POST path client - let post_simulate_operation ?endpoint ?hooks ?(chain = "main") ?(block = "head") ~data client = let path = diff --git a/tezt/lib_tezos/RPC_legacy.mli b/tezt/lib_tezos/RPC_legacy.mli index b67149b3894bc6336182d602a896070fdadb472b..5222235804901f947488cc0bbd82b395e13fd493 100644 --- a/tezt/lib_tezos/RPC_legacy.mli +++ b/tezt/lib_tezos/RPC_legacy.mli @@ -27,7 +27,7 @@ (** Legacy node RPCs. *) (** THIS MODULE IS DEPRECATED: ITS FUNCTIONS SHOULD BE PORTED TO THE NEW RPC - ENGINE (IN [rpc.ml], USING MODULE [RPC_core]). *) + ENGINE (IN [RPC.ml], USING MODULE [RPC_core]). *) (** In all RPCs, default [chain] is "main" and default [block] is "head~2" to pick the finalized branch for Tenderbake. *) @@ -175,16 +175,6 @@ val post_forge_operations : Client.t -> JSON.t Lwt.t -(** Call RPC /chain/[chain]/blocks/[block]/helpers/scripts/run_operation *) -val post_run_operation : - ?endpoint:Client.endpoint -> - ?hooks:Process.hooks -> - ?chain:string -> - ?block:string -> - data:JSON.u -> - Client.t -> - JSON.t Lwt.t - (** Call RPC /chain/[chain]/blocks/[block]/helpers/scripts/simulate_operation *) val post_simulate_operation : ?endpoint:Client.endpoint -> diff --git a/tezt/lib_tezos/mempool.ml b/tezt/lib_tezos/mempool.ml index 6d31d0cfbfe99dac7c00bb54b477e47f694696fc..6aac99965ce620b95980f2954029b55a66a8370b 100644 --- a/tezt/lib_tezos/mempool.ml +++ b/tezt/lib_tezos/mempool.ml @@ -88,10 +88,23 @@ let symmetric_diff left right = unprocessed = diff left.unprocessed right.unprocessed; } +let of_json mempool_json = + let get_hash op = JSON.(op |-> "hash" |> as_string) in + let get_hashes classification = + List.map get_hash JSON.(mempool_json |-> classification |> as_list) + in + let applied = get_hashes "applied" in + let branch_delayed = get_hashes "branch_delayed" in + let branch_refused = get_hashes "branch_refused" in + let refused = get_hashes "refused" in + let outdated = get_hashes "outdated" in + let unprocessed = get_hashes "unprocessed" in + {applied; branch_delayed; branch_refused; refused; outdated; unprocessed} + let get_mempool ?endpoint ?hooks ?chain ?(applied = true) ?(branch_delayed = true) ?(branch_refused = true) ?(refused = true) ?(outdated = true) client = - let* pending_ops = + let* mempool_json = RPC.get_mempool_pending_operations ?endpoint ?hooks @@ -104,15 +117,14 @@ let get_mempool ?endpoint ?hooks ?chain ?(applied = true) ~outdated client in - let get_hash op = JSON.(op |-> "hash" |> as_string) in - let get_hashes classification = - List.map get_hash JSON.(pending_ops |-> classification |> as_list) - in - let applied = get_hashes "applied" in - let branch_delayed = get_hashes "branch_delayed" in - let branch_refused = get_hashes "branch_refused" in - let refused = get_hashes "refused" in - let outdated = get_hashes "outdated" in - let unprocessed = get_hashes "unprocessed" in - return + return (of_json mempool_json) + +let check_mempool ?(applied = []) ?(branch_delayed = []) ?(branch_refused = []) + ?(refused = []) ?(outdated = []) ?(unprocessed = []) mempool = + let expected_mempool = {applied; branch_delayed; branch_refused; refused; outdated; unprocessed} + in + Check.( + (expected_mempool = mempool) + classified_typ + ~error_msg:"Expected mempool %L, got %R") diff --git a/tezt/lib_tezos/mempool.mli b/tezt/lib_tezos/mempool.mli index d0e8cdc139e88c4e452409de9e6c02e72e1d97a2..0c8c5d2e8d828859f8bb8e55de431c511849322a 100644 --- a/tezt/lib_tezos/mempool.mli +++ b/tezt/lib_tezos/mempool.mli @@ -45,6 +45,10 @@ val empty : t (** Symetric difference (union(a, b) - intersection(a, b)) *) val symmetric_diff : t -> t -> t +(** Build a value of type {!t} from a json returned by + {!RPC.get_mempool_pending_operations}. *) +val of_json : JSON.t -> t + (** Call [RPC.get_mempool_pending_operations] and wrap the result in a value of type [Mempool.t] *) val get_mempool : @@ -58,3 +62,17 @@ val get_mempool : ?outdated:bool -> Client.t -> t Lwt.t + +(** Check that each field of [t] contains the same elements as the + argument of the same name. Ordening does not matter. Omitted + arguments default to the empty list. This is useful when we expect a + sparse mempool. *) +val check_mempool : + ?applied:string list -> + ?branch_delayed:string list -> + ?branch_refused:string list -> + ?refused:string list -> + ?outdated:string list -> + ?unprocessed:string list -> + t -> + unit diff --git a/tezt/lib_tezos/operation_core.ml b/tezt/lib_tezos/operation_core.ml index 7cbabe6d679904d363c0cc51efe76c71e19dcce9..00b3695ce35f9fb7fa8ee000ca08083e1fcf44e7 100644 --- a/tezt/lib_tezos/operation_core.ml +++ b/tezt/lib_tezos/operation_core.ml @@ -149,6 +149,28 @@ let inject_operations ?(request = `Inject) ?(force = false) ?error t client : let* () = Process.check_error ~msg process in Lwt_list.map_s (fun op -> hash op client) t +let make_run_operation_input ?chain_id t client = + let* chain_id = + match chain_id with + | Some chain_id -> return chain_id + | None -> RPC.(Client.call client (get_chain_chain_id ())) + in + (* The [run_operation] RPC does not check the signature. *) + let signature = Tezos_crypto.Signature.zero in + return + (`O + [ + ( "operation", + `O + [ + ("branch", `String t.branch); + ("contents", t.contents); + ( "signature", + `String (Tezos_crypto.Signature.to_b58check signature) ); + ] ); + ("chain_id", `String chain_id); + ]) + module Consensus = struct type t = Slot_availability of {endorsement : bool array} @@ -191,11 +213,14 @@ end module Manager = struct type payload = + | Reveal of Account.key | Transfer of {amount : int; dest : Account.key} | Dal_publish_slot_header of {level : int; index : int; header : int} | Sc_rollup_dal_slot_subscribe of {rollup : string; slot_index : int} | Delegation of {delegate : Account.key} + let reveal account = Reveal account + let transfer ?(dest = Constant.bootstrap2) ?(amount = 1_000_000) () = Transfer {amount; dest} @@ -233,6 +258,8 @@ module Manager = struct return (1 + JSON.as_int json) let json_payload_binding = function + | Reveal account -> + [("kind", `String "reveal"); ("public_key", `String account.public_key)] | Transfer {amount; dest} -> [ ("kind", `String "transaction"); @@ -308,8 +335,8 @@ module Manager = struct let gas_limit = Option.value gas_limit ~default:1_040 in let storage_limit = Option.value storage_limit ~default:257 in {source; counter; fee; gas_limit; storage_limit; payload} - | Dal_publish_slot_header _ | Delegation _ | Sc_rollup_dal_slot_subscribe _ - -> + | Reveal _ | Dal_publish_slot_header _ | Delegation _ + | Sc_rollup_dal_slot_subscribe _ -> let fee = Option.value fee ~default:1_000 in let gas_limit = Option.value gas_limit ~default:1_040 in let storage_limit = Option.value storage_limit ~default:0 in diff --git a/tezt/lib_tezos/operation_core.mli b/tezt/lib_tezos/operation_core.mli index d24c82b5b25448e899f7c3f24b84fba7c2be78c3..fff4c7fafb4f298d1b2f308c6136de063e44aa27 100644 --- a/tezt/lib_tezos/operation_core.mli +++ b/tezt/lib_tezos/operation_core.mli @@ -134,6 +134,24 @@ val inject_operations : Client.t -> [`OpHash of string] list Lwt.t +(** Craft a json representing the full operation, in a format that is + compatible with the [run_operation] RPC + ({!RPC.post_chain_block_helpers_scripts_run_operation}). + + This json contains many more fields than the one produced by the + {!json} function above. + + The operation is signed with {!Tezos_crypto.Signature.zero}, + because the [run_operation] RPC skips signature checks anyway. + + @param chain_id Allows to manually provide the [chain_id]. If + omitted, the [chain_id] is retrieved via RPC using the provided + [client]. + + @param client The {!Client.t} argument is used to retrieve the + [chain_id] when it is not provided. *) +val make_run_operation_input : ?chain_id:string -> t -> Client.t -> JSON.u Lwt.t + module Consensus : sig (** A representation of a consensus operation. *) type t @@ -175,6 +193,12 @@ module Manager : sig common to all manager operations. See {!type:t}. *) type payload + (** Build a public key revelation. + + The [Account.key] argument has no default value because it will + typically be a fresh account. *) + val reveal : Account.key -> payload + (** [transfer ?(dest=Constant.bootstrap2) ~amount:1_000_000 ()] builds a transfer operation. Note that the amount is expressed in mutez. *) diff --git a/tezt/tests/main.ml b/tezt/tests/main.ml index 6e39f32a0babe8fb9f28d2eaa6d61b4f03439f8e..6830576dd540a4fd6407e6ac59a55f5028f41696 100644 --- a/tezt/tests/main.ml +++ b/tezt/tests/main.ml @@ -104,7 +104,6 @@ let register_protocol_agnostic_tests () = Monitor_operations.register ~protocols:[Alpha] ; Node_event_level.register ~protocols:[Alpha] ; Normalize.register ~protocols:[Alpha] ; - Op_validation.register ~protocols ; Precheck.register ~protocols ; Prevalidator.register ~protocols ; Protocol_limits.register ~protocols:[Alpha] ; @@ -115,6 +114,7 @@ let register_protocol_agnostic_tests () = Replace_by_fees.register ~protocols ; Rpc_config_logging.register ~protocols:[Alpha] ; RPC_test.register protocols ; + Run_operation_RPC.register ~protocols ; Runtime_script_failure.register ~protocols ; Signer_test.register ~protocols:[Alpha] ; Stresstest_command.register ~protocols:[Alpha] ; diff --git a/tezt/tests/op_validation.ml b/tezt/tests/op_validation.ml deleted file mode 100644 index e53125b6246a7ceb068855f887189f5c4ce357f4..0000000000000000000000000000000000000000 --- a/tezt/tests/op_validation.ml +++ /dev/null @@ -1,116 +0,0 @@ -(*****************************************************************************) -(* *) -(* Open Source License *) -(* Copyright (c) 2022 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: Validation components - Invocation: dune exec tezt/tests/main.exe -- --file "op_validation.ml" - Subject: Checks the validation of operations -*) - -let check_run_operation_illformed_batch ~supports check_answer = - Protocol.register_test - ~__FILE__ - ~supports - ~title:"Run_operation ill-formed batch" - ~tags:["rpc"; "run_operation"; "batch"] - @@ fun protocol -> - Log.info "Initialize a node and a client." ; - let* node, client = - Client.init_with_protocol - ~nodes_args:[Synchronisation_threshold 0] - ~protocol - `Client - () - in - - Log.info - "Do a transfer from %s and bake to increment its counter." - Constant.bootstrap2.alias ; - let* _ = - Client.transfer - ~amount:Tez.one - ~giver:Constant.bootstrap2.alias - ~receiver:Constant.bootstrap3.alias - client - in - let* _ = Client.bake_for_and_wait ~protocol ~node client in - - Log.info "Create a first operation." ; - let source1 = Constant.bootstrap1 in - let dest = Constant.bootstrap3 in - let op1 = Operation.Manager.(make ~source:source1 @@ transfer ~dest ()) in - let* op1_json = Operation.Manager.json client op1 in - - Log.info - "Create a second operation with a different source and an incremented \ - counter." ; - let source2 = Constant.bootstrap2 in - let* counter = Operation.get_next_counter ~source:source2 client in - let op2 = - Operation.Manager.(make ~source:source2 ~counter @@ transfer ~dest ()) - in - let* op2_json = Operation.Manager.json client op2 in - - Log.info "Craft a batch in JSON that contains both operations." ; - let* branch = Operation.get_injection_branch client in - let signature = Tezos_crypto.Signature.zero in - let* chain_id = RPC.Client.call client @@ RPC.get_chain_chain_id () in - let batch = - Format.asprintf - {|{ "operation": - {"branch": "%s", - "contents": [%s,%s], - "signature": "%a" }, - "chain_id": %s }|} - branch - (Ezjsonm.value_to_string op1_json) - (Ezjsonm.value_to_string op2_json) - Tezos_crypto.Signature.pp - signature - (JSON.encode_u (`String chain_id)) - in - - Log.info "Call the [run_operation] RPC with this JSON batch." ; - let*? p = - RPC.Client.spawn client - @@ RPC.post_run_operation (Ezjsonm.from_string batch) - in - check_answer p - -(** This test checks that the [run_operation] RPC used to allow - batches of manager operations containing different sources in - protocol versions before 14, but rejects them from 14 on. *) -let check_run_operation_illformed_batch ~protocols = - check_run_operation_illformed_batch - ~supports:(Protocol.Until_protocol 13) - (Process.check ~expect_failure:false) - protocols ; - check_run_operation_illformed_batch - ~supports:(Protocol.From_protocol 14) - (Process.check ~expect_failure:true) - protocols - -let register ~protocols = check_run_operation_illformed_batch ~protocols diff --git a/tezt/tests/prevalidator.ml b/tezt/tests/prevalidator.ml index 3f9776cb0d47ae0027cb2db66536a1ee5b610d5e..dac727b62a45da72bc03e4346867ae34756d36cd 100644 --- a/tezt/tests/prevalidator.ml +++ b/tezt/tests/prevalidator.ml @@ -35,8 +35,6 @@ Some refactorisation is needed. All new tests should be in the Revamped module (which will be erased once we have rewrote all the Legacy tests. *) -module Mempool = Tezt_tezos.Mempool - module Revamped = struct let log_step counter msg = let color = Log.Color.(bold ++ FG.blue) in @@ -112,26 +110,23 @@ module Revamped = struct let* _ = RPC.mempool_request_operations client in mempool_notify_waiter - let check_mempool ?(applied = []) ?(branch_delayed = []) - ?(branch_refused = []) ?(refused = []) ?(outdated = []) - ?(unprocessed = []) client = + (* Call the [/chains/[chain]/mempool/pending_operations] RPC and + check that in the returned mempool, each field [applied], + [branch_delayed], etc. contains exactly the operation hashes + listed in the argument of the same name. Omitted arguments + default to the empty list. *) + let check_mempool ?applied ?branch_delayed ?branch_refused ?refused ?outdated + ?unprocessed client = let* mempool = Mempool.get_mempool client in - let expected_mempool = - Mempool. - { - applied; - branch_delayed; - branch_refused; - refused; - outdated; - unprocessed; - } - in - Check.( - (expected_mempool = mempool) - Mempool.classified_typ - ~error_msg:"Expected mempool %L, got %R") ; - unit + return + (Mempool.check_mempool + ?applied + ?branch_delayed + ?branch_refused + ?refused + ?outdated + ?unprocessed + mempool) (** {2 Tests } *) @@ -1984,13 +1979,58 @@ module Revamped = struct inject_operations ~force:true [List.nth ops 0; List.nth ops 4] client) in let injected_ops2 = List.map (fun (`OpHash op) -> op) injected_ops2 in - let* () = - check_mempool - ~applied:((List.nth injected_ops2 1 :: injected_ops) @ mempool.applied) - ~branch_refused:[List.nth injected_ops2 0] - client + check_mempool + ~applied:((List.nth injected_ops2 1 :: injected_ops) @ mempool.applied) + ~branch_refused:[List.nth injected_ops2 0] + client + + (** This test injects a well-formed batch of manager operations and + checks that it is [applied] in the mempool. *) + let test_inject_manager_batch = + Protocol.register_test + ~__FILE__ + ~title:"Inject manager batch" + ~tags:["mempool"; "manager"; "batch"; "injection"; "applied"] + @@ fun protocol -> + log_step 1 "Initialize a node and a client." ; + let* _node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + + let n_transactions = 3 in + log_step 2 "Inject a well-formed batch of %d transactions." n_transactions ; + let* (`OpHash oph) = + let payload = Operation.Manager.transfer ~dest:Constant.bootstrap2 () in + let source = Constant.bootstrap1 in + let* counter = Operation.Manager.get_next_counter ~source client in + let batch = + Operation.Manager.make_batch + ~source + ~counter + (List.init n_transactions (fun _ -> payload)) + in + Operation.Manager.inject batch client in + log_step 3 "Check that the batch is correctly [applied] in the mempool." ; + let* mempool_json = RPC.get_mempool_pending_operations client in + let mempool = Mempool.of_json mempool_json in + Mempool.check_mempool ~applied:[oph] mempool ; + Log.info + "The mempool contains exactly one [applied] operation with the correct \ + hash." ; + let batch_payloads = + JSON.(mempool_json |-> "applied" |=> 0 |-> "contents" |> as_list) + in + Check.( + (List.compare_length_with batch_payloads n_transactions = 0) + int + ~error_msg:"The [applied] batch has a wrong number of manager payloads.") ; + Log.info "The [applied] batch as the correct number of manager payloads." ; unit end @@ -2167,170 +2207,9 @@ let forge_and_inject_operation ~branch ~fee ~gas_limit ~source ~destination let signature = Operation.sign_manager_op_hex ~signer op_str_hex in inject_operation ~client op_str_hex signature -let forge_and_inject_n_operations ~branch ~fee ~gas_limit ~source ~destination - ~counter ~signer ~client ~node n = - let rec loop ((oph_list, counter) as acc) = function - | 0 -> return acc - | n -> - let transfer_1 = wait_for_injection node in - let* oph = - forge_and_inject_operation - ~branch - ~fee - ~gas_limit - ~source - ~destination - ~counter - ~signer - ~client - in - let* () = transfer_1 in - let oph_list = oph :: oph_list in - loop (oph_list, counter + 1) (pred n) - in - loop ([], counter + 1) n - -(** Bakes with an empty mempool to force synchronisation between nodes. *) -let bake_empty_block ?endpoint ~protocol client = - let mempool = Client.empty_mempool_file () in - Client.bake_for_and_wait ~protocol ?endpoint ~mempool client - -(** [bake_empty_mempool_and_wait_for_flush client node] bakes for [client] - with an empty mempool, then waits for a [flush] event on [node] (which - will usually be the node corresponding to [client], but could be any - node with a connection path to it). *) -let _bake_empty_block_and_wait_for_flush ?(log = false) ~protocol client node = - let waiter = wait_for_flush node in - let* () = bake_empty_block ~protocol client in - if log then - Log.info "Baked for %s with an empty mempool." (Client.name client) ; - waiter - (* TODO: add a test than ensure that we cannot have more than 1000 branch delayed/branch refused/refused *) -let forge_run_and_inject_n_batched_operation n ~branch ~fee ~gas_limit ~source - ~destination ~counter ~signer ~client = - let ops_json = - String.concat ", " - @@ List.map - (fun counter -> - operation_json ~fee ~gas_limit ~source ~destination ~counter) - (range (counter + 1) (counter + n)) - in - let op_json_branch = operation_json_branch ~branch ops_json in - let* op_hex = - RPC.post_forge_operations ~data:(Ezjsonm.from_string op_json_branch) client - in - let op_str_hex = JSON.as_string op_hex in - let signature = - Operation.sign_manager_op_bytes ~signer (Hex.to_bytes (`Hex op_str_hex)) - in - let* _run = - let* chain_id = RPC.Client.call client @@ RPC.get_chain_chain_id () in - let op_runnable = - (* Please don't do that. Build [JSON.u] values and use [JSON.encode_u]. *) - Format.asprintf - {|{ "operation": - {"branch": "%s", - "contents": [ %s ], - "signature": "%a" }, - "chain_id": %s }|} - branch - ops_json - Tezos_crypto.Signature.pp - signature - (JSON.encode_u (`String chain_id)) - in - RPC.Client.call client - @@ RPC.post_run_operation (Ezjsonm.from_string op_runnable) - in - let (`Hex signature) = Tezos_crypto.Signature.to_hex signature in - let signed_op = op_str_hex ^ signature in - RPC.Client.call client @@ RPC.post_injection_operation (`String signed_op) - -let check_batch_operations_are_in_applied_mempool ops oph n = - let open JSON in - let ops_list = as_list (ops |-> "applied") in - let res = - List.exists - (fun e -> - let contents = as_list (e |-> "contents") in - let h = as_string (e |-> "hash") in - List.compare_length_with contents n = 0 && h = as_string oph) - ops_list - in - if not res then - Test.fail - "Batch Operation %s was not found in the mempool or it does not contain \ - %d operations" - (JSON.encode oph) - n - -(** This test tries to run manually forged operations before injecting them - - Scenario: - - + Node 1 activates a protocol - - + Retrieve the counter and the branch for bootstrap1 - - + Forge, run and inject operations in the node - - + Check that the batch is correctly injected - *) -let run_batched_operation = - Protocol.register_test - ~__FILE__ - ~title:"Run batched operations before injecting them" - ~tags:["forge"; "mempool"; "batch"; "run_operation"] - @@ fun protocol -> - (* Step 1 *) - (* A Node is started and we activate the protocol and wait for the node to be synced *) - let* node_1 = Node.init [Synchronisation_threshold 0] in - let* client_1 = Client.init ~endpoint:(Node node_1) () in - let* () = Client.activate_protocol ~protocol client_1 in - Log.info "Activated protocol." ; - let* _ = Node.wait_for_level node_1 1 in - Log.info "Node is at level %d." 1 ; - (* Step 2 *) - (* Get the counter and the current branch *) - let*! counter = - RPC.Contracts.get_counter - ~contract_id:Constant.bootstrap1.public_key_hash - client_1 - in - let counter = JSON.as_int counter in - let* branch = RPC.get_branch client_1 in - let branch = JSON.as_string branch in - (* Step 3 *) - (* Forge operations, run and inject them *) - let number_of_transactions = 3 in - let* oph = - forge_run_and_inject_n_batched_operation - number_of_transactions - ~branch - ~fee:1000 (* Minimal fees to successfully apply the transfer *) - ~gas_limit:1040 (* Minimal gas to successfully apply the transfer *) - ~source:Constant.bootstrap2.public_key_hash - ~destination:Constant.bootstrap1.public_key_hash - ~counter - ~signer:Constant.bootstrap2 - ~client:client_1 - in - Log.info "Operations forged, signed, run and injected" ; - (* Step 4 *) - (* Check that the batch is correctly injected *) - let* mempool_after_batch = RPC.get_mempool_pending_operations client_1 in - check_batch_operations_are_in_applied_mempool - mempool_after_batch - oph - number_of_transactions ; - Log.info - "%d operations are applied as a batch in the mempool" - number_of_transactions ; - unit - let check_if_op_is_in_mempool client ~classification oph = let* ops = RPC.get_mempool_pending_operations ~version:"1" client in let open JSON in @@ -4156,7 +4035,7 @@ let register ~protocols = Revamped.precheck_with_empty_balance [Protocol.Ithaca] (* FIXME: handle the case for Alpha. *) ; Revamped.inject_operations protocols ; - run_batched_operation protocols ; + Revamped.test_inject_manager_batch protocols ; propagation_future_endorsement protocols ; forge_pre_filtered_operation protocols ; refetch_failed_operation protocols ; diff --git a/tezt/tests/reject_malformed_micheline.ml b/tezt/tests/reject_malformed_micheline.ml index 0a4c7596deb6fcaf7f0f4701408a59adb62615af..b1e10669df5481798c9319127ee966ce1974f29b 100644 --- a/tezt/tests/reject_malformed_micheline.ml +++ b/tezt/tests/reject_malformed_micheline.ml @@ -64,26 +64,26 @@ let make_data s = to an RPC endpoint. *) let reject_malformed_micheline = - Protocol.register_test ~__FILE__ ~title:"Reject malformed micheline" ~tags:[] + Protocol.register_test + ~__FILE__ + ~title:"Reject malformed micheline" + ~tags: + [ + "micheline"; + "empty_implicit_contract"; + "malformed_annotation"; + "run_operation"; + ] @@ fun protocol -> let* node, _client = Client.init_with_protocol `Client ~protocol () in let send_operation data = - (* This RPC path is used because it doesn't require valid signatures. *) - let rpc_path = - sf - "http://localhost:%d/chains/main/blocks/head/helpers/scripts/run_operation" - @@ Node.rpc_port node + (* The [run_operation] RPC is used because it doesn't require + valid signatures. *) + let json = Ezjsonm.from_string data in + let* response = + RPC.(call_raw node (post_chain_block_helpers_scripts_run_operation json)) in - let proc_malformed_annots = - (* We cannot use the client to test the injection of malformed - annotations. This is because the client will reject the invalid - annotations and will not propagate the malformed data to the server. - Instead we have to use RPCs directly. Hence [curl]. *) - Process.spawn - "curl" - ["-H"; "Content-type: application/json"; "-d"; data; rpc_path] - in - Process.check_and_read_stdout proc_malformed_annots + return response.body in (* We send a valid annotation. *) let* output = send_operation @@ make_data "\"%test\"" in diff --git a/tezt/tests/run_operation_RPC.ml b/tezt/tests/run_operation_RPC.ml new file mode 100644 index 0000000000000000000000000000000000000000..7da94c8f5cfcf811a091823f415e18cbb23cc5fb --- /dev/null +++ b/tezt/tests/run_operation_RPC.ml @@ -0,0 +1,546 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 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: Protocol's plugin + Invocation: dune exec tezt/tests/main.exe -- --file "run_operation_RPC.ml" + Subject: Test the [run_operation] RPC: + [POST /chains//blocks//helpers/scripts/run_operation]. + These tests focus on the semantics of the RPC, ie. whether + the operation is successfully run, rather than on the exact + form of the output, which is why they are in their own file + instead of [RPC_test.ml]. +*) + +(** Tags shared by all tests in this file. *) +let run_operation_tags = ["rpc"; "run_operation"] + +(** Check that the RPC [response]'s code is [500] (Internal Server + Error), and that its body has an "id" field that ends in + [expected_proto_error]. *) +let check_response_contains_proto_error ~expected_proto_error + (response : JSON.t RPC.response) = + Log.info + "Checking RPC response:\n code: %s\n body: %s" + Cohttp.Code.(string_of_status (status_of_code response.code)) + (JSON.encode response.body) ; + Check.( + (response.code = 500) + int + ~error_msg:"Expected response code %R, but got %L.") ; + let response_proto_error = + try + let id = JSON.(response.body |=> 0 |-> "id" |> as_string) in + List.(hd (rev (String.split_on_char '.' id))) + with exn -> + Test.fail + "Failed to parse the following RPC response body:\n\ + %s.\n\ + The following exception was raised:\n\ + %s" + (JSON.encode response.body) + (Printexc.to_string exn) + in + Check.( + (response_proto_error = expected_proto_error) + string + ~error_msg:"Expected the %R protocol error, but got %L.") + +(** Craft a batch that contains the given individual manager + operation(s), call the [run_operation] RPC on it, and call + {!check_response_contains_proto_error} and the RPC response. *) +let run_manager_operations_and_check_proto_error ~expected_proto_error + (manager_operations : Operation_core.Manager.t list) node client = + let* op = Operation.Manager.operation manager_operations client in + let* op_json = Operation.make_run_operation_input op client in + Log.debug + "Crafted operation: %s" + (Ezjsonm.value_to_string ~minify:false op_json) ; + let* response = + RPC.( + call_json node (post_chain_block_helpers_scripts_run_operation op_json)) + in + check_response_contains_proto_error ~expected_proto_error response ; + unit + +(** This test checks that the [run_operation] RPC used to allow + batches of manager operations containing different sources in + protocol versions before Kathmandu (014), but rejects them from + Kathmandu on. *) +let test_batch_inconsistent_sources protocols = + let register_inconsistent_sources ~supports ~title + call_run_operation_and_check_response = + Protocol.register_test + ~__FILE__ + ~supports + ~title + ~tags:(run_operation_tags @ ["manager"; "batch"; "inconsistent_sources"]) + (fun protocol -> + Log.info "Initialize a node and a client." ; + let* node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + let source1 = Constant.bootstrap1 + and source2 = Constant.bootstrap2 + and dest = Constant.bootstrap3 in + Log.info + "Increment [%s]'s counter so that the batch we craft below has \ + consistent counters. To do this, we inject a transaction from this \ + account and bake a block." + source2.alias ; + let* () = + Client.transfer + ~amount:Tez.one + ~giver:source2.alias + ~receiver:dest.alias + client + in + let* () = Client.bake_for_and_wait ~protocol ~node client in + Log.info + "Craft a batch containing an operation from [%s] and an operation \ + from [%s]." + source1.alias + source2.alias ; + let manager_op1 = + Operation.Manager.(make ~source:source1 (transfer ~dest ())) + in + let manager_op2 = + Operation.Manager.(make ~source:source2 (transfer ~dest ())) + in + let* batch = + Operation.Manager.operation [manager_op1; manager_op2] client + in + let* batch_json = Operation.make_run_operation_input batch client in + Log.info + "Crafted batch: %s" + (Ezjsonm.value_to_string ~minify:false batch_json) ; + call_run_operation_and_check_response node batch_json) + in + register_inconsistent_sources + ~supports:Protocol.(Until_protocol (number Jakarta)) + ~title:"Run_operation inconsistent sources ok" + (fun node batch_json -> + Log.info + "Call the [run_operation] RPC on this batch and check that it succeeds." ; + let* _run_operation_output = + RPC.( + call node (post_chain_block_helpers_scripts_run_operation batch_json)) + in + unit) + protocols ; + register_inconsistent_sources + ~supports:(Protocol.From_protocol 014) + ~title:"Run_operation inconsistent sources ko" + (fun node batch_json -> + let expected_proto_error = "inconsistent_sources" in + Log.info + "Call the [run_operation] RPC on this batch, and check that it fails \ + with code [500] (Internal Server Error) and protocol error [%s]." + expected_proto_error ; + let* response = + RPC.call_json + node + (RPC.post_chain_block_helpers_scripts_run_operation batch_json) + in + check_response_contains_proto_error ~expected_proto_error response ; + unit) + protocols + +(** This test calls the [run_operation] RPC on various operations with + unexpected or inconsistent counters, and checks that the + appropriate protocol error is returned. *) +let test_inconsistent_counters = + Protocol.register_test + ~__FILE__ + ~supports:Protocol.(From_protocol 013) + ~title:"Run_operation inconsistent counters" + ~tags: + (run_operation_tags + @ [ + "manager"; + "batch"; + "counter"; + "counter_in_the_past"; + "counter_in_the_future"; + "inconsistent_counters"; + ]) + (fun protocol -> + Log.info "Initialize a node and a client." ; + let* node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + let run_manager_operations_and_check_proto_error ~expected_proto_error + manager_ops = + run_manager_operations_and_check_proto_error + ~expected_proto_error + manager_ops + node + client + in + let source = Constant.bootstrap1 in + let* next_counter = Operation.Manager.get_next_counter ~source client in + Log.info + "All the operations in this test will be from %s. The expected counter \ + for the next manager operation from this source is %d." + source.alias + next_counter ; + let current_counter = next_counter - 1 in + Log.info + "Call [run_operation] on a transaction with counter %d." + current_counter ; + let* () = + let transaction = + Operation.Manager.( + make ~source ~counter:current_counter (transfer ())) + in + run_manager_operations_and_check_proto_error + ~expected_proto_error:"counter_in_the_past" + [transaction] + in + let next_plus_one = next_counter + 1 in + Log.info + "Call [run_operation] on a transaction with counter %d." + next_plus_one ; + let* () = + let transaction = + Operation.Manager.(make ~source ~counter:next_plus_one (transfer ())) + in + run_manager_operations_and_check_proto_error + ~expected_proto_error:"counter_in_the_future" + [transaction] + in + Log.info + "Call [run_operation] on a batch where the first operation has the \ + expected counter %d, but the second operation also has the same \ + counter %d." + next_counter + next_counter ; + let transaction_next_counter = + Operation.Manager.( + make + ~source + ~counter:next_counter + (transfer ~dest:Constant.bootstrap2 ())) + in + let* () = + run_manager_operations_and_check_proto_error + ~expected_proto_error:"inconsistent_counters" + [transaction_next_counter; transaction_next_counter] + in + let next_plus_two = next_counter + 2 in + Log.info + "Call [run_operation] on a batch where the first operation has the \ + expected counter %d, but the second operation has the counter %d." + next_counter + next_plus_two ; + let transaction2 = + Operation.Manager.( + make + ~source + ~counter:next_plus_two + (transfer ~dest:Constant.bootstrap2 ())) + in + run_manager_operations_and_check_proto_error + ~expected_proto_error:"inconsistent_counters" + [transaction_next_counter; transaction2]) + +(** This test calls the [run_operation] RPC on various faulty + revelations. + + This test only supports protocol versions from Kathmandu (014) on, + because of changes to the revelation semantic introduced in this + protocol. *) +let test_bad_revelations = + Protocol.register_test + ~__FILE__ + ~supports:(Protocol.From_protocol 014) + ~title:"Run_operation bad revelations" + ~tags:(run_operation_tags @ ["manager"; "reveal"; "bad_revelations"]) + @@ fun protocol -> + Log.info "Initialize a node and a client." ; + let* node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + Log.info + "Create a fresh account: generate a key, inject a transaction that funds \ + it, and bake a block to apply the transaction." ; + let* fresh_account = Client.gen_and_show_keys client in + let* _oph = + Operation.inject_transfer + client + ~source:Constant.bootstrap2 + ~dest:fresh_account + ~gas_limit:1500 + ~amount:10_000_000 + in + let* () = Client.bake_for_and_wait ~node client in + let* fresh_account_next_counter = + Operation.Manager.get_next_counter ~source:fresh_account client + in + let incorrect_reveal_position_error = "incorrect_reveal_position" in + Log.info + "Call [run_operation] on a batch with a reveal in 2nd position, and check \ + that it returns the [%s] protocol error." + incorrect_reveal_position_error ; + let* () = + let* op = + let transaction_payload = Operation.Manager.transfer () in + let reveal_payload = Operation.Manager.reveal fresh_account in + Operation.Manager.( + operation + (make_batch + ~source:fresh_account + ~counter:fresh_account_next_counter + [transaction_payload; reveal_payload]) + client) + in + let* op_json = Operation.make_run_operation_input op client in + let* response = + RPC.( + call_json node (post_chain_block_helpers_scripts_run_operation op_json)) + in + check_response_contains_proto_error + ~expected_proto_error:incorrect_reveal_position_error + response ; + unit + in + let inconsistent_hash_error = "inconsistent_hash" in + Log.info + "Call [run_operation] on a revelation of a public key that is not \ + consistent with the source's public key hash, and check that it returns \ + the [%s] protocol error." + inconsistent_hash_error ; + let* () = + let reveal_manager_op = + Operation.Manager.( + make ~source:fresh_account (reveal Constant.bootstrap1)) + in + let* op = Operation.Manager.operation [reveal_manager_op] client in + let* op_json = Operation.make_run_operation_input op client in + let* response = + RPC.( + call_json node (post_chain_block_helpers_scripts_run_operation op_json)) + in + check_response_contains_proto_error + ~expected_proto_error:inconsistent_hash_error + response ; + unit + in + let previously_revealed_error = "previously_revealed_key" in + Log.info + "Call [run_operation] on a revelation of an already revealed key. Check \ + that the call succeeds, but the returned metadata indicate that the \ + operation's application has failed with the [%s] protocol error." + previously_revealed_error ; + let* () = + let source = Constant.bootstrap1 (* this source is already revealed *) in + let manager_op = Operation.Manager.(make ~source (reveal source)) in + let* op = Operation.Manager.operation [manager_op] client in + let* op_json = Operation.make_run_operation_input op client in + let* output = + RPC.call node (RPC.post_chain_block_helpers_scripts_run_operation op_json) + in + let operation_result = + JSON.(output |-> "contents" |=> 0 |-> "metadata" |-> "operation_result") + in + Log.info + "Checking metadata.operation_result: %s" + (JSON.encode operation_result) ; + Check.( + (JSON.(operation_result |-> "status" |> as_string) = "failed") + string + ~error_msg:"Expected operation_result status to be %R, but got %L.") ; + let id = JSON.(operation_result |-> "errors" |=> 0 |-> "id" |> as_string) in + let proto_error = + try List.(hd (rev (String.split_on_char '.' id))) + with exn -> + Test.fail + "Failed to extract proto_error from %s:\n%s" + id + (Printexc.to_string exn) + in + Check.( + (proto_error = previously_revealed_error) + string + ~error_msg:"Expected protocol error %R, but got %L.") ; + unit + in + unit + +(** This test checks that the [run_operation] RPC succeeds on a + well-formed batch containing a transaction, a delegation, and a + second transaction. *) +let test_correct_batch = + Protocol.register_test + ~__FILE__ + ~title:"Run_operation correct batch" + ~tags: + (run_operation_tags + @ ["manager"; "batch"; "transaction"; "delegation"; "correct_batch"]) + @@ fun protocol -> + Log.info "Initialize a node and a client." ; + let* node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + Log.info + "Craft a batch containing: a transaction, a delegation, and a second \ + transaction." ; + let* batch = + let source = Constant.bootstrap1 in + let* counter = Operation.Manager.get_next_counter ~source client in + let transaction1_payload = + Operation.Manager.transfer ~dest:Constant.bootstrap2 () + in + let delegation_payload = + Operation.Manager.delegation ~delegate:Constant.bootstrap3 () + in + let transaction2_payload = + Operation.Manager.transfer ~dest:Constant.bootstrap4 () + in + Operation.Manager.( + operation + (make_batch + ~source + ~counter + [transaction1_payload; delegation_payload; transaction2_payload]) + client) + in + let* batch_json = Operation.make_run_operation_input batch client in + Log.info + "Crafted batch: %s" + (Ezjsonm.value_to_string ~minify:false batch_json) ; + Log.info "Call the [run_operation] RPC on the batch." ; + let* _output = + RPC.(call node (post_chain_block_helpers_scripts_run_operation batch_json)) + in + unit + +(** This test creates a fresh account and calls the [run_operation] + RPC on the revelation of its public key. Then it actually injects + this revelation, and calls [run_operation] on a some other manager + operations from this fresh account. *) +let test_misc_manager_ops_from_fresh_account = + Protocol.register_test + ~__FILE__ + ~title:"Run_operation misc manager ops from fresh account" + ~tags: + (run_operation_tags + @ ["fresh_account"; "manager"; "reveal"; "transaction"; "delegation"]) + @@ fun protocol -> + Log.info "Initialize a node and a client." ; + let* node, client = + Client.init_with_protocol + ~nodes_args:[Synchronisation_threshold 0] + ~protocol + `Client + () + in + let amount = 10_000_000 (* amount of the final transaction (in mutez) *) in + Log.info + "Create a fresh account by giving it [2 x amount] mutez, then baking a \ + block to apply the transaction." ; + let* fresh_account = Client.gen_and_show_keys client in + let* _oph = + Operation.inject_transfer + client + ~source:Constant.bootstrap2 + ~dest:fresh_account + ~gas_limit:1500 + ~amount:(2 * amount) + in + let* () = Client.bake_for_and_wait ~node client in + Log.info + "Craft a revelation of the fresh account's key and call the \ + [run_operation] RPC on it." ; + let* reveal_op = + let manager_op = + Operation.Manager.(make ~source:fresh_account (reveal fresh_account)) + in + Operation.Manager.operation [manager_op] client + in + let* _run_operation_output = + let* op_json = Operation.make_run_operation_input reveal_op client in + RPC.(call node (post_chain_block_helpers_scripts_run_operation op_json)) + in + Log.info "Inject the crafted revelation and bake a block to apply it." ; + let* _oph = Operation.inject reveal_op client in + let* () = Client.bake_for_and_wait ~node client in + Log.info + "Craft a transaction (of [amount] mutez) from the fresh account and call \ + the [run_operation] RPC on it." ; + let* () = + let manager_op = + Operation.Manager.( + make + ~source:fresh_account + (transfer ~dest:Constant.bootstrap1 ~amount ())) + in + let* op = Operation.Manager.operation [manager_op] client in + let* op_json = Operation.make_run_operation_input op client in + let* _output = + RPC.(call node (post_chain_block_helpers_scripts_run_operation op_json)) + in + unit + in + Log.info + "Craft a delegation from the fresh account and call the [run_operation] \ + RPC on it." ; + let* () = + let manager_op = + Operation.Manager.( + make ~source:fresh_account (delegation ~delegate:Constant.bootstrap1 ())) + in + let* op = Operation.Manager.operation [manager_op] client in + let* op_json = Operation.make_run_operation_input op client in + let* _output = + RPC.(call node (post_chain_block_helpers_scripts_run_operation op_json)) + in + unit + in + unit + +let register ~protocols = + test_batch_inconsistent_sources protocols ; + test_inconsistent_counters protocols ; + test_bad_revelations protocols ; + test_correct_batch protocols ; + test_misc_manager_ops_from_fresh_account protocols