Raw File
sc_rollup_game_repr.mli
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2021 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2022 Trili Tech, <contact@trili.tech>                       *)
(*                                                                           *)
(* 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(** The smart contract rollup refutation game types are defined here, as
    well as the basic pure logic for:

    - how to create a new game from a pair of commits in the commit tree;

    - how to update a game or complete a game when a move is played.

    This game logic is used by the protocol when two commitments are in
    conflict to determine which one of the commitments is wrong.

    Game state and moves
    ====================

    The first step consists of dissecting the commitment's number of ticks.
    The game stores a list [dissection] of state hashes and tick counts.
    These are the claims about the PVM history made by the player who has
    just moved.

    The next player to move will specify a tick count which appears in
    the [dissection]; this is the last of the state hashes which she
    agrees with. She will then either:

    - provide a new [dissection] by giving a list of state hashes and
    tick counts that starts at the chosen tick count and ends at the
    next tick count in the previous [dissection]. It must agree at the
    start but disagree with the final state.

    - if the tick difference between this state and the next is one,
    there is no 'room' for a new [dissection]. In this case she must
    provide a Merkle proof that shows the step in the current
    [dissection] is invalid.

    If a player failed to prove that the current [dissection] is valid.
    We reach the final move of the game. The other player will have
    a chance to prove that the [dissection] is valid.
    If both player fails to invalidate each other, the game ends in a draw.

    Initializing a game
    ===================

    In order to trigger the start of a game, one player must publish a
    first move.

    The [initial] function is called at this point. It converts a
    parent-child pair of commitments (belonging to the other player) into
    an initial [dissection]. The first move is immediately applied to
    this to give the first state of the game.

    Note: it is quite possible for the game to end immediately after
    this first move, either if the commitment has a tick count of one or
    more probably if the refutation proves that the commitment was
    'premature' (the state is not blocked---there are further
    computation steps to do or more inbox messages to read).

    Expected properties
    ===================

    P1 - If [dissection] is honest, the next move must be dishonest:

      There is only one honest state hash for a given tick count. The
      next player must provide a different hash to the honest hash in
      the [dissection].

    P2 - If [dissection] is dishonest, there is a strategy for a player
    equipped with a perfect PVM to play an honest next move:

      The player with a perfect PVM can calculate honest hashes until
      one disagrees with the [dissection], and challenges the dissection
      at that point, publishing either an honest [dissection] or an
      honest [Proof].

    Each [dissection] has a maximum tick count step shorter than the
    last, so by induction using P1 and P2 we have

    P1' - If [dissection] is honest, the last player has a winning
    strategy.

    P2' - If [dissection] is dishonest, the next player has a winning
    strategy.

    This allows us to see the following. (We use [refuter] to mean the
    first player to move, and [defender] to mean the other player.)

    Honest refuter wins:
      An honest refuter will be refuting a dishonest commitment, because
      there is only one honest state possible per level. Therefore the
      initial [dissection] will be dishonest. By P2' the refuter has a
      winning strategy.

    Honest defender wins:
      An honest defender will have made an honest commitment which will
      be translated into an honest initial [dissection]. By P1' the
      defender has a winning strategy.

*)

open Sc_rollup_repr

type error +=
  | Dissection_choice_not_found of Sc_rollup_tick_repr.t
        (** The given choice in a refutation is not a starting tick of any of
          the sections in the current dissection. *)
  | Proof_unexpected_section_size of Z.t
        (** Invalid proof step because there is more than one tick. *)
  | Proof_start_state_hash_mismatch of {
      start_state_hash : Sc_rollup_repr.State_hash.t option;
      start_proof : Sc_rollup_repr.State_hash.t;
    }
        (** The given proof's starting state doesn't match the expected one. *)
  | Proof_stop_state_hash_failed_to_refute of {
      stop_state_hash : Sc_rollup_repr.State_hash.t option;
      stop_proof : Sc_rollup_repr.State_hash.t option;
    }
        (** The given proof's ending state should not match the state being
          refuted. *)
  | Proof_stop_state_hash_failed_to_validate of {
      stop_state_hash : Sc_rollup_repr.State_hash.t option;
      stop_proof : Sc_rollup_repr.State_hash.t option;
    }
        (** The given proof's ending state should match the state being
          refuted. *)
  | Dissecting_during_final_move
        (** The step move is a dissecting where the final move has started
            already. *)

(** The two stakers index the game in the storage as a pair of public
    key hashes which is in lexical order. We use [Alice] and [Bob] to
    represent the first and second player in the pair respectively. *)
type player = Alice | Bob

module V1 : sig
  type dissection_chunk = Sc_rollup_dissection_chunk_repr.t

  (** Describes the current state of a game. *)
  type game_state =
    | Dissecting of {
        dissection : dissection_chunk list;
            (** [dissection], a list of states with tick counts. The current
                player will specify, in the next move, a tick count that
                indicates the last of these states that she agrees with. *)
        default_number_of_sections : int;
            (** [default_number_of_sections] is the number of sections a
                disection should contain in the more general case where we still
                have a high enough number of disputed ticks. *)
      }
        (** When the state is [Dissecting], both player are still dissecting
            the commitment to find the tick to refute. *)
    | Final_move of {
        agreed_start_chunk : dissection_chunk;
        refuted_stop_chunk : dissection_chunk;
      }
        (** When the state is [Final_move], either [Alice] or [Bob] already
            played an invalid proof.

            The other player will have a chance to prove that the
            [refuted_stop_state] is valid.
            If both players fail to either validate or refute the stop state,
            the current game state describes a draw situation.
            In the same way, the draw can be described by the situation where
            the two players manage to validate or refute the stop state. *)

  val game_state_encoding : game_state Data_encoding.t

  val game_state_equal : game_state -> game_state -> bool

  (** A game is characterized by:

    - [refuter_commitment_hash], the hash of the commitment of the player that
      has initiated the game.

    - [defender_commitment_hash], the hash of the commitment of the player that
      is tentatively refuted.

    - [turn], the player that must provide the next move.

    - [inbox_snapshot], a snapshot of the inbox state at the moment the
      game is created. This is only used when checking [Input_step] and
      [Blocked_step] proofs; it makes the proofs easier to create---
      otherwise they would have a 'moving target' because the actual
      inbox may be updated continuously.

    - [dal_snapshot], a snapshot of the DAL's confirmed slots history at the
      moment the game is created. In fact, since the confirmed slots history at
      initialization would likely evolve during the game, we need a (fixed)
      reference w.r.t. which Dal input proofs would be produced and verified if
      needed.

    - [level], the inbox level of the commitment the game is refuting.
      This is only used when checking [Blocked_step] proofs---the proof
      will show that the next message available in [inbox_snapshot] is
      at [level], so shouldn't be included in this commitment.

    - [game_state], the current state of the game, see {!type-game_state}
      for more information.

    Invariants:
    -----------
    - [dissection] must contain at least 2 values (normally it will be 32
    values, but smaller if there isn't enough space for a dissection
    that size. The initial game dissection will be 3 values except in
    the case of a zero-tick commit when it will have 2 values.)
    - the first state hash value in [dissection] must not be [None]
    - [inbox_snapshot] and [dal_snapshot] never change once the game is created
  *)
  type t = {
    turn : player;
    inbox_snapshot : Sc_rollup_inbox_repr.history_proof;
    dal_snapshot : Dal_slot_repr.History.t;
    start_level : Raw_level_repr.t;
    inbox_level : Raw_level_repr.t;
    game_state : game_state;
  }

  (** [equal g1 g2] returns [true] iff [g1] is equal to [g2]. *)
  val equal : t -> t -> bool

  (** Return the other player *)
  val opponent : player -> player

  val encoding : t Data_encoding.t

  val pp_dissection : Format.formatter -> dissection_chunk list -> unit

  val player_equal : player -> player -> bool

  val player_encoding : player Data_encoding.t

  val pp : Format.formatter -> t -> unit
end

(** Versioning, see {!Sc_rollup_data_version_sig.S} for more information. *)
include Sc_rollup_data_version_sig.S with type t = V1.t

include
  module type of V1
    with type dissection_chunk = V1.dissection_chunk
     and type game_state = V1.game_state
     and type t = V1.t

module Index : sig
  type t = private {alice : Staker.t; bob : Staker.t}

  (** [to_path i p] returns a new path with the path to the game indexed
      by [i] added as a prefix to path [p]. See [Path_encoding] module. *)
  val to_path : t -> string list -> string list

  val of_path : string list -> t option

  val path_length : int

  val rpc_arg : t RPC_arg.t

  val encoding : t Data_encoding.t

  val compare : t -> t -> int

  val make : Staker.t -> Staker.t -> t

  (** Given an index in normal form, resolve a given [player] ([Alice]
      or [Bob]) to the actual staker they represent. *)
  val staker : t -> player -> Staker.t
end

(** To begin a game, first the conflict point in the commit tree is
    found, and then this function is applied.

    [initial inbox dal_slots_history ~start_level ~parent_commitment
    ~defender_commitment ~refuter ~defender ~default_number_of_sections] will
    construct an initial game where [refuter] is next to play. The game has
    [dissection] with three states:

      - firstly, the state (with tick zero) of [parent_commitment], the
      commitment that both stakers agree on.

      - secondly, the state and tick count of [defender_commitment], the
      commitment that [defender] has staked on.

      - thirdly, a [None] state which is a single tick after the
      [defender_commitment] commitment. This represents the claim, implicit in
      the commitment, that the state given is blocked.

    This gives [refuter] a binary choice: she can refute the commit
    itself by providing a new dissection between the two committed
    states, or she can refute the claim that the [child] commit is a
    blocked state by immediately providing a proof of a single tick
    increment from that state to its successor. *)
val initial :
  Sc_rollup_inbox_repr.history_proof ->
  Dal_slot_repr.History.t ->
  start_level:Raw_level_repr.t ->
  parent_commitment:Sc_rollup_commitment_repr.t ->
  defender_commitment:Sc_rollup_commitment_repr.t ->
  refuter:Signature.public_key_hash ->
  defender:Signature.public_key_hash ->
  default_number_of_sections:int ->
  t

(** A [step] in the game is either a new dissection (if there are
    intermediate ticks remaining to put in it) or a proof. *)
type step =
  | Dissection of dissection_chunk list
  | Proof of Sc_rollup_proof_repr.serialized Sc_rollup_proof_repr.t

(** A [refutation] is a move in the game. *)
type refutation =
  | Start of {
      player_commitment_hash : Sc_rollup_commitment_repr.Hash.t;
      opponent_commitment_hash : Sc_rollup_commitment_repr.Hash.t;
    }
  | Move of {choice : Sc_rollup_tick_repr.t; step : step}
      (** [choice] is the final tick in the current dissection at which
          the two players agree. *)

val pp_refutation : Format.formatter -> refutation -> unit

val refutation_encoding : refutation Data_encoding.t

(** A game ends for one of two reasons: the conflict has been
resolved via a proof or a player has been timed out. *)
type reason = Conflict_resolved | Timeout

val pp_reason : Format.formatter -> reason -> unit

val reason_encoding : reason Data_encoding.t

(** The game result. *)
type game_result =
  | Loser of {reason : reason; loser : Staker.t}  (** One player lost. *)
  | Draw  (** The game ended in a draw *)

val pp_game_result : Format.formatter -> game_result -> unit

val game_result_encoding : game_result Data_encoding.t

(** A type that represents the current game status in a way that is
    useful to the outside world (using actual [Staker.t] values
    instead of the internal [player] type).

    The [Staker.t] in the [Ended] case is the loser of the game: the
    staker who will have their stake slashed.

    Used in operation result types. *)
type status = Ongoing | Ended of game_result

val pp_status : Format.formatter -> status -> unit

val status_encoding : status Data_encoding.t

(* FIXME/DAL: https://gitlab.com/tezos/tezos/-/issues/3997
   Providing DAL parameters here is not resilient to their change during
   protocol upgrade. *)

(** Applies the move [refutation] to the game. Returns the game {!status}
    after applying the move.

    In the case of the game continuing, this swaps the current
    player and returns a [Ongoing] status. Otherwise, it returns a
    [Ended <game_result>] status.

    The provided DAL related parameters are used in case the game needs to:
    - Check that a page's content is part of a slot (using the slot's commitment)
      when refuting a DAL page reveal.
    - Check that the parameters are correct when refuting a DAL parameter reveal.
*)
val play :
  Sc_rollups.Kind.t ->
  Dal_slot_repr.parameters ->
  dal_activation_level:Raw_level_repr.t option ->
  dal_attestation_lag:int ->
  dal_number_of_slots:int ->
  stakers:Index.t ->
  Sc_rollup_metadata_repr.t ->
  t ->
  step:step ->
  choice:Sc_rollup_tick_repr.t ->
  is_reveal_enabled:Sc_rollup_PVM_sig.is_reveal_enabled ->
  dal_attested_slots_validity_lag:int ->
  (game_result, t) Either.t tzresult Lwt.t

