swh:1:snp:505c374fd75bb208ae4e9a54e64bb310bc49295e
Raw File
Tip revision: b8772902f7efed80f8f7884d6706ba33bd0018f2 authored by Mehdi Bouaziz on 16 January 2024, 13:59:13 UTC
Proto: Changes to commentary for allow_forged_tickets
Tip revision: b877290
test_p2p_socket.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com>     *)
(* Copyright (c) 2019-2020 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:    P2P
    Invocation:   dune exec src/lib_p2p/test/test_p2p_socket.exe
    Dependencies: src/lib_p2p/test/process.ml
    Subject:      Sockets and client-server communications.
*)

open P2p_test_utils

let tzassert b pos =
  let open Lwt_result_syntax in
  let p (file, lnum, cnum, _) = (file, lnum, cnum) in
  if b then return_unit else fail_with_exn (Assert_failure (p pos))

let high_pow_target = Tezos_crypto.Crypto_box.make_pow_target 100.

let sync ch =
  let open Lwt_result_syntax in
  let* () = Process.Channel.push ch () in
  let* () = Process.Channel.pop ch in
  return_unit

(** [connect ?target_id ?proof_of_work_target sched addr port] connect
    and performs [P2p_socket.authenticate] with the given
    [proof_of_work_target] (also checking that the remote point is the
    expected [target_id]). *)
let connect ?proof_of_work_target ?(target_id = id1) sched addr port id =
  let open Lwt_result_syntax in
  let* info, auth_fd =
    P2p_test_utils.connect ?proof_of_work_target sched addr port id
  in
  let*! id1 = target_id in
  let* () = tzassert (not info.incoming) __POS__ in
  let* () =
    tzassert (P2p_peer.Id.compare info.peer_id id1.peer_id = 0) __POS__
  in
  return auth_fd

let is_connection_closed = function
  | Error (Tezos_p2p_services.P2p_errors.Connection_closed :: _) -> true
  | Ok _ -> false
  | Error err ->
      Tezt.Log.info "Error: %a" pp_print_trace err ;
      false

let is_decoding_error = function
  | Error (Tezos_p2p_services.P2p_errors.Decoding_error _ :: _) -> true
  | Ok _ -> false
  | Error err ->
      Tezt.Log.info "Error: %a" pp_print_trace err ;
      false

