Revision 34d6ac8915ee8173e1752d6db140648602c10b3a authored by iguerNL@Functori on 23 January 2023, 09:52:06 UTC, committed by Marge Bot on 23 January 2023, 12:57:17 UTC
1 parent 10318d8
Raw File
manager_operations.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2021 Nomadic Labs <contact@nomadic-labs.com>                *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(* Testing
   -------
   Component:    Precheck
   Invocation:   dune exec tezt/tests/main.exe -- --file manager_operations.ml
   Subject:      Tests that covers differents cases of manager operations
*)

open Tezt_tezos
open Lwt.Infix

(** A node with an associated client (with endpoint this node).  *)
type node_client = {node : Node.t; client : Client.t}

(** A pair of connected nodes. [main] is the node/client with which the tests interact
    directlty (for injections, baking, queries, etc.)
    [observer] acts as a node on which we observe events of the prevalidator. *)
type two_nodes = {main : node_client; observer : node_client}

(* Default transferred amounts and paid fees for the tests declared in this
   file. *)

(** Default fee (1tz) for operations injected in this module.
    This is a large enough value so that all operations should have enough
    fee to cover their gas (unless otherwise specified).
 *)
let fee = 1_000_000

(** Default amount (10tz) for operations injected in this module. *)
let amount = 10_000_000

module Log = struct
  include Log

  let section fmt =
    Log.info ~color:Log.Color.(bold ++ BG.blue ++ FG.bright_white) fmt

  let subsection fmt = Log.info ~color:Log.Color.(bold ++ FG.blue) fmt

  let ok fmt = Log.info ~color:Log.Color.FG.green fmt
end

module Events = struct
  type wait_observer_result =
    [ `New_block_hash of string option
    | `Notify_mempool of string list * string list ]

  let wait_for_injection ?signed_op node =
    let filter json =
      let ev_req = JSON.(json |-> "view" |-> "request" |> as_string_opt) in
      let ev_op =
        JSON.(json |-> "view" |-> "operation" |-> "data" |> as_string_opt)
      in
      match ev_req with
      | Some "inject" -> (
          match signed_op with
          | None -> Some ()
          | Some sop -> (
              (* remove branch field *)
              if String.length sop < 64 then
                Test.fail "signed operation too short" ;
              let sop = String.sub sop 64 (String.length sop - 64) in
              match ev_op with Some op when op = sop -> Some () | _ -> None))
      | _ -> None
    in
    let* () = Node.wait_for node "request_completed_notice.v0" filter in
    return ()

  (** Wait for a node to be notified of a mempool. *)
  let wait_for_notify node =
    let filter json =
      let open JSON in
      match
        ( json |-> "view" |-> "request" |> as_string_opt,
          json |-> "view" |-> "mempool" |-> "known_valid" |> as_list_opt,
          json |-> "view" |-> "mempool" |-> "pending" |> as_list_opt )
      with
      | Some "notify", Some [], Some [] -> None
      | Some "notify", Some known_valid, Some pending ->
          let known_valid = List.map JSON.as_string known_valid in
          let pending = List.map JSON.as_string pending in
          Some (known_valid, pending)
      | _ -> None
    in
    Node.wait_for node "request_completed_debug.v0" filter

  (** Wait for a node to be have processed a new block. *)
  let wait_for_processed_block node =
    let filter json =
      let open JSON in
      match json |-> "view" |-> "hash" |> as_string_opt with
      | None -> None
      | Some s when s.[0] = 'B' -> Some (Some s)
      | Some _ -> Some None
    in
    Lwt.pick
      [
        Node.wait_for node "head_increment.v0" filter;
        Node.wait_for node "branch_switch.v0" filter;
        Node.wait_for node "ignore_head.v0" filter;
      ]

  (** Wait for a node to be notified of a mempool or have processed a
      new block. *)
  let wait_for_notify_or_processed_block node =
    let notify_promise =
      wait_for_notify node >|= fun mempool -> `Notify_mempool mempool
    in
    let processed_promise =
      wait_for_processed_block node >|= fun b -> `New_block_hash b
    in
    Lwt.npick [notify_promise; processed_promise]

  (** Wait for nodes to be synchronized, i.e. for node observer to be

     at the same level as main node (the baker only bakes on main node
     in these tests). *)
  let wait_sync nodes =
    let level = Node.get_level nodes.main.node in
    Node.wait_for_level nodes.observer.node level
end

module Operation = struct
  include Operation

  (* We always inject operations in async and force mode for these tests. *)

  let inject_transfer
      ?(gas_limit =
        1520 (* We make transfers to non allocated contracts in these tests *))
      =
    inject_transfer ~gas_limit ~async:true ~force:true

  let inject_transfers =
    inject_transfers
      ~gas_limit:1520
        (* We make transfers to non allocated contracts in these tests *)
      ~async:true
      ~force:true

  let inject_contract_call = inject_contract_call ~async:true ~force:true

  let inject_public_key_revelation =
    inject_public_key_revelation ~async:true ~force:true

  let inject_origination = inject_origination ~async:true ~force:true

  let inject_transfer_ticket = inject_transfer_ticket ~async:true ~force:true

  let forge_and_inject_operation ?protocol ?branch ~batch ~signer
      ?patch_unsigned client =
    let* branch = get_injection_branch ?branch client in
    let* unsigned_op = forge_operation ?protocol ~batch ~branch client in
    let unsigned_op =
      match patch_unsigned with
      | None -> unsigned_op
      | Some patch ->
          Log.debug "Unsigned op before patching: %a." Hex.pp unsigned_op ;
          let unsigned_op = patch unsigned_op in
          Log.debug "Unsigned op after patching: %a." Hex.pp unsigned_op ;
          unsigned_op
    in
    let signature = sign_manager_op_hex ~signer unsigned_op in
    inject_operation ~async:true ~force:true ~unsigned_op ~signature client
end