(** [cost_play ~step ~choice] returns the gas cost of [play] applied with[step],
    and [choice]. *)
val cost_play : step:step -> choice:Sc_rollup_tick_repr.t -> Gas_limit_repr.cost

(** A type that represents the number of blocks left for players to play. Each
    player has her timeout value. `timeout` is expressed in the number of
    blocks.

    Timeout logic is similar to a chess clock. Each player starts with the same
    timeout. Each game move updates the timeout of the current player by
    decreasing it by the amount of time she took to play, i.e. number of blocks
    since the opponent last move. See {!Sc_rollup_refutation_storage.game_move}
    to see the implementation.
*)
type timeout = {
  alice : int;  (** Timeout of [Alice]. *)
  bob : int;  (** Timeout of [Bob]. *)
  last_turn_level : Raw_level_repr.t;  (** Block level of the last turn move. *)
}

val timeout_encoding : timeout Data_encoding.t

module Internal_for_tests : sig
  (** Checks that the tick count chosen by the current move is one of
    the ones in the current dissection. Returns a tuple containing
    the current dissection interval (including the two states) between
    this tick and the next. *)
  val find_choice :
    dissection_chunk list ->
    Sc_rollup_tick_repr.t ->
    (dissection_chunk * dissection_chunk) tzresult

  (** See {!Sc_rollup_dissection_chunk_repr.default_check} *)
  val check_dissection :
    default_number_of_sections:int ->
    start_chunk:dissection_chunk ->
    stop_chunk:dissection_chunk ->
    dissection_chunk list ->
    unit tzresult
end
back to top