(** Writing then reading through the same pipe a chunk of message [msg]
    with encryption/decryption.
*)
module Crypto_test = struct
  (* maximal size of the buffer *)
  let bufsize = (1 lsl 16) - 1

  let header_length = 2

  (* The size of extra data added by encryption. *)
  let tag_length = Tezos_crypto.Crypto_box.tag_length

  (* The number of bytes added by encryption + header *)
  let extrabytes = header_length + tag_length

  let max_content_length = bufsize - extrabytes

  type data = {
    channel_key : Tezos_crypto.Crypto_box.channel_key;
    mutable local_nonce : Tezos_crypto.Crypto_box.nonce;
    mutable remote_nonce : Tezos_crypto.Crypto_box.nonce;
  }

  let () = assert (tag_length >= header_length)

  let write_chunk fd cryptobox_data msg =
    let open Lwt_result_syntax in
    let msg_length = Bytes.length msg in
    let* () =
      fail_unless
        (msg_length <= max_content_length)
        Tezos_p2p_services.P2p_errors.Invalid_message_size
    in
    let encrypted_length = tag_length + msg_length in
    let payload_length = header_length + encrypted_length in
    let tag = Bytes.make tag_length '\x00' in
    let cmsg = Bytes.copy msg in
    let local_nonce = cryptobox_data.local_nonce in
    cryptobox_data.local_nonce <-
      Tezos_crypto.Crypto_box.increment_nonce local_nonce ;
    Tezos_crypto.Crypto_box.fast_box_noalloc
      cryptobox_data.channel_key
      local_nonce
      tag
      cmsg ;
    let payload = Bytes.make payload_length '\x00' in
    TzEndian.set_int16 payload 0 encrypted_length ;
    Bytes.blit tag 0 payload header_length tag_length ;
    Bytes.blit cmsg 0 payload extrabytes msg_length ;
    let*! i = Lwt_unix.write fd payload 0 payload_length in
    tzassert (payload_length = i) __POS__

  let read_chunk fd cryptobox_data =
    let open Lwt_result_syntax in
    let header_buf = Bytes.create header_length in
    let*! i = Lwt_unix.read fd header_buf 0 header_length in
    let* () = tzassert (header_length = i) __POS__ in
    let encrypted_length = TzEndian.get_uint16 header_buf 0 in
    assert (encrypted_length >= tag_length) ;
    let msg_length = encrypted_length - tag_length in
    let tag = Bytes.make tag_length '\x00' in
    let*! i = Lwt_unix.read fd tag 0 tag_length in
    let* () = tzassert (tag_length = i) __POS__ in
    let msg = Bytes.make msg_length '\x00' in
    let*! i =
      if msg_length > 0 then Lwt_unix.read fd msg 0 msg_length else Lwt.return 0
    in
    let* () = tzassert (msg_length = i) __POS__ in
    let remote_nonce = cryptobox_data.remote_nonce in
    cryptobox_data.remote_nonce <-
      Tezos_crypto.Crypto_box.increment_nonce remote_nonce ;
    let*? () =
      error_unless
        (Tezos_crypto.Crypto_box.fast_box_open_noalloc
           cryptobox_data.channel_key
           remote_nonce
           tag
           msg)
        Tezos_p2p_services.P2p_errors.Decipher_error
    in
    return msg

  let sk, pk, _pkh = Tezos_crypto.Crypto_box.random_keypair ()

  let zero_nonce = Tezos_crypto.Crypto_box.zero_nonce

  let channel_key = Tezos_crypto.Crypto_box.precompute sk pk

  let data = {channel_key; local_nonce = zero_nonce; remote_nonce = zero_nonce}

  let wrap () =
    Alcotest_lwt.test_case "ACK" `Quick (fun _ () ->
        let open Lwt_syntax in
        let msg = Bytes.of_string "test" in
        let in_fd, out_fd = Lwt_unix.pipe ~cloexec:true () in
        let* _ = write_chunk out_fd data msg in
        let* r = read_chunk in_fd data in
        let* () = Lwt_unix.close in_fd in
        let* () = Lwt_unix.close out_fd in
        match r with
        | Ok res when Bytes.equal msg res -> Lwt.return_unit
        | Ok res ->
            Format.kasprintf
              Stdlib.failwith
              "Error : %s <> %s"
              (Bytes.to_string res)
              (Bytes.to_string msg)
        | Error error ->
            Format.kasprintf Stdlib.failwith "%a" pp_print_trace error)
end

(** Spawns a client and a server. The client connects to the server
    using identity [id0] (pow_target=0). The
    server check it against a unreachable pow target.
*)
module Pow_check = struct
  let is_failing = function
    | Error (P2p_errors.Not_enough_proof_of_work _ :: _) -> true
    | _ -> false

  let server _ch sched socket =
    let open Lwt_syntax in
    let* res = accept ~proof_of_work_target:high_pow_target sched socket in
    tzassert (is_failing res) __POS__

  let client _ch sched addr port =
    let open Lwt_syntax in
    let* id = id2 in
    let* conn = connect sched addr port id in
    tzassert (is_connection_closed conn) __POS__

  let run addr _dir = run_nodes ~addr client server
end

(** Spawns a client and a server. After the client getting connected to
    the server, it reads a message [simple_msg] sent by the server and
    stores in [msg] of fixed same size. It asserts that both messages
    and identical.
*)
module Low_level = struct
  let simple_msg = Tezos_crypto.Rand.generate (1 lsl 4)

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let msg = Bytes.create (Bytes.length simple_msg) in
    let*! r = raw_connect sched addr port in
    match r with
    | Error (`Connection_failed | `Unexpected_error _) ->
        Lwt.fail Alcotest.Test_error
    | Ok fd ->
        let* () =
          P2p_buffer_reader.(
            read_full (P2p_io_scheduler.to_readable fd) @@ mk_buffer_safe msg)
        in
        let* () = tzassert (Bytes.compare simple_msg msg = 0) __POS__ in
        let* () = sync ch in
        P2p_io_scheduler.close ~reason:(User "client shutdown") fd

  let server ch sched socket =
    let open Lwt_result_syntax in
    let*! r = raw_accept sched socket in
    match r with
    | Error (`Socket_error ex | `System_error ex | `Unexpected_error ex) ->
        Lwt.fail ex
    | Ok (fd, _point) ->
        let* () = P2p_io_scheduler.write fd simple_msg in
        let* () = sync ch in
        let* () = P2p_io_scheduler.close ~reason:(User "server shutdown") fd in
        return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** Spawns a client and a server. A client trying to connect to a
    server receives and Ack message but replies with a Nack. The
    connection is hence rejected by the client.
*)
module Nacked = struct
  let is_rejected = function
    | Error (Tezos_p2p_services.P2p_errors.Rejected_by_nack _ :: _) -> true
    | Ok _ -> false
    | Error err ->
        Tezt.Log.info "Error: %a" pp_print_trace err ;
        false

  let encoding = Data_encoding.bytes

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let*! conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = tzassert (is_rejected conn) __POS__ in
    sync ch

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let*! () = P2p_socket.nack auth_fd P2p_rejection.No_motive [] in
    sync ch

  let run addr _dir = run_nodes ~addr client server
end

(** Spawns a client and a server. A client tries to connect to a
    server. Both parties acknowledge. The server sends [simple_msg],
    while the client sends [simple_msg2]. Both messages are checked for
    consistency. Then, the connection is closed.
*)
module Simple_message = struct
  let encoding = Data_encoding.bytes

  let simple_msg = Tezos_crypto.Rand.generate (1 lsl 4)

  let simple_msg2 = Tezos_crypto.Rand.generate (1 lsl 4)

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = P2p_socket.write_sync conn simple_msg in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg2 msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    return_unit

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = P2p_socket.write_sync conn simple_msg2 in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** Spawns a client and a server. A client tries to connect to a
    server. Both parties acknowledge. The server sends [simple_msg] and
    the client sends [simple_msg2] with a binary chunk size of 21. Both
    messages are checked for consistency.
*)
module Chunked_message = struct
  let encoding = Data_encoding.bytes

  let simple_msg = Tezos_crypto.Rand.generate (1 lsl 8)

  let simple_msg2 = Tezos_crypto.Rand.generate (1 lsl 8)

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn =
      P2p_socket.accept ~canceler ~binary_chunks_size:21 auth_fd encoding
    in
    let* () = P2p_socket.write_sync conn simple_msg in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg2 msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    return_unit

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn =
      P2p_socket.accept ~canceler ~binary_chunks_size:21 auth_fd encoding
    in
    let* () = P2p_socket.write_sync conn simple_msg2 in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** Two messages of size 131072 bytes are randomly generated. After
    successful connection, both parties send the latter messages.
*)
module Oversized_message = struct
  let encoding = Data_encoding.bytes

  let rec rand_gen () =
    try Tezos_crypto.Rand.generate (1 lsl 17)
    with _ ->
      Tezt.Log.error "Not enough entropy, retrying to generate random data" ;
      rand_gen ()

  let simple_msg = rand_gen ()

  let simple_msg2 = rand_gen ()

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = P2p_socket.write_sync conn simple_msg in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg2 msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    return_unit

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = P2p_socket.write_sync conn simple_msg2 in
    let* _msg_size, msg = P2p_socket.read conn in
    let* () = tzassert (Bytes.compare simple_msg msg = 0) __POS__ in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** After then successful connection of a client to a server, the client
    attempts to read a message. However, the server decides to close
    the connection.
*)
module Close_on_read = struct
  let encoding = Data_encoding.bytes

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = sync ch in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    return_unit

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = sync ch in
    let*! err = P2p_socket.read conn in
    let* () = tzassert (is_connection_closed err) __POS__ in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** After the successful connection of a client to a server, the client
    attempts to send a message. However, the server decides to close
    the connection.
*)
module Close_on_write = struct
  let encoding = Data_encoding.bytes

  let simple_msg = Tezos_crypto.Rand.generate (1 lsl 4)

  let server ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    let* () = sync ch in
    return_unit

  let client ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = sync ch in
    let*! () = Lwt_unix.sleep 0.1 in
    let*! err = P2p_socket.write_sync conn simple_msg in
    let* () = tzassert (is_connection_closed err) __POS__ in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

(** A dummy message is generated into [garbled_msg]. After the
    successful connection of a client to a server, the server sends
    [garbled_msg] and waits by reading a close connection is declared
    by the client.  On the side of the client, it is asserted that the
    message cannot be decoded.
*)
module Garbled_data = struct
  let encoding =
    let open Data_encoding in
    dynamic_size @@ option @@ string

  (* generate a fixed garbled_msg to avoid 'Data_encoding.Binary.Await
     _', which blocks 'make test' *)
  let garbled_msg =
    let buf = Bytes.create (1 lsl 4) in
    TzEndian.set_int32 buf 0 (Int32.of_int 4) ;
    TzEndian.set_int32 buf 4 (Int32.of_int (-1)) ;
    TzEndian.set_int32 buf 8 (Int32.of_int (-1)) ;
    TzEndian.set_int32 buf 12 (Int32.of_int (-1)) ;
    buf

  let server _ch sched socket =
    let open Lwt_result_syntax in
    let* _info, auth_fd = accept sched socket in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let* () = P2p_socket.Internal_for_tests.raw_write_sync conn garbled_msg in
    let*! err = P2p_socket.read conn in
    let* () = tzassert (is_connection_closed err) __POS__ in
    let*! _stat = P2p_socket.close ~reason:(User "server shutdown") conn in
    return_unit

  let client _ch sched addr port =
    let open Lwt_result_syntax in
    let*! id2 in
    let* auth_fd = connect sched addr port id2 in
    let* conn = P2p_socket.accept ~canceler auth_fd encoding in
    let*! err = P2p_socket.read conn in
    let* () = tzassert (is_decoding_error err) __POS__ in
    let*! _stat = P2p_socket.close ~reason:(User "client shutdown") conn in
    return_unit

  let run addr _dir = run_nodes ~addr client server
end

let init_logs =
  let log_cfg =
    Tezos_base_unix.Logs_simple_config.create_cfg
      ~rules:"test.p2p.connection -> info; p2p.connection -> info"
      ()
  in
  let config =
    Tezos_base_unix.Internal_event_unix.make_with_defaults ~log_cfg ()
  in
  lazy (Tezos_base_unix.Internal_event_unix.init ~config ())

let wrap n f =
  let addr = Node.default_ipv6_addr in
  Alcotest_lwt.test_case n `Quick (fun _ () ->
      let open Lwt_syntax in
      let* () = Lazy.force init_logs in
      let* r = f addr () in
      match r with
      | Ok () -> Lwt.return_unit
      | Error error ->
          Format.kasprintf Stdlib.failwith "%a" pp_print_trace error)

let main () =
  Lwt_main.run
  @@ Alcotest_lwt.run
       ~__FILE__
       "tezos-p2p"
       [
         ( "p2p-socket.",
           [
             wrap "low-level" Low_level.run;
             wrap "pow" Pow_check.run;
             wrap "nacked" Nacked.run;
             wrap "simple-message" Simple_message.run;
             wrap "chunked-message" Chunked_message.run;
             wrap "oversized-message" Oversized_message.run;
             wrap "close-on-read" Close_on_read.run;
             wrap "close-on-write" Close_on_write.run;
             wrap "garbled-data" Garbled_data.run;
             Crypto_test.wrap ();
           ] );
       ]

let () =
  Sys.catch_break true ;
  try main () with _ -> ()

let () = Tezt.Test.run ()
back to top