(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com>     *)
(* Copyright (c) 2018 Nomadic Labs. <nomadic@tezcore.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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

type block_error =
  | Cannot_parse_operation of Operation_hash.t
  | Invalid_fitness of { expected: Fitness.t ; found: Fitness.t }
  | Non_increasing_timestamp
  | Non_increasing_fitness
  | Invalid_level of { expected: Int32.t ; found: Int32.t }
  | Invalid_proto_level of { expected: int ; found: int }
  | Replayed_operation of Operation_hash.t
  | Outdated_operation of
      { operation: Operation_hash.t;
        originating_block: Block_hash.t }
  | Expired_chain of
      { chain_id: Chain_id.t ;
        expiration: Time.t ;
        timestamp: Time.t ;
      }
  | Unexpected_number_of_validation_passes of int (* uint8 *)
  | Too_many_operations of { pass: int; found: int; max: int }
  | Oversized_operation of { operation: Operation_hash.t;
                             size: int; max: int }
  | Unallowed_pass of { operation: Operation_hash.t ;
                        pass: int ;
                        allowed_pass: int list }
  | Cannot_parse_block_header

let errno : Unix.error Data_encoding.t =
  let open Data_encoding in
  union [
    case
      ~title:"unknown_unix_error"
      (Tag 0) int8
      (function Unix.EUNKNOWNERR i -> Some i | _ -> None)
      (fun i -> EUNKNOWNERR i) ;
    case
      ~title:"unix_error"
      (Tag 1)
      (string_enum
         Unix.[
           "2big", E2BIG ;
           "acces", EACCES ;
           "again", EAGAIN ;
           "badf", EBADF ;
           "busy", EBUSY ;
           "child", ECHILD ;
           "deadlk", EDEADLK ;
           "dom", EDOM ;
           "exist", EEXIST ;
           "fault", EFAULT ;
           "fbig", EFBIG ;
           "intr", EINTR ;
           "inval", EINVAL ;
           "io", EIO ;
           "isdir", EISDIR ;
           "mfile", EMFILE ;
           "mlink", EMLINK ;
           "nametoolong", ENAMETOOLONG ;
           "nfile", ENFILE ;
           "nodev", ENODEV ;
           "noent", ENOENT ;
           "noexec", ENOEXEC ;
           "nolck", ENOLCK ;
           "nomem", ENOMEM ;
           "nospc", ENOSPC ;
           "nosys", ENOSYS ;
           "notdir", ENOTDIR ;
           "notempty", ENOTEMPTY ;
           "notty", ENOTTY ;
           "nxio", ENXIO ;
           "perm", EPERM ;
           "pipe", EPIPE ;
           "range", ERANGE ;
           "rofs", EROFS ;
           "spipe", ESPIPE ;
           "srch", ESRCH ;
           "xdev", EXDEV ;
           "wouldblock", EWOULDBLOCK ;
           "inprogress", EINPROGRESS ;
           "already", EALREADY ;
           "notsock", ENOTSOCK ;
           "destaddrreq", EDESTADDRREQ ;
           "msgsize", EMSGSIZE ;
           "prototype", EPROTOTYPE ;
           "noprotoopt", ENOPROTOOPT ;
           "protonosupport", EPROTONOSUPPORT ;
           "socktnosupport", ESOCKTNOSUPPORT ;
           "opnotsupp", EOPNOTSUPP ;
           "pfnosupport", EPFNOSUPPORT ;
           "afnosupport", EAFNOSUPPORT ;
           "addrinuse", EADDRINUSE ;
           "addrnotavail", EADDRNOTAVAIL ;
           "netdown", ENETDOWN ;
           "netunreach", ENETUNREACH ;
           "netreset", ENETRESET ;
           "connaborted", ECONNABORTED ;
           "connreset", ECONNRESET ;
           "nobufs", ENOBUFS ;
           "isconn", EISCONN ;
           "notconn", ENOTCONN ;
           "shutdown", ESHUTDOWN ;
           "toomanyrefs", ETOOMANYREFS ;
           "timedout", ETIMEDOUT ;
           "connrefused", ECONNREFUSED ;
           "hostdown", EHOSTDOWN ;
           "hostunreach", EHOSTUNREACH ;
           "loop", ELOOP ;
           "overflow", EOVERFLOW ])
      (fun x -> Some x)
      (fun x -> x)
  ]

let block_error_encoding =
  let open Data_encoding in
  union
    [
      case (Tag 0)
        ~title:"Cannot_parse_operation"
        (obj2
           (req "error" (constant "cannot_parse_operation"))
           (req "operation" Operation_hash.encoding))
        (function Cannot_parse_operation operation -> Some ((), operation)
                | _ -> None)
        (fun ((), operation) -> Cannot_parse_operation operation) ;
      case (Tag 1)
        ~title:"Invalid_fitness"
        (obj3
           (req "error" (constant "invalid_fitness"))
           (req "expected" Fitness.encoding)
           (req "found" Fitness.encoding))
        (function
          | Invalid_fitness { expected ; found } ->
              Some ((), expected, found)
          | _ -> None)
        (fun ((), expected, found) -> Invalid_fitness { expected ; found }) ;
      case (Tag 2)
        ~title:"Non_increasing_timestamp"
        (obj1
           (req "error" (constant "non_increasing_timestamp")))
        (function Non_increasing_timestamp -> Some ()
                | _ -> None)
        (fun () -> Non_increasing_timestamp) ;
      case (Tag 3)
        ~title:"Non_increasing_fitness"
        (obj1
           (req "error" (constant "non_increasing_fitness")))
        (function Non_increasing_fitness -> Some ()
                | _ -> None)
        (fun () -> Non_increasing_fitness) ;
      case (Tag 4)
        ~title:"Invalid_level"
        (obj3
           (req "error" (constant "invalid_level"))
           (req "expected" int32)
           (req "found" int32))
        (function
          | Invalid_level { expected ; found } ->
              Some ((), expected, found)
          | _ -> None)
        (fun ((), expected, found) -> Invalid_level { expected ; found }) ;
      case (Tag 5)
        ~title:"Invalid_proto_level"
        (obj3
           (req "error" (constant "invalid_proto_level"))
           (req "expected" uint8)
           (req "found" uint8))
        (function
          | Invalid_proto_level { expected ; found } ->
              Some ((), expected, found)
          | _ -> None)
        (fun ((), expected, found) ->
           Invalid_proto_level { expected ; found }) ;
      case (Tag 6)
        ~title:"Replayed_operation"
        (obj2
           (req "error" (constant "replayed_operation"))
           (req "operation" Operation_hash.encoding))
        (function Replayed_operation operation -> Some ((), operation)
                | _ -> None)
        (fun ((), operation) -> Replayed_operation operation) ;
      case (Tag 7)
        ~title:"Outdated_operation"
        (obj3
           (req "error" (constant "outdated_operation"))
           (req "operation" Operation_hash.encoding)
           (req "originating_block" Block_hash.encoding))
        (function
          | Outdated_operation { operation ; originating_block } ->
              Some ((), operation, originating_block)
          | _ -> None)
        (fun ((), operation, originating_block) ->
           Outdated_operation { operation ; originating_block }) ;
      case (Tag 8)
        ~title:"Unexpected_number_of_validation_passes"
        (obj2
           (req "error" (constant "unexpected_number_of_passes"))
           (req "found" uint8))
        (function
          | Unexpected_number_of_validation_passes n -> Some ((), n)
          | _ -> None)
        (fun ((), n) -> Unexpected_number_of_validation_passes n) ;
      case (Tag 9)
        ~title:"Too_many_operations"
        (obj4
           (req "error" (constant "too_many_operations"))
           (req "validation_pass" uint8)
           (req "found" uint16)
           (req "max" uint16))
        (function
          | Too_many_operations { pass ; found ; max } ->
              Some ((), pass, found, max)
          | _ -> None)
        (fun ((), pass, found, max) ->
           Too_many_operations { pass ; found ; max }) ;
      case (Tag 10)
        ~title:"Oversized_operation"
        (obj4
           (req "error" (constant "oversized_operation"))
           (req "operation" Operation_hash.encoding)
           (req "found" int31)
           (req "max" int31))
        (function
          | Oversized_operation { operation ; size ; max } ->
              Some ((), operation, size, max)
          | _ -> None)
        (fun ((), operation, size, max) ->
           Oversized_operation { operation ; size ; max }) ;
      case (Tag 11)
        ~title:"Unallowed_pass"
        (obj4
           (req "error" (constant "invalid_pass"))
           (req "operation" Operation_hash.encoding)
           (req "pass" uint8)
           (req "allowed_pass" (list uint8)))
        (function
          | Unallowed_pass { operation ; pass ; allowed_pass } ->
              Some ((), operation, pass, allowed_pass)
          | _ -> None)
        (fun ((), operation, pass, allowed_pass) ->
           Unallowed_pass { operation ; pass ; allowed_pass }) ;
    ]

let pp_block_error ppf = function
  | Cannot_parse_operation oph ->
      Format.fprintf ppf
        "Failed to parse the operation %a."
        Operation_hash.pp_short oph
  | Invalid_fitness { expected ; found } ->
      Format.fprintf ppf
        "@[<v 2>Invalid fitness:@ \
        \ expected %a@ \
        \ found %a@]"
        Fitness.pp expected
        Fitness.pp found
  | Non_increasing_timestamp ->
      Format.fprintf ppf "Non increasing timestamp"
  | Non_increasing_fitness ->
      Format.fprintf ppf "Non increasing fitness"
  | Invalid_level { expected ; found } ->
      Format.fprintf ppf
        "Invalid level:@ \
        \ expected %ld@ \
        \ found %ld"
        expected
        found
  | Invalid_proto_level { expected ; found } ->
      Format.fprintf ppf
        "Invalid protocol level:@ \
        \ expected %d@ \
        \ found %d"
        expected
        found
  | Replayed_operation oph ->
      Format.fprintf ppf
        "The operation %a was previously included in the chain."
        Operation_hash.pp_short oph
  | Outdated_operation { operation ; originating_block } ->
      Format.fprintf ppf
        "The operation %a is outdated (originated in block: %a)"
        Operation_hash.pp_short operation
        Block_hash.pp_short originating_block
  | Expired_chain { chain_id ; expiration ; timestamp } ->
      Format.fprintf ppf
        "The block timestamp (%a) is later than \
         its chain expiration date: %a (chain: %a)."
        Time.pp_hum timestamp
        Time.pp_hum expiration
        Chain_id.pp_short chain_id
  | Unexpected_number_of_validation_passes n ->
      Format.fprintf ppf
        "Invalid number of validation passes (found: %d)"
        n
  | Too_many_operations { pass ; found ; max } ->
      Format.fprintf ppf
        "Too many operations in validation pass %d (found: %d, max: %d)"
        pass found max
  | Oversized_operation { operation ; size ; max } ->
      Format.fprintf ppf
        "Oversized operation %a (size: %d, max: %d)"
        Operation_hash.pp_short operation size max
  | Unallowed_pass { operation ; pass ; allowed_pass } ->
      Format.fprintf ppf
        "Operation %a included in validation pass %d, \
        \ while only the following passes are allowed: @[<h>%a@]"
        Operation_hash.pp_short operation pass
        Format.(pp_print_list pp_print_int) allowed_pass
  | Cannot_parse_block_header ->
      Format.fprintf ppf "Failed to parse the block header."

type error +=
  | Invalid_block of
      { block: Block_hash.t ; error: block_error }
  | Unavailable_protocol of
      { block: Block_hash.t ; protocol: Protocol_hash.t }
  | Inconsistent_operations_hash of
      { block: Block_hash.t ;
        expected: Operation_list_list_hash.t ;
        found: Operation_list_list_hash.t }
  | Failed_to_checkout_context of Context_hash.t
  | System_error of { errno: Unix.error ;
                      fn: string ;
                      msg: string }

let () =
  Error_monad.register_error_kind
    `Permanent
    ~id:"validator.invalid_block"
    ~title:"Invalid block"
    ~description:"Invalid block."
    ~pp:begin fun ppf (block, error) ->
      Format.fprintf ppf
        "@[<v 2>Invalid block %a@ %a@]"
        Block_hash.pp_short block pp_block_error error
    end
    Data_encoding.(merge_objs
                     (obj1 (req "invalid_block" Block_hash.encoding))
                     block_error_encoding)
    (function Invalid_block { block ; error } ->
       Some (block, error) | _ -> None)
    (fun (block, error) ->
       Invalid_block { block ; error }) ;
  Error_monad.register_error_kind
    `Temporary
    ~id:"validator.unavailable_protocol"
    ~title:"Missing protocol"
    ~description:"The protocol required for validating a block is missing."
    ~pp:begin fun ppf (block, protocol) ->
      Format.fprintf ppf
        "Missing protocol (%a) when validating the block %a."
        Protocol_hash.pp_short protocol
        Block_hash.pp_short block
    end
    Data_encoding.(
      obj2
        (req "block" Block_hash.encoding)
        (req "missing_protocol" Protocol_hash.encoding))
    (function
      | Unavailable_protocol { block ; protocol } ->
          Some (block, protocol)
      | _ -> None)
    (fun (block, protocol) -> Unavailable_protocol { block ; protocol }) ;
  Error_monad.register_error_kind
    `Temporary
    ~id:"validator.inconsistent_operations_hash"
    ~title:"Invalid merkle tree"
    ~description:"The provided list of operations is inconsistent with \
                  the block header."
    ~pp:begin fun ppf (block, expected, found) ->
      Format.fprintf ppf
        "@[<v 2>The provided list of operations for block %a \
        \ is inconsistent with the block header@ \
        \ expected: %a@ \
        \ found: %a@]"
        Block_hash.pp_short block
        Operation_list_list_hash.pp_short expected
        Operation_list_list_hash.pp_short found
    end
    Data_encoding.(
      obj3
        (req "block" Block_hash.encoding)
        (req "expected" Operation_list_list_hash.encoding)
        (req "found" Operation_list_list_hash.encoding))
    (function
      | Inconsistent_operations_hash { block ; expected ; found } ->
          Some (block, expected, found)
      | _ -> None)
    (fun (block, expected, found) ->
       Inconsistent_operations_hash { block ; expected ; found });
  Error_monad.register_error_kind
    `Permanent
    ~id:"Validator_process.failed_to_checkout_context"
    ~title: "Fail during checkout context"
    ~description: "The context checkout failed using a given hash"
    ~pp:(fun ppf (hash:Context_hash.t) ->
        Format.fprintf ppf
          "@[Failed to checkout the context with hash %a@]"
          Context_hash.pp_short hash)
    Data_encoding.(obj1 (req "hash" Context_hash.encoding))
    (function
      | Failed_to_checkout_context h -> Some h
      | _ -> None)
    (fun h -> Failed_to_checkout_context h) ;
  Error_monad.register_error_kind
    `Temporary
    ~id:"Validator_process.system_error_while_validating"
    ~title: "Failed to validate block because of a system error"
    ~description: "The validator failed because of a system error"
    ~pp:(fun ppf (errno, fn, msg) ->
        Format.fprintf ppf
          "System error while validating a block (in function %s(%s)):@ %s"
          fn msg (Unix.error_message errno))
    Data_encoding.(obj3
                     (req "errno" errno)
                     (req "function" string)
                     (req "msg" string))
    (function
      | System_error { errno ; fn ; msg } -> Some (errno, fn, msg)
      | _ -> None)
    (fun (errno, fn, msg) -> System_error { errno ; fn ; msg })

let invalid_block block error = Invalid_block { block ; error }