(** Helper functions specific to these tests *)
module Helpers = struct
  (** Bake a block and wait for the node to switch to this head *)
  let bake_and_wait_block {client; node} =
    (* We need to have the client build the block without the
       /helpers/preapply/block RPC to the node because this RPC serializes the
       operations before sending them off to Block_validator.preapply.

       This is needed to expose the bug where a baker could build an invalid
       block (wrt. the context hash), if it got the operation deserialized from
       the mempool and then builds a block without accounting for the
       deserialization cost of the parameters. (This is captured by the test
       Deserialization.test_deserialization_gas_accounting.)
    *)
    Client.bake_for_and_wait
      ~context_path:(Node.data_dir node // "context")
      client

  (** Initialize a network with two nodes *)
  let init ?(disable_operation_precheck = false)
      ?(event_sections_levels = [("prevalidator", `Debug)]) ~protocol () =
    let args =
      [Node.Synchronisation_threshold 0; Connections 1]
      @ if disable_operation_precheck then [Disable_operations_precheck] else []
    in
    let node1 = Node.create args in
    let node2 = Node.create args in
    let* client1 = Client.init ~endpoint:(Node node1) ()
    and* client2 = Client.init ~endpoint:(Node node2) () in
    let nodes =
      {
        main = {node = node1; client = client1};
        observer = {node = node2; client = client2};
      }
    in
    Cluster.symmetric_add_peer node1 node2 ;
    let* () = Cluster.start ~event_sections_levels [node1; node2] in
    let* () = Client.activate_protocol_and_wait ~protocol client1 in
    Log.info "Activated protocol" ;
    let* _ = Node.wait_for_level node2 1 in
    let* _ = Events.wait_sync nodes in
    return nodes

  (** Generate a new account (key pair) and credit it with [amount] mutez. If
      the [reveal] flag is [true], the public key of the new account in also
      revealed in a subsequent operation. A block is baked after each
      operation. *)
  let init_fresh_account ?(reveal = false) ?protocol ~amount ~fee nodes =
    let* key = Client.gen_and_show_keys nodes.main.client in
    Log.section "Initializing fresh account %s" key.public_key_hash ;
    let* _oph =
      Operation.inject_transfer
        ?protocol
        nodes.main.client
        ~source:Constant.bootstrap2
        ~dest:key
        ~gas_limit:1500
        ~amount
        ~fee
    in
    let* () = bake_and_wait_block nodes.main in
    let* () =
      if not reveal then unit
      else
        let* _oph =
          Operation.inject_public_key_revelation
            ?protocol
            nodes.main.client
            ~source:key
            ~fee
        in
        bake_and_wait_block nodes.main
    in
    Lwt.return key

  let originate_contract protocol nodes script_name =
    Log.info
      "- Auxiliary step: originate contract %s."
      Michelson_script.(find script_name protocol |> name_s) ;
    let* _alias, contract =
      Client.originate_contract_at
        ~wait:"none"
        ~init:"{}"
        ~amount:Tez.zero
        ~burn_cap:(Tez.of_int 10)
        ~src:Constant.bootstrap1.alias
        nodes.main.client
        script_name
        protocol
    in
    let* () = bake_and_wait_block nodes.main in
    Log.info "  - Contract address is %s." contract ;
    return contract

  type hard_gas_limits = {
    hard_gas_limit_per_operation : int;
    hard_gas_limit_per_block : int;
  }

  let gas_limits client =
    let* constants =
      RPC.Client.call client @@ RPC.get_chain_block_context_constants ()
    in
    let hard_gas_limit_per_operation =
      JSON.(constants |-> "hard_gas_limit_per_operation" |> as_int)
    in
    let hard_gas_limit_per_block =
      JSON.(constants |-> "hard_gas_limit_per_block" |> as_int)
    in
    return {hard_gas_limit_per_operation; hard_gas_limit_per_block}
end

(** This module provides helper functions and wrappers to check the
    classification of operations in the mempool, their propagation and
    their inclusion in a block. *)
module Memchecks = struct
  let string_of_classification = function
    | `Applied -> "applied"
    | `Refused -> "refused"
    | `Branch_refused -> "branch_refused"
    | `Branch_delayed -> "branch_delayed"
    | `Outdated -> "outdated"
    | `Unprocessed -> "unprocessed"

  let string_of_ext_classification = function
    | `Absent -> "absent"
    | `Not c -> "not " ^ string_of_classification c
    | ( `Applied | `Refused | `Branch_refused | `Branch_delayed | `Outdated
      | `Unprocessed ) as c ->
        string_of_classification c

  let mempool_get_operations ?classification (mempool : Mempool.t) =
    match classification with
    | None ->
        List.concat
          [
            mempool.applied;
            mempool.refused;
            mempool.branch_refused;
            mempool.branch_delayed;
            mempool.outdated;
            mempool.unprocessed;
          ]
    | Some c -> (
        match c with
        | `Applied -> mempool.applied
        | `Refused -> mempool.refused
        | `Branch_refused -> mempool.branch_refused
        | `Branch_delayed -> mempool.branch_delayed
        | `Outdated -> mempool.outdated
        | `Unprocessed -> mempool.unprocessed)

  let check_operation_is_in_mempool ~__LOC__ classification ?(explain = "")
      mempool oph =
    Check.(list_mem string)
      ~__LOC__
      oph
      (mempool_get_operations ~classification mempool)
      ~error_msg:
        (sf
           "expected to find %%L in mempool.%s = %%R %s"
           (string_of_classification classification)
           explain)

  let check_operation_not_in_mempool ~__LOC__ ?classification ?(explain = "")
      mempool oph =
    let classification_str =
      match classification with
      | None -> "mempool"
      | Some c -> string_of_classification c
    in
    Check.(list_not_mem string)
      ~__LOC__
      oph
      (mempool_get_operations ?classification mempool)
      ~error_msg:
        (sf "expected %%L to not be in %s = %%R %s" classification_str explain)

  type classification =
    [ `Applied
    | `Branch_delayed
    | `Branch_refused
    | `Outdated
    | `Unprocessed
    | `Refused ]

  type extended_classification =
    [`Absent | classification | `Not of classification]

  let check_operation_classification ~__LOC__ classification ?explain mempool
      oph =
    match classification with
    | ( `Applied | `Refused | `Branch_refused | `Branch_delayed | `Outdated
      | `Unprocessed ) as classification ->
        check_operation_is_in_mempool
          ~__LOC__
          classification
          ?explain
          mempool
          oph
    | `Absent -> check_operation_not_in_mempool ~__LOC__ ?explain mempool oph
    | `Not classification ->
        check_operation_not_in_mempool
          ~__LOC__
          ?explain
          ~classification
          mempool
          oph

  let get_op_status op =
    JSON.(op |-> "metadata" |-> "operation_result" |-> "status" |> as_string)

  let get_op_errors_ids op =
    let errs =
      JSON.(op |-> "metadata" |-> "operation_result" |-> "errors" |> as_list)
    in
    List.map (fun err -> JSON.(err |-> "id" |> as_string)) errs

  let is_in_block ?block client oph =
    let* head = RPC.Client.call client @@ RPC.get_chain_block ?block () in
    let ops = JSON.(head |-> "operations" |=> 3 |> as_list) in
    Lwt.return
    @@ List.exists (fun op -> oph = JSON.(op |-> "hash" |> as_string)) ops

  let check_op_in_block ~__LOC__ ?block ?(explain = "") ~should_include client
      oph =
    let* in_block = is_in_block ?block client oph in
    if (not in_block) && should_include then
      Test.fail ~__LOC__ "%s not included in block %s." oph explain ;
    if in_block && not should_include then
      Test.fail ~__LOC__ "%s found in head block %s" oph explain ;
    unit

  let check_op_not_propagated ~__LOC__ ?(explain = "") ~should_include observer
      oph observer_result =
    Lwt_list.iter_p
      (function
        | `Notify_mempool (known_valid, pending) ->
            if List.mem oph known_valid then
              Test.fail
                ~__LOC__
                "%s was propagated to observer node as valid %s"
                oph
                explain
            else if List.mem oph pending then
              Test.fail
                ~__LOC__
                "%s was propagated to observer node as pending %s"
                oph
                explain
            else () ;
            return ()
        | `New_block_hash block ->
            check_op_in_block
              ~should_include
              ~__LOC__
              ?block
              observer.client
              oph
              ~explain:("of observer node " ^ explain))
      observer_result
    >|= fun () ->
    Log.ok "  - %s was not propagated to observer node %s." oph explain

  (** Check that errors appear in the list of errors for an operation included
      in a block, where [op_content] is the operation and associated metadata as
      found in a block (in JSON).

      If [expected_errors] is [None], don't check the errors. If
      [expected_errors = Some []], ensures there are no errors.  Otherwise all
      the specified errors must appear (in any order) as substrings of the errors
      associated to this opeation in the block's metadata. *)
  let check_op_errors ~__LOC__ ~op_contents ~expected_errors =
    match expected_errors with
    | None -> ()
    | Some expected_errors ->
        Check.((List.length op_contents = List.length expected_errors) int)
          ~error_msg:
            "expected content length = expected error count (%R), but got %L" ;
        List.iter2
          (fun op expected_errors ->
            let errors = get_op_errors_ids op in
            match expected_errors with
            | [] ->
                if errors <> [] then
                  Test.fail
                    ~__LOC__
                    "Operation should not have any errors:\n%s"
                    (JSON.encode op)
            | _ ->
                List.iter
                  (fun expected_error ->
                    if
                      not
                      @@ List.exists (fun s -> s =~ rex expected_error) errors
                    then
                      Test.fail
                        ~__LOC__
                        "Operation is not included with error %s. Errors are \
                         [%s]"
                        expected_error
                        (String.concat ", " errors))
                  expected_errors)
          op_contents
          expected_errors

  (** Check that operation whose hash is [oph] is included in the
      block [block] or the head, with the statuses [expected_statuses]
      (there can be several if the operation is a batch).

      If [expected_statuses] is empty, check that the operation is not
      included in the block.

      Also check (when argument [expected_errors] is provided) that it
      is included with these errors (all elements in each item of the list
      must be a substring of one of the errors). If one of the list of
      errors is empty, then the operation at this position in the batch must
      be included without errors.
      For instance [~expected_errors:[["ill_typed"]]] checks that there is
      only one operation in the batch and that the result {i contains} an error
      with ["ill_typed"] somewhere in its identifier.
*)
  let check_status_in_block ~__LOC__ ~who ~oph ~expected_statuses
      ?expected_errors ?block client =
    Log.info "- Checking inclusion and status of operation in %s's block." who ;
    let* head = RPC.Client.call client @@ RPC.get_chain_block ?block () in
    let ops = JSON.(head |-> "operations" |=> 3 |> as_list) in
    let head_hash = JSON.(head |-> "hash" |> as_string) in
    let op_contents =
      match
        List.find_opt (fun op -> oph = JSON.(op |-> "hash" |> as_string)) ops
      with
      | None -> []
      | Some op -> JSON.(op |-> "contents" |> as_list)
    in
    Check.((List.length op_contents = List.length expected_statuses) int)
      ~error_msg:"expected contents to contain %R values, got %L" ;
    List.iter2
      (fun op expected_status ->
        let status = get_op_status op in
        if not (String.equal status expected_status) then
          Test.fail
            ~__LOC__
            "Unexpected operation status: %s got %s instead of %s for operation:\n\
             %s"
            who
            status
            expected_status
            (JSON.encode op))
      op_contents
      expected_statuses ;
    check_op_errors ~__LOC__ ~op_contents ~expected_errors ;
    return head_hash

  (** Inject an opertion while performing a series of checks:
      - Start waiting for the observer node to receive operations
      - Inject operation
      - Check classification in mempool of main node
      - Wait for the operations to be propagated to observer
      - Check that observer was indeed notified of the operation
      - Check classification in mempool of observer node
      - Bake a block on main node
      - Check that the operation is included in said block
      - Check that the operation is not in the mempool anymore
  *)
  let with_applied_checks ~__LOC__ nodes ~expected_statuses ?expected_errors
      ?(bake = true) ?(observer_classification = `Applied) inject =
    Log.subsection "Checking applied operation" ;
    let* _ = Events.wait_sync nodes in
    let client = nodes.main.client in
    let wait_observer = Events.wait_for_notify nodes.observer.node in
    Log.info "- Injecting operation." ;
    let* (`OpHash oph) = inject () in
    let* mempool_after_injection = Mempool.get_mempool client in
    check_operation_is_in_mempool
      `Applied
      ~__LOC__
      ~explain:"after injection"
      mempool_after_injection
      oph ;
    Log.info "- Waiting for observer to be notified of operation." ;
    let* observer_result = wait_observer in
    Log.info "- Checking observer received operations." ;
    let known_valid, pending = observer_result in
    if List.mem oph known_valid then
      Log.ok "  - %s was propagated to observer node as valid." oph
    else if List.mem oph pending then
      Test.fail ~__LOC__ "%s was propagated to observer node as pending" oph ;
    let* mempool_observer = Mempool.get_mempool nodes.observer.client in
    let check_observer_mempool =
      match observer_classification with
      | ( `Applied | `Refused | `Branch_refused | `Branch_delayed | `Outdated
        | `Unprocessed ) as classification ->
          check_operation_is_in_mempool classification
      | `Absent -> check_operation_not_in_mempool ?classification:None
    in
    check_observer_mempool ~__LOC__ ~explain:"in observer" mempool_observer oph ;
    if bake then (
      Log.info "- Baking (should include operation %s)." oph ;
      let* () = Helpers.bake_and_wait_block nodes.main in
      let* _head_hash =
        check_status_in_block
          ~__LOC__
          ~oph
          ~expected_statuses
          ?expected_errors
          ~who:"main"
          client
      in
      let* mempool_after_baking = Mempool.get_mempool client in
      check_operation_not_in_mempool
        ~__LOC__
        ~classification:`Applied
        ~explain:"after baking"
        mempool_after_baking
        oph ;
      return oph)
    else return oph

  let with_checks ~__LOC__ ?(bake = true) ~classification
      ?(classification_after_flush = classification) ~should_propagate
      ?(should_include = should_propagate) nodes inject =
    Log.subsection
      "Checking %s operation"
      (string_of_ext_classification classification) ;
    let* _ = Events.wait_sync nodes in
    let client = nodes.main.client in
    let wait_observer =
      Events.wait_for_notify_or_processed_block nodes.observer.node
    in
    Log.info "- Injecting operation." ;
    let* (`OpHash oph) = inject () in
    let* mempool_after_injection = Mempool.get_mempool client in
    check_operation_classification
      classification
      ~__LOC__
      mempool_after_injection
      oph
      ~explain:"after injection" ;
    Log.info
      "- Baking (should%s include operation %s)."
      (if should_include then "" else " not")
      oph ;
    if not bake then return oph
    else
      let* () = Helpers.bake_and_wait_block nodes.main in
      Log.info "- Waiting for observer to see operation or block." ;
      let* observer_result = wait_observer in
      Log.info "- Checking mempool of main node." ;
      let* mempool_after_baking = Mempool.get_mempool client in
      check_operation_classification
        classification_after_flush
        ~__LOC__
        mempool_after_baking
        oph
        ~explain:"after baking" ;
      Log.info "- Checking that observer did not observe operation." ;
      let* () =
        check_op_in_block
          ~__LOC__
          client
          oph
          ~should_include
          ~explain:"newly baked"
      and* () =
        check_op_not_propagated
          ~__LOC__
          nodes.observer
          oph
          observer_result
          ~should_include
          ~explain:(string_of_ext_classification classification)
      in
      return oph

  let with_refused_checks =
    with_checks ~classification:`Refused ~should_propagate:false

  let with_branch_refused_checks =
    with_checks ~classification:`Branch_refused ~should_propagate:false

  let with_branch_delayed_checks =
    with_checks ~classification:`Branch_delayed ~should_propagate:false

  let with_absent_checks =
    with_checks ~classification:`Absent ~should_propagate:false

  let check_balance ~__LOC__ {client; _} key amount =
    let* bal =
      RPC.Client.call client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:key.Account.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    if bal <> amount then
      Test.fail
        ~__LOC__
        "Unexpected balance. Got %d instead of %d mutez"
        bal
        amount ;
    unit

  let check_revealed ~__LOC__ {client; _} key ~revealed =
    let* res =
      RPC.Client.call client
      @@ RPC.get_chain_block_context_contract_manager_key
           ~id:key.Account.public_key_hash
           ()
    in
    let is_revealed = not (JSON.is_null res) in
    if is_revealed && JSON.as_string res <> key.public_key then
      Test.fail
        ~__LOC__
        "Bad revealed public key: %s but should be %s"
        (JSON.as_string res)
        key.public_key ;
    if is_revealed == revealed then
      Log.ok
        "  - Public key is %srevealed."
        (if is_revealed then "" else "not ")
    else
      Test.fail
        ~__LOC__
        "should_be_revealed = %b but is_revealed = %b"
        revealed
        is_revealed ;
    unit
end
(* of Memchecks*)

(* Hardcoded predefined contracts *)
module Contracts = struct
  (* parameter unit ;
     storage nat ;
     code { DROP ; PUSH int 0 ; NIL operation ; PAIR } *)
  let illtyped_contract_body_1 =
    Ezjsonm.from_string
      {|
[ { "prim": "parameter", "args": [ { "prim": "unit" } ] },
  { "prim": "storage", "args": [ { "prim": "nat" } ] },
  { "prim": "code",
    "args":
      [ [ { "prim": "DROP" },
          { "prim": "PUSH", "args": [ { "prim": "int" }, { "int": "0" } ] },
          { "prim": "NIL", "args": [ { "prim": "operation" } ] },
          { "prim": "PAIR" } ] ] } ]
|}

  (* parameter unit ;
     storage nat ;
     code { DROP ; PUSH nat 0 ; NEG; NIL operation ; PAIR } *)
  let illtyped_contract_body_2 =
    Ezjsonm.from_string
      {|
[ { "prim": "parameter", "args": [ { "prim": "unit" } ] },
  { "prim": "storage", "args": [ { "prim": "nat" } ] },
  { "prim": "code",
    "args":
      [ [ { "prim": "DROP" },
          { "prim": "PUSH", "args": [ { "prim": "nat" }, { "int": "0" } ] },
          { "prim": "NEG" },
          { "prim": "NIL", "args": [ { "prim": "operation" } ] },
          { "prim": "PAIR" } ] ] } ]
|}
end

module Illtyped_originations = struct
  let contract_body_illtyped_1 =
    Protocol.register_test
      ~__FILE__
      ~title:"Contract's body illtyped 1"
      ~tags:["precheck"; "illtyped"; "origination"; "typing"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["ill_typed_contract"; "bad_return"]]
      @@ fun () ->
      Operation.inject_origination
        ~protocol
        ~source:Constant.bootstrap1
        ~init_storage:(`Json (`O [("int", `String "0")]))
        ~code:(`Json Contracts.illtyped_contract_body_1)
        nodes.main.client
    in
    unit

  let contract_body_illtyped_2 =
    Protocol.register_test
      ~__FILE__
      ~title:"Contract's body illtyped 2"
      ~tags:["precheck"; "illtyped"; "origination"; "typing"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["ill_typed_contract"; "bad_return"]]
      @@ fun () ->
      Operation.inject_origination
        ~protocol
        ~source:Constant.bootstrap1
        ~init_storage:(`Json (`O [("int", `String "0")]))
        ~code:(`Json Contracts.illtyped_contract_body_2)
        nodes.main.client
    in
    unit

  let initial_storage_illtyped =
    Protocol.register_test
      ~__FILE__
      ~title:"Contract's initial storage illtyped"
      ~tags:["precheck"; "illtyped"; "origination"; "typing"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["ill_typed_data"; "invalid_constant"]]
      @@ fun () ->
      Operation.inject_origination
        ~protocol
        ~source:Constant.bootstrap1
        ~init_storage:(`Json (`O [("int", `String "-10")]))
        ~code:
          (`File
            Michelson_script.(
              find ["mini_scenarios"; "parsable_contract"] protocol |> path))
        nodes.main.client
    in
    unit

  let register ~protocols =
    contract_body_illtyped_1 protocols ;
    contract_body_illtyped_2 protocols ;
    initial_storage_illtyped protocols
end

module Deserialisation = struct
  let milligas_per_byte = 20 (* As per the protocol *)

  (** Returns the gas needed for the deserialization of an argument of
      size [size_kB] in kilobytes. *)
  let deserialization_gas ~size_kB = size_kB * milligas_per_byte

  (** Returns an hexadecimal representation of a zero byte sequence of
     size [size_kB]. *)
  let make_zero_hex ~size_kB =
    (* A hex representation for a byte sequence of n bytes is 2n long,
       so for n kB it is 2000n long *)
    String.make (size_kB * 2000) '0'

  (* Originate a contract that takes a byte sequence as argument and does nothing *)
  let originate_noop_contract protocol nc =
    let* _alias, contract =
      Client.originate_contract_at
        ~wait:"none"
        ~init:"Unit"
        ~amount:Tez.zero
        ~burn_cap:Tez.one
        ~src:Constant.bootstrap1.alias
        nc.client
        ["mini_scenarios"; "noop_bytes"]
        protocol
    in
    let* () = Helpers.bake_and_wait_block nc in
    return contract

  (* Gas to execute call to noop contract without deserialization *)
  let gas_to_execute_rest_noop = function
    | Protocol.Lima | Mumbai | Alpha -> 2109

  let inject_call_with_bytes ?(source = Constant.bootstrap5) ?protocol ~contract
      ~size_kB ~gas_limit client =
    let* op =
      Operation.mk_call
        ~entrypoint:"default"
        ~arg:(`Json (`O [("bytes", `String (make_zero_hex ~size_kB))]))
        ~gas_limit
        ~dest:contract
        ~source
        client
    in
    Operation.forge_and_inject_operation
      ?protocol
      ~batch:(`Manager [op])
      ~signer:source
      client

  let test_deserialization_gas_canary =
    Protocol.register_test
      ~__FILE__
      ~title:
        "Smart contract call that should succeeds with the provided gas limit"
      ~tags:["precheck"; "gas"; "deserialization"; "canary"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract = originate_noop_contract protocol nodes.main in
    let size_kB = 20 in
    let min_deserialization_gas = deserialization_gas ~size_kB in
    let gas_for_the_rest = gas_to_execute_rest_noop protocol in
    (* This is specific to this contract, obtained empirically *)
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["applied"]
      @@ fun () ->
      inject_call_with_bytes
        ~protocol
        ~contract
        ~size_kB:20
        ~gas_limit:(min_deserialization_gas + gas_for_the_rest)
        (* Enough gas to deserialize and do the application *)
        nodes.main.client
    in
    unit

  let test_not_enough_gas_deserialization =
    Protocol.register_test
      ~__FILE__
      ~title:"Contract call with not enough gas to deserialize argument"
      ~supports:(Protocol.From_protocol 14)
      ~tags:["precheck"; "gas"; "deserialization"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract = originate_noop_contract protocol nodes.main in
    let size_kB = 20 in
    let min_deserialization_gas =
      Constant.manager_operation_gas_cost + deserialization_gas ~size_kB
    in
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      inject_call_with_bytes
        ~protocol
        ~contract
        ~size_kB
        ~gas_limit:(min_deserialization_gas - 1)
        nodes.main.client
    in
    unit

  let test_deserialization_gas_accounting =
    Protocol.register_test
      ~__FILE__
      ~title:
        "Smart contract call that would succeed if we did not account \
         deserialization gas correctly"
      ~tags:["precheck"; "gas"; "deserialization"; "lazy_expr"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract = originate_noop_contract protocol nodes.main in
    let size_kB = 20 in
    let min_deserialization_gas = deserialization_gas ~size_kB in
    let gas_for_the_rest = gas_to_execute_rest_noop protocol in
    (* This is specific to this contract, obtained empirically *)
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["gas_exhausted.operation"]]
      @@ fun () ->
      inject_call_with_bytes
        ~protocol
        ~contract
        ~size_kB:20
        ~gas_limit:(min_deserialization_gas + gas_for_the_rest - 1)
        (* Enough gas to deserialize or to do the rest, but not to do both *)
        nodes.main.client
    in
    unit

  let register ~protocols =
    test_deserialization_gas_canary protocols ;
    test_not_enough_gas_deserialization protocols ;
    test_deserialization_gas_accounting protocols
end

module Gas_limits = struct
  (** Build a batch of transfers with the same given gas limit for every one of
      them.  *)
  let mk_batch ?(source = Constant.bootstrap2) ?(dest = Constant.bootstrap3) ~nb
      ~gas_limit client =
    let open Operation.Manager in
    let fee = 1_000_000 in
    let* counter = get_next_counter client ~source:Constant.bootstrap1 in
    let transfers = List.map (fun _ -> transfer ~dest ()) (range 1 nb) in
    make_batch ~source ~gas_limit ~fee ~counter transfers |> return

  let block_below_ops_below =
    Protocol.register_test
      ~__FILE__
      ~title:"Batch below block limit with each operation below limit"
      ~tags:["precheck"; "batch"; "gas"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* limits = Helpers.gas_limits nodes.main.client in
    (* Gas limit per op is ok *)
    let* batch =
      mk_batch
        ~nb:2
        ~gas_limit:limits.hard_gas_limit_per_operation
        nodes.main.client
    in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["applied"; "applied"]
      @@ fun () -> Operation.Manager.inject batch nodes.main.client
    in
    unit

  let block_below_ops_over =
    Protocol.register_test
      ~__FILE__
      ~title:"Batch below block limit with operations over limit"
      ~tags:["precheck"; "batch"; "gas"; "op_gas"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* limits = Helpers.gas_limits nodes.main.client in
    let* batch =
      mk_batch
        ~nb:2
        ~gas_limit:(limits.hard_gas_limit_per_operation + 1)
        nodes.main.client
    in
    let* _oph =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      (* Gas limit per op is too high *)
      Operation.Manager.inject ~force:true batch nodes.main.client
    in
    unit

  let block_over_ops_below =
    Protocol.register_test
      ~__FILE__
      ~title:"Batch over block limit with operations below limit"
      ~tags:["precheck"; "batch"; "gas"; "block_gas"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* limits = Helpers.gas_limits nodes.main.client in
    (* Gas limit per block is too high *)
    let too_many_ops =
      (limits.hard_gas_limit_per_block / limits.hard_gas_limit_per_operation)
      + 1
    in
    let* batch =
      mk_batch
        ~nb:too_many_ops
        ~gas_limit:limits.hard_gas_limit_per_operation
        nodes.main.client
    in
    let* _oph =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.Manager.inject ~force:true batch nodes.main.client
    in
    unit

  let register ~protocols =
    block_below_ops_below protocols ;
    block_below_ops_over protocols ;
    block_over_ops_below protocols
end

module Reveal = struct
  (* This auxiliary function forges and injects a batched operation
     made of two revelations pk1 and pk2. The transaction is signed by
     the given key. *)
  let mk_reveal_twice {client; _} key pk1 pk2 =
    let* cpt = Operation.get_counter client ~source:key in
    let s1 = {key with Account.public_key = pk1} in
    let s2 = {key with Account.public_key = pk2} in
    let* op1 = Operation.mk_reveal ~source:s1 ~counter:(cpt + 1) ~fee client in
    let* op2 = Operation.mk_reveal ~source:s2 ~counter:(cpt + 2) ~fee client in
    Operation.forge_and_inject_operation
      ~batch:(`Manager [op1; op2])
      ~signer:key
      client

  let simple_reveal_bad_pk =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple revelation with a wrong public key"
      ~tags:["reveal"; "revelation"; "batch"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    let* key_to_reveal = Client.gen_and_show_keys nodes.main.client in
    Log.section "Make the revelation" ;
    let* _oph =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_public_key_revelation
        ~protocol
        ~source:key
        ~public_key:
          key_to_reveal.public_key (* key_to_reveal is different from key *)
        ~fee
        nodes.main.client
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key amount in
    Memchecks.check_revealed ~__LOC__ nodes.main key ~revealed:false

  let simple_reveal_not_a_pk =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple revelation with something that is not a public key"
      ~tags:["reveal"; "revelation"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    Log.section "Make the revelation" ;
    let* op = Operation.mk_reveal ~source:key ~fee nodes.main.client in
    let patch_unsigned (`Hex op) =
      (* public key is in the last field, add extra byte *)
      `Hex (op ^ "00")
    in
    let* _oph =
      Memchecks.with_absent_checks ~__LOC__ nodes @@ fun () ->
      Operation.forge_and_inject_operation
        ~protocol
        ~batch:(`Manager [op])
        ~signer:key
        ~patch_unsigned
        nodes.main.client
    in
    unit

  let revealed_twice_in_batch =
    Protocol.register_test
      ~__FILE__
      ~title:"Correct public key revealed twice in a batch"
      ~tags:["reveal"; "revelation"; "batch"]
      ~supports:(Protocol.From_protocol 14)
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    Log.section "Make the revelation" ;
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      mk_reveal_twice nodes.main key key.public_key key.public_key
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key amount in
    Memchecks.check_revealed ~__LOC__ nodes.main key ~revealed:false

  (* After the work in !5182, which enforces that reveal operations
     can only be placed at the head of the batch, this test should
     fail with a permanent, Apply.Incorrect_reveal_position error (see
     #2774). *)
  let revealed_twice_in_batch_bad_first_key =
    Protocol.register_test
      ~__FILE__
      ~title:"Two reveals in a batch. First key is wrong"
      ~tags:["reveal"; "revelation"; "batch"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    Log.section "Make the revelation" ;
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      mk_reveal_twice
        nodes.main
        key
        Constant.bootstrap1.public_key
        key.public_key
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key amount in
    Memchecks.check_revealed ~__LOC__ nodes.main key ~revealed:false

  let revealed_twice_in_batch_bad_second_key =
    Protocol.register_test
      ~__FILE__
      ~title:"Two reveals in a batch. Second key is wrong"
      ~tags:["reveal"; "revelation"]
      ~supports:(Protocol.From_protocol 14)
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    Log.section "Make the revelation" ;
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      mk_reveal_twice
        nodes.main
        key
        key.public_key
        Constant.bootstrap1.public_key
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key amount in
    Memchecks.check_revealed ~__LOC__ nodes.main key ~revealed:false

  let register ~protocols =
    simple_reveal_bad_pk protocols ;
    simple_reveal_not_a_pk protocols ;
    revealed_twice_in_batch protocols ;
    revealed_twice_in_batch_bad_first_key protocols ;
    revealed_twice_in_batch_bad_second_key protocols
end

module Simple_transfers = struct
  let test_simple_transfer_applied =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer applied"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key = Helpers.init_fresh_account ~protocol nodes ~amount ~fee in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key amount in
    Memchecks.check_revealed ~__LOC__ nodes.main key ~revealed:false

  let test_simple_transfer_low_balance_to_pay_fees =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer not enough balance to pay fees"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* bal =
      RPC.Client.call nodes.main.client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:Constant.bootstrap2.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    let* _ =
      Memchecks.with_branch_delayed_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee:((2 * bal) + 1) (* Too high fee *)
        ~amount:1
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main Constant.bootstrap2 bal
    in
    Memchecks.check_revealed
      ~__LOC__
      nodes.main
      Constant.bootstrap2
      ~revealed:true

  let test_simple_transfer_low_balance_to_make_transfer =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer not enough balance to make transfer"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* bal =
      RPC.Client.call nodes.main.client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:Constant.bootstrap2.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["balance_too_low"]]
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee:(bal - 1) (* fee and amount too large: cannot pay [fee + amount] *)
        ~amount:(bal / 2)
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main Constant.bootstrap2 1
    in
    Memchecks.check_revealed
      ~__LOC__
      nodes.main
      Constant.bootstrap2
      ~revealed:true

  let test_simple_transfer_counter_in_the_past =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer counter in the past"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* counter =
      Operation.get_counter nodes.main.client ~source:Constant.bootstrap2
    in
    let* bal =
      RPC.Client.call nodes.main.client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:Constant.bootstrap2.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    let* _ =
      Memchecks.with_branch_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee:(fee + 1)
        ~amount:(bal - fee)
        ~counter (* Specifying existing counter: wrong *)
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main Constant.bootstrap2 bal
    in
    Memchecks.check_revealed
      ~__LOC__
      nodes.main
      Constant.bootstrap2
      ~revealed:true

  let test_simple_transfer_counter_in_the_future =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer counter in the future"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* counter =
      Operation.get_counter nodes.main.client ~source:Constant.bootstrap2
    in
    let* bal =
      RPC.Client.call nodes.main.client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:Constant.bootstrap2.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    let* _ =
      Memchecks.with_branch_delayed_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee:(fee + 1)
        ~amount:(bal - fee)
        ~counter:(counter + 5)
          (* Counter too large (aka "in the future"): wrong *)
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main Constant.bootstrap2 bal
    in
    Memchecks.check_revealed
      ~__LOC__
      nodes.main
      Constant.bootstrap2
      ~revealed:true

  let test_simple_transfer_wrong_signature =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer with wrong signature"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* bal =
      RPC.Client.call nodes.main.client
      @@ RPC.get_chain_block_context_contract_balance
           ~id:Constant.bootstrap2.public_key_hash
           ()
    in
    let bal = Tez.to_mutez bal in
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~signer:Constant.bootstrap3 (* signer is different from source *)
        ~amount
        ~fee
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main Constant.bootstrap2 bal
    in
    Memchecks.check_revealed
      ~__LOC__
      nodes.main
      Constant.bootstrap2
      ~revealed:true

  let test_simple_transfer_not_enough_gas =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer with not enough gas"
      ~tags:["transaction"; "transfer"]
      ~supports:(Protocol.From_protocol 14)
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee
        ~amount
        nodes.main.client
        ~gas_limit:1
      (* Gas too small *)
    in
    unit

  (* FIXME: https://gitlab.com/tezos/tezos/-/issues/2077
     Once this issue is fixed change the test to check that the operation is refused
     and not propagated.

     Note: At the moment, the pre-filter (hence pre-check) of the mempool is not
     called for operations injected to the node directly (but rather for the
     ones that are received from another node) otherwise these would have been
     classified as "rejected".
  *)
  let test_simple_transfer_not_enough_fees_for_gas =
    Protocol.register_test
      ~__FILE__
      ~title:"Simple transfer with not enough fees to cover gas"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:[]
        ~observer_classification:`Refused
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~fee:150
        ~amount
        nodes.main.client
    in
    unit

  let test_simple_transfer_low_balance_to_pay_allocation_1 =
    Protocol.register_test
      ~__FILE__
      ~title:"Test simple transfer with low balance to pay allocation (1)"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key1 =
      Helpers.init_fresh_account ~protocol ~reveal:true nodes ~amount ~fee
    in
    let* key2 = Client.gen_and_show_keys nodes.main.client in
    let balance = amount - fee in
    (* subtract fees payed for revelation *)
    let to_transfer = balance - fee - 1 in
    (* In theory, if the operation succeeds, there will remain 1 mutez on the account *)
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["backtracked"]
        ~expected_errors:[["contract.cannot_pay_storage_fee"]]
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:key1
        ~dest:key2
        ~gas_limit:1500
        ~fee
        ~amount:to_transfer
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main key1 (balance - fee)
    in
    let* () =
      Memchecks.check_revealed ~__LOC__ nodes.main key1 ~revealed:true
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key2 0 in
    Memchecks.check_revealed ~__LOC__ nodes.main key2 ~revealed:false

  let test_simple_transfer_low_balance_to_pay_allocation_2 =
    Protocol.register_test
      ~__FILE__
      ~title:"Test simple transfer with low balance to pay allocation (2)"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key1 =
      Helpers.init_fresh_account ~protocol ~reveal:true nodes ~amount ~fee
    in
    let* key2 = Client.gen_and_show_keys nodes.main.client in
    let balance = amount - fee in
    (* subtract revelation fees *)
    let to_transfer = balance - fee in
    (* In theory, if the operation succeeds, there will remain 0 mutez on the account *)
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["backtracked"]
        ~expected_errors:[["contract.cannot_pay_storage_fee"]]
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:key1
        ~dest:key2
        ~gas_limit:1900
        ~fee
        ~amount:to_transfer
        nodes.main.client
    in
    let* () =
      Memchecks.check_balance ~__LOC__ nodes.main key1 (balance - fee)
    in
    let* () =
      Memchecks.check_revealed ~__LOC__ nodes.main key1 ~revealed:true
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key2 0 in
    Memchecks.check_revealed ~__LOC__ nodes.main key2 ~revealed:false

  let test_simple_transfer_of_the_whole_balance =
    Protocol.register_test
      ~__FILE__
      ~title:"Test simple transfer of the whole balance"
      ~tags:["transaction"; "transfer"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* key1 =
      Helpers.init_fresh_account ~protocol ~reveal:true nodes ~amount ~fee
    in
    let balance = amount - fee in
    (* subtract revelation fees *)
    let to_transfer = balance - fee in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["applied"]
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:key1
        ~dest:Constant.bootstrap2
        ~gas_limit:1900
        ~fee
        ~amount:to_transfer
        nodes.main.client
    in
    let* () = Memchecks.check_balance ~__LOC__ nodes.main key1 0 in
    let* () =
      Memchecks.check_revealed ~__LOC__ nodes.main key1 ~revealed:false
    in
    unit

  let test_simple_transfers_successive_wrong_counters =
    Protocol.register_test
      ~__FILE__
      ~title:"Test succesive injections with same manager"
      ~supports:(Protocol.From_protocol 14)
      ~tags:["transaction"; "transfer"; "counters"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* counter =
      Operation.get_counter nodes.main.client ~source:Constant.bootstrap2
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:[]
        ~bake:false
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~counter:(counter + 1) (* Reuse counter: wrong *)
        ~amount:1
        nodes.main.client
    in
    let* _ =
      Memchecks.with_branch_delayed_checks
        ~__LOC__
        nodes
        ~classification_after_flush:`Branch_refused
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~counter:(counter + 1)
        ~amount:2
        nodes.main.client
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:[]
        ~bake:false
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~counter:(counter + 2)
        ~amount:1
        nodes.main.client
    in
    let* _ =
      Memchecks.with_branch_delayed_checks
        ~__LOC__ (* ~classification_after_flush:`Branch_delayed *)
        ~classification_after_flush:`Applied
        ~should_include:false (* applied after flush *)
        nodes
      @@ fun () ->
      Operation.inject_transfer
        ~protocol
        ~source:Constant.bootstrap2
        ~dest:Constant.bootstrap3
        ~counter:(counter + 3)
        ~amount:2
        nodes.main.client
    in
    unit

  let test_batch_simple_transfers_wrong_counters =
    Protocol.register_test
      ~__FILE__
      ~title:"Test batch with wrong counters (+1, +2, +2)"
      ~tags:["transaction"; "transfer"; "counters"; "batch"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* counter =
      Operation.get_counter nodes.main.client ~source:Constant.bootstrap2
    in
    let make_transfer ~counter =
      Operation.Manager.(
        make ~source:Constant.bootstrap2 ~counter
        @@ transfer ~dest:Constant.bootstrap3 ())
    in
    let op1 = make_transfer ~counter:(counter + 1) in
    let op2 = make_transfer ~counter:(counter + 2) in
    let op3 = op2 in
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.Manager.inject ~force:true [op1; op2; op3] nodes.main.client
    in
    unit

  let test_batch_simple_transfers_wrong_counters_2 =
    Protocol.register_test
      ~__FILE__
      ~title:"Test batch with wrong counters (+1, +2, +4)"
      ~tags:["transaction"; "transfer"; "counters"; "batch"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* counter =
      Operation.get_counter nodes.main.client ~source:Constant.bootstrap2
    in
    let make_transfer ~counter =
      Operation.Manager.(
        make ~source:Constant.bootstrap2 ~counter
        @@ transfer ~dest:Constant.bootstrap3 ())
    in
    let op1 = make_transfer ~counter:(counter + 1) in
    let op2 = make_transfer ~counter:(counter + 2) in
    let op3 = make_transfer ~counter:(counter + 4) in
    let* _ =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.Manager.inject ~force:true [op1; op2; op3] nodes.main.client
    in
    unit

  let register ~protocols =
    test_simple_transfer_applied protocols ;
    test_simple_transfer_low_balance_to_pay_fees protocols ;
    test_simple_transfer_low_balance_to_make_transfer protocols ;
    test_simple_transfer_counter_in_the_past protocols ;
    test_simple_transfer_counter_in_the_future protocols ;
    test_simple_transfer_wrong_signature protocols ;
    test_simple_transfer_not_enough_gas protocols ;
    test_simple_transfer_not_enough_fees_for_gas protocols ;
    test_simple_transfer_low_balance_to_pay_allocation_1 protocols ;
    test_simple_transfer_low_balance_to_pay_allocation_2 protocols ;
    test_simple_transfer_of_the_whole_balance protocols ;
    test_simple_transfers_successive_wrong_counters protocols ;
    test_batch_simple_transfers_wrong_counters protocols ;
    test_batch_simple_transfers_wrong_counters_2 protocols
end

module Simple_contract_calls = struct
  let sucessful_smart_contract_call =
    Protocol.register_test
      ~__FILE__
      ~title:"Successful smart contract call"
      ~tags:["simple_contract_calls"; "smart"; "contract"; "call"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract =
      Helpers.originate_contract
        protocol
        nodes
        ["mini_scenarios"; "parsable_contract"]
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["applied"]
      @@ fun () ->
      Operation.inject_contract_call
        ~protocol
        ~entrypoint:"default"
        ~arg:(`Michelson "76")
        ~dest:contract
        ~source:Constant.bootstrap1
        nodes.main.client
    in
    unit

  let call_with_illtyped_argument =
    Protocol.register_test
      ~__FILE__
      ~title:"Smart contract call with illtyped argument"
      ~tags:["simple_contract_calls"; "smart"; "contract"; "call"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract =
      Helpers.originate_contract
        protocol
        nodes
        ["mini_scenarios"; "parsable_contract"]
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["bad_contract_parameter"; "invalid_constant"]]
      @@ fun () ->
      Operation.inject_contract_call
        ~protocol
        ~entrypoint:"default"
        ~arg:(`Michelson "Unit")
        ~dest:contract
        ~source:Constant.bootstrap1
        nodes.main.client
    in
    unit

  let test_contract_call_with_failwith =
    Protocol.register_test
      ~__FILE__
      ~title:"Smart contract call that throws a failwith"
      ~tags:["simple_contract_calls"; "smart"; "contract"; "call"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract =
      Helpers.originate_contract
        protocol
        nodes
        ["mini_scenarios"; "parsable_contract"]
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["runtime_error"; "script_rejected"]]
      @@ fun () ->
      Operation.inject_contract_call
        ~protocol
        ~entrypoint:"default"
        ~arg:(`Michelson "-33")
        ~dest:contract
        ~source:Constant.bootstrap1
        nodes.main.client
    in
    unit

  let test_contract_call_with_loop_gas_exhaution =
    Protocol.register_test
      ~__FILE__
      ~title:
        "Smart contract call that loops/fails with 'not enough gas' at exec"
      ~tags:["simple_contract_calls"; "smart"; "contract"; "call"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let* contract =
      Helpers.originate_contract
        protocol
        nodes
        ["mini_scenarios"; "parsable_contract"]
    in
    let* _ =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["gas_exhausted.operation"]]
      @@ fun () ->
      Operation.inject_contract_call
        ~protocol
        ~entrypoint:"default"
        ~arg:(`Michelson "0")
        ~dest:contract
        ~source:Constant.bootstrap1
        nodes.main.client
    in
    unit

  let register ~protocols =
    sucessful_smart_contract_call protocols ;
    call_with_illtyped_argument protocols ;
    test_contract_call_with_failwith protocols ;
    test_contract_call_with_loop_gas_exhaution protocols
end

module Tx_rollup = struct
  open Deserialisation (* for the constants *)

  (* In this test, we ensure that we can build a [transfer ticket] operation
     which passes the precheck. Note that we do not build a transfer ticket
     operation which succeeds because this requires a lot more work to build a
     context where it is valid. This tests serves as a "canary" to ensure that
     the next one still remain meaningful even after changes in the protocol. *)
  let transfer_ticket_deserialization_canary =
    Protocol.register_test
      ~__FILE__
      ~title:"Deserialization of transfer ticket"
        (* TX rollups activated with (Proto 13) Jakarta  *)
      ~supports:(Protocol.Between_protocols (13, 15))
      ~tags:
        [
          "precheck";
          "deserialization";
          "gas";
          "transfer_ticket";
          "tx_rollup";
          "canary";
        ]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let size_kB = 20 in
    let min_deserialization_gas =
      (* contents *) deserialization_gas ~size_kB + (* ty *) 1
    in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["cannot_transfer_ticket_to_implicit"]]
      (* Does not fail in precheck, so is applied (as failed) *)
      @@ fun () ->
      Operation.inject_transfer_ticket
        ~protocol
        ~source:Constant.bootstrap1
        ~gas_limit:(min_deserialization_gas + 1000)
          (* we add 1000 (the gas for manager operation) to avoid failing with
             gas_exhausted right after precheck *)
        ~contents:(`Json (`O [("bytes", `String (make_zero_hex ~size_kB))]))
        ~ty:(`Json (`O [("prim", `String "bytes")]))
        ~ticketer:Constant.bootstrap2.public_key_hash
        ~amount:1
        ~destination:Constant.bootstrap1.public_key_hash
        ~entrypoint:"default"
        nodes.main.client
    in
    unit

  let transfer_ticket_no_overdraft =
    Protocol.register_test
      ~__FILE__
      ~title:"Deserialization of transfer ticket to implicit account"
        (* Ticket transfer to implicit accounts was introduced in (Proto 16) M *)
      ~supports:(Protocol.From_protocol 16)
      ~tags:
        [
          "precheck";
          "deserialization";
          "gas";
          "transfer_ticket";
          "tx_rollup";
          "canary";
        ]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let size_kB = 20 in
    let min_deserialization_gas =
      (* contents *) deserialization_gas ~size_kB + (* ty *) 1
    in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["gas_exhausted.operation"]]
      (* Does not fail in precheck, so is applied (as failed) *)
      @@ fun () ->
      Operation.inject_transfer_ticket
        ~protocol
        ~source:Constant.bootstrap1
        ~gas_limit:(min_deserialization_gas + 1000)
          (* we add 1000 (the gas for manager operation) to avoid failing with
             gas_exhausted right after precheck *)
        ~contents:(`Json (`O [("bytes", `String (make_zero_hex ~size_kB))]))
        ~ty:(`Json (`O [("prim", `String "bytes")]))
        ~ticketer:Constant.bootstrap2.public_key_hash
        ~amount:1
        ~destination:Constant.bootstrap1.public_key_hash
        ~entrypoint:"default"
        nodes.main.client
    in
    let* _oph =
      Memchecks.with_applied_checks
        ~__LOC__
        nodes
        ~expected_statuses:["failed"]
        ~expected_errors:[["Negative_ticket_balance"]]
      (* Does not fail in precheck, is applied with expected failure *)
      @@ fun () ->
      Operation.inject_transfer_ticket
        ~protocol
        ~source:Constant.bootstrap1
        ~gas_limit:(min_deserialization_gas + 10000)
          (* we add 10000 (the gas for manager operation) to make sure it goes all the way to trial application *)
        ~contents:(`Json (`O [("bytes", `String (make_zero_hex ~size_kB))]))
        ~ty:(`Json (`O [("prim", `String "bytes")]))
        ~ticketer:Constant.bootstrap2.public_key_hash
        ~amount:1
        ~destination:Constant.bootstrap1.public_key_hash
        ~entrypoint:"default"
        nodes.main.client
    in
    unit

  (* This test makes sure that the deserialization is performed in the
     precheck. The operation should be refused because there isn't enough gas to
     deserialize the micheline parameters. *)
  let transfer_ticket_deserialization_too_large =
    Protocol.register_test
      ~__FILE__
      ~title:"Deserialization of transfer ticket too large"
        (* TX rollups activated with (Proto 13) Jakarta  *)
      ~supports:(Protocol.From_protocol 13)
      ~tags:
        ["precheck"; "deserialization"; "gas"; "transfer_ticket"; "tx_rollup"]
    @@ fun protocol ->
    let* nodes = Helpers.init ~protocol () in
    let size_kB = 20 in
    let min_deserialization_gas =
      (* contents *) deserialization_gas ~size_kB + (* ty *) 1
    in
    let* _oph =
      Memchecks.with_refused_checks ~__LOC__ nodes @@ fun () ->
      Operation.inject_transfer_ticket
        ~protocol
        ~source:Constant.bootstrap1
        ~gas_limit:(min_deserialization_gas - 1)
        ~contents:(`Json (`O [("bytes", `String (make_zero_hex ~size_kB))]))
        ~ty:(`Json (`O [("prim", `String "bytes")]))
        ~ticketer:Constant.bootstrap2.public_key_hash
        ~amount:1
        ~destination:Constant.bootstrap1.public_key_hash
        ~entrypoint:"default"
        nodes.main.client
    in
    unit

  let register ~protocols =
    transfer_ticket_deserialization_canary protocols ;
    transfer_ticket_deserialization_too_large protocols ;
    transfer_ticket_no_overdraft protocols
end

let register ~protocols =
  Illtyped_originations.register ~protocols ;
  Deserialisation.register ~protocols ;
  Gas_limits.register ~protocols ;
  Reveal.register ~protocols ;
  Simple_transfers.register ~protocols ;
  Simple_contract_calls.register ~protocols ;
  Tx_rollup.register ~protocols
back to top