From a6813dce944975a3652b38e7727271245f7e5207 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Tue, 13 Sep 2022 15:59:54 +0200 Subject: [PATCH 1/4] Scoru,Proto: remove [invalid_move] --- .../lib_protocol/alpha_context.mli | 77 +- .../lib_protocol/sc_rollup_game_repr.ml | 802 +++++++++--------- .../lib_protocol/sc_rollup_game_repr.mli | 134 ++- .../sc_rollup_refutation_storage.ml | 2 +- .../integration/operations/test_sc_rollup.ml | 41 +- .../test/pbt/test_refutation_game.ml | 155 ++-- .../test/unit/test_sc_rollup_game.ml | 27 +- 7 files changed, 605 insertions(+), 633 deletions(-) diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index 13b3d6a08151..583e50631739 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -3597,42 +3597,7 @@ module Sc_rollup : sig val pp_refutation : Format.formatter -> refutation -> unit - type invalid_move = - | Dissection_choice_not_found of Tick.t - | Dissection_number_of_sections_mismatch of {expected : Z.t; given : Z.t} - | Dissection_invalid_number_of_sections of Z.t - | Dissection_start_hash_mismatch of { - expected : State_hash.t option; - given : State_hash.t option; - } - | Dissection_stop_hash_mismatch of State_hash.t option - | Dissection_edge_ticks_mismatch of { - dissection_start_tick : Tick.t; - dissection_stop_tick : Tick.t; - chunk_start_tick : Tick.t; - chunk_stop_tick : Tick.t; - } - | Dissection_ticks_not_increasing - | Dissection_invalid_distribution - | Dissection_invalid_successive_states_shape - | Proof_unexpected_section_size of Z.t - | Proof_start_state_hash_mismatch of { - start_state_hash : State_hash.t option; - start_proof : State_hash.t; - } - | Proof_stop_state_hash_failed_to_refute of { - stop_state_hash : State_hash.t option; - stop_proof : State_hash.t option; - } - | Proof_stop_state_hash_failed_to_validate of { - stop_state_hash : State_hash.t option; - stop_proof : State_hash.t option; - } - | Proof_invalid of string - - val pp_invalid_move : Format.formatter -> invalid_move -> unit - - type reason = Conflict_resolved | Invalid_move of invalid_move | Timeout + type reason = Conflict_resolved | Timeout val pp_reason : Format.formatter -> reason -> unit @@ -3664,19 +3629,55 @@ module Sc_rollup : sig t val play : - stakers:Index.t -> t -> refutation -> (game_result, t) Either.t Lwt.t + stakers:Index.t -> + t -> + refutation -> + (game_result, t) Either.t tzresult Lwt.t type timeout = {alice : int; bob : int; last_turn_level : Raw_level_repr.t} val timeout_encoding : timeout Data_encoding.t + type error += + | Dissection_choice_not_found of Tick.t + | Dissection_number_of_sections_mismatch of {expected : Z.t; given : Z.t} + | Dissection_invalid_number_of_sections of Z.t + | Dissection_start_hash_mismatch of { + expected : State_hash.t option; + given : State_hash.t option; + } + | Dissection_stop_hash_mismatch of State_hash.t option + | Dissection_edge_ticks_mismatch of { + dissection_start_tick : Tick.t; + dissection_stop_tick : Tick.t; + chunk_start_tick : Tick.t; + chunk_stop_tick : Tick.t; + } + | Dissection_ticks_not_increasing + | Dissection_invalid_distribution + | Dissection_invalid_successive_states_shape + | Proof_unexpected_section_size of Z.t + | Proof_start_state_hash_mismatch of { + start_state_hash : State_hash.t option; + start_proof : State_hash.t; + } + | Proof_stop_state_hash_failed_to_refute of { + stop_state_hash : State_hash.t option; + stop_proof : State_hash.t option; + } + | Proof_stop_state_hash_failed_to_validate of { + stop_state_hash : State_hash.t option; + stop_proof : State_hash.t option; + } + | Dissecting_during_final_move + module Internal_for_tests : sig val check_dissection : default_number_of_sections:int -> start_chunk:dissection_chunk -> stop_chunk:dissection_chunk -> dissection_chunk list -> - (unit, reason) result Lwt.t + unit tzresult end end diff --git a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml index df0034b2e26e..16dff6619cc9 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml @@ -26,6 +26,331 @@ open Sc_rollup_repr +(** {2 Refutation game errors} *) + +type error += + | (* `Temporary *) + Dissection_choice_not_found of Sc_rollup_tick_repr.t + | (* `Temporary *) + Dissection_number_of_sections_mismatch of { + expected : Z.t; + given : Z.t; + } + | (* `Permanent *) Dissection_invalid_number_of_sections of Z.t + | (* `Temporary *) + Dissection_start_hash_mismatch of { + expected : Sc_rollup_repr.State_hash.t option; + given : Sc_rollup_repr.State_hash.t option; + } + | (* `Temporary *) + Dissection_stop_hash_mismatch of + Sc_rollup_repr.State_hash.t option + | (* `Temporary *) + Dissection_edge_ticks_mismatch of { + dissection_start_tick : Sc_rollup_tick_repr.t; + dissection_stop_tick : Sc_rollup_tick_repr.t; + chunk_start_tick : Sc_rollup_tick_repr.t; + chunk_stop_tick : Sc_rollup_tick_repr.t; + } + | (* `Permanent *) Dissection_ticks_not_increasing + | (* `Permanent *) Dissection_invalid_distribution + | (* `Permanent *) Dissection_invalid_successive_states_shape + | (* `Permanent *) Proof_unexpected_section_size of Z.t + | (* `Temporary *) + Proof_start_state_hash_mismatch of { + start_state_hash : Sc_rollup_repr.State_hash.t option; + start_proof : Sc_rollup_repr.State_hash.t; + } + | (* `Temporary *) + 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; + } + | (* `Temporary *) + 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; + } + | (* `Temporary *) Dissecting_during_final_move + +let pp_hash_opt fmt = function + | None -> Format.fprintf fmt "None" + | Some x -> Sc_rollup_repr.State_hash.pp fmt x + +let () = + let description = "Dissection choice not found" in + register_error_kind + `Temporary + ~id:"Dissection_choice_not_found" + ~title:description + ~description + ~pp:(fun ppf choice -> + Format.fprintf + ppf + "No section starting with tick %a found" + Sc_rollup_tick_repr.pp + choice) + Data_encoding.(obj1 (req "choice" Sc_rollup_tick_repr.encoding)) + (function Dissection_choice_not_found tick -> Some tick | _ -> None) + (fun tick -> Dissection_choice_not_found tick) ; + let description = "Mismatch in the number of sections in the dissection" in + register_error_kind + `Temporary + ~id:"Dissection_number_of_sections_mismatch" + ~title:description + ~description + ~pp:(fun ppf (expected, given) -> + Format.fprintf + ppf + "The number of sections must be equal to %a instead of %a" + Z.pp_print + expected + Z.pp_print + given) + Data_encoding.(obj2 (req "expected" n) (req "given" n)) + (function + | Dissection_number_of_sections_mismatch {expected; given} -> + Some (expected, given) + | _ -> None) + (fun (expected, given) -> + Dissection_number_of_sections_mismatch {expected; given}) ; + let description = "Invalid number of sections in the dissection" in + register_error_kind + `Permanent + ~id:"Dissection_invalid_number_of_sections" + ~title:description + ~description + ~pp:(fun ppf n -> + Format.fprintf + ppf + "A dissection with %a sections can never be valid" + Z.pp_print + n) + Data_encoding.(obj1 (req "value" n)) + (function Dissection_invalid_number_of_sections n -> Some n | _ -> None) + (fun n -> Dissection_invalid_number_of_sections n) ; + let description = "Mismatch in the start hash of the dissection" in + register_error_kind + `Temporary + ~id:"Dissection_start_hash_mismatch" + ~title:description + ~description + ~pp:(fun ppf (given, expected) -> + match given with + | None -> Format.fprintf ppf "The start hash must not be None" + | Some _ -> + Format.fprintf + ppf + "The start hash should be equal to %a, but the provided hash is %a" + pp_hash_opt + expected + pp_hash_opt + given) + Data_encoding.( + obj2 + (req "expected" (option Sc_rollup_repr.State_hash.encoding)) + (req "given" (option Sc_rollup_repr.State_hash.encoding))) + (function + | Dissection_start_hash_mismatch {expected; given} -> + Some (expected, given) + | _ -> None) + (fun (expected, given) -> Dissection_start_hash_mismatch {expected; given}) ; + let description = "Mismatch in the stop hash of the dissection" in + register_error_kind + `Temporary + ~id:"Dissection_stop_hash_mismatch" + ~title:description + ~description + ~pp:(fun ppf h -> + Format.fprintf ppf "The stop hash should not be equal to %a" pp_hash_opt h) + Data_encoding.( + obj1 (req "hash" (option Sc_rollup_repr.State_hash.encoding))) + (function Dissection_stop_hash_mismatch hopt -> Some hopt | _ -> None) + (fun hopt -> Dissection_stop_hash_mismatch hopt) ; + let description = "Mismatch in the edge ticks of the dissection" in + register_error_kind + `Temporary + ~id:"Dissection_edge_ticks_mismatch" + ~title:description + ~description + ~pp: + (fun ppf + ( dissection_start_tick, + dissection_stop_tick, + chunk_start_tick, + chunk_stop_tick ) -> + Sc_rollup_tick_repr.( + Format.fprintf + ppf + "We should have dissection_start_tick(%a) = %a and \ + dissection_stop_tick(%a) = %a" + pp + dissection_start_tick + pp + chunk_start_tick + pp + dissection_stop_tick + pp + chunk_stop_tick)) + Data_encoding.( + obj4 + (req "dissection_start_tick" Sc_rollup_tick_repr.encoding) + (req "dissection_stop_tick" Sc_rollup_tick_repr.encoding) + (req "chunk_start_tick" Sc_rollup_tick_repr.encoding) + (req "chunk_stop_tick" Sc_rollup_tick_repr.encoding)) + (function + | Dissection_edge_ticks_mismatch e -> + Some + ( e.dissection_start_tick, + e.dissection_stop_tick, + e.chunk_start_tick, + e.chunk_stop_tick ) + | _ -> None) + (fun ( dissection_start_tick, + dissection_stop_tick, + chunk_start_tick, + chunk_stop_tick ) -> + Dissection_edge_ticks_mismatch + { + dissection_start_tick; + dissection_stop_tick; + chunk_start_tick; + chunk_stop_tick; + }) ; + let description = "Ticks should only increase in dissection" in + register_error_kind + `Permanent + ~id:"Dissection_ticks_not_increasing" + ~title:description + ~description + ~pp:(fun ppf () -> Format.pp_print_string ppf description) + Data_encoding.empty + (function Dissection_ticks_not_increasing -> Some () | _ -> None) + (fun () -> Dissection_ticks_not_increasing) ; + let description = + "Maximum tick increment in a section cannot be more than half total \ + dissection length" + in + register_error_kind + `Permanent + ~id:"Dissection_invalid_distribution" + ~title:description + ~description + ~pp:(fun ppf () -> Format.pp_print_string ppf description) + Data_encoding.empty + (function Dissection_invalid_distribution -> Some () | _ -> None) + (fun () -> Dissection_invalid_distribution) ; + let description = "Cannot recover from a blocked state in a dissection" in + register_error_kind + `Permanent + ~id:"Dissection_invalid_successive_states_shape" + ~title:description + ~description + ~pp:(fun ppf () -> Format.pp_print_string ppf description) + Data_encoding.empty + (function + | Dissection_invalid_successive_states_shape -> Some () | _ -> None) + (fun () -> Dissection_invalid_successive_states_shape) ; + let description = "The distance for a proof should be equal to 1" in + register_error_kind + `Permanent + ~id:"Dissection_unexpected_section_size" + ~title:description + ~description + ~pp:(fun ppf n -> + Format.fprintf + ppf + "Distance should be equal to 1 in a proof, but got %a" + Z.pp_print + n) + Data_encoding.(obj1 (req "n" n)) + (function Proof_unexpected_section_size n -> Some n | _ -> None) + (fun n -> Proof_unexpected_section_size n) ; + let description = "The start state hash of the proof is invalid" in + register_error_kind + `Temporary + ~id:"Proof_start_state_hash_mismatch" + ~title:description + ~description + ~pp:(fun ppf (start_state_hash, start_proof) -> + Format.fprintf + ppf + "start(%a) should be equal to start_proof(%a)" + pp_hash_opt + start_state_hash + Sc_rollup_repr.State_hash.pp + start_proof) + Data_encoding.( + obj2 + (req "start_state_hash" (option Sc_rollup_repr.State_hash.encoding)) + (req "start_proof" Sc_rollup_repr.State_hash.encoding)) + (function + | Proof_start_state_hash_mismatch {start_state_hash; start_proof} -> + Some (start_state_hash, start_proof) + | _ -> None) + (fun (start_state_hash, start_proof) -> + Proof_start_state_hash_mismatch {start_state_hash; start_proof}) ; + let description = "Failed to refute the stop state hash with the proof" in + register_error_kind + `Temporary + ~id:"Proof_stop_state_hash_failed_to_refute" + ~title:description + ~description + ~pp:(fun ppf (stop_state_hash, stop_proof) -> + Format.fprintf + ppf + "Trying to refute %a, the stop_proof must not be equal to %a" + pp_hash_opt + stop_state_hash + pp_hash_opt + stop_proof) + Data_encoding.( + obj2 + (req "stop_state_hash" (option Sc_rollup_repr.State_hash.encoding)) + (req "stop_proof" (option Sc_rollup_repr.State_hash.encoding))) + (function + | Proof_stop_state_hash_failed_to_refute {stop_state_hash; stop_proof} -> + Some (stop_state_hash, stop_proof) + | _ -> None) + (fun (stop_state_hash, stop_proof) -> + Proof_stop_state_hash_failed_to_refute {stop_state_hash; stop_proof}) ; + let description = "Failed to validate the stop state hash with the proof" in + register_error_kind + `Temporary + ~id:"Proof_stop_state_hash_failed_to_validate" + ~title:description + ~description + ~pp:(fun ppf (stop_state_hash, stop_proof) -> + Format.fprintf + ppf + "Trying to validate %a, the stop_proof must not be equal to %a" + pp_hash_opt + stop_state_hash + pp_hash_opt + stop_proof) + Data_encoding.( + obj2 + (req "stop_state_hash" (option Sc_rollup_repr.State_hash.encoding)) + (req "stop_proof" (option Sc_rollup_repr.State_hash.encoding))) + (function + | Proof_stop_state_hash_failed_to_validate {stop_state_hash; stop_proof} + -> + Some (stop_state_hash, stop_proof) + | _ -> None) + (fun (stop_state_hash, stop_proof) -> + Proof_stop_state_hash_failed_to_validate {stop_state_hash; stop_proof}) ; + let description = "Tried to play a dissecting when the final move started" in + register_error_kind + `Temporary + ~id:"Dissecting_during_final_move" + ~title:description + ~description + ~pp:(fun ppf () -> Format.pp_print_string ppf description) + Data_encoding.empty + (function Dissecting_during_final_move -> Some () | _ -> None) + (fun () -> Dissecting_during_final_move) ; + () + type player = Alice | Bob module V1 = struct @@ -153,7 +478,8 @@ module V1 = struct let string_of_player = function Alice -> "alice" | Bob -> "bob" - let pp_player ppf player = Format.fprintf ppf "%s" (string_of_player player) + let pp_player ppf player = + Format.pp_print_string ppf (string_of_player player) let opponent = function Alice -> Bob | Bob -> Alice @@ -441,310 +767,11 @@ let refutation_encoding = (req "choice" Sc_rollup_tick_repr.encoding) (req "step" step_encoding)) -type invalid_move = - | Dissection_choice_not_found of Sc_rollup_tick_repr.t - | Dissection_number_of_sections_mismatch of {expected : Z.t; given : Z.t} - | Dissection_invalid_number_of_sections of Z.t - | Dissection_start_hash_mismatch of { - expected : State_hash.t option; - given : State_hash.t option; - } - | Dissection_stop_hash_mismatch of State_hash.t option - | Dissection_edge_ticks_mismatch of { - dissection_start_tick : Sc_rollup_tick_repr.t; - dissection_stop_tick : Sc_rollup_tick_repr.t; - chunk_start_tick : Sc_rollup_tick_repr.t; - chunk_stop_tick : Sc_rollup_tick_repr.t; - } - | Dissection_ticks_not_increasing - | Dissection_invalid_distribution - | Dissection_invalid_successive_states_shape - | Proof_unexpected_section_size of Z.t - | Proof_start_state_hash_mismatch of { - start_state_hash : State_hash.t option; - start_proof : State_hash.t; - } - | Proof_stop_state_hash_failed_to_refute of { - stop_state_hash : State_hash.t option; - stop_proof : State_hash.t option; - } - | Proof_stop_state_hash_failed_to_validate of { - stop_state_hash : State_hash.t option; - stop_proof : State_hash.t option; - } - | Proof_invalid of string - -let pp_invalid_move fmt = - let pp_hash_opt fmt = function - | None -> Format.fprintf fmt "None" - | Some x -> State_hash.pp fmt x - in - function - | Dissection_choice_not_found tick -> - Format.fprintf - fmt - "No section starting with tick %a found" - Sc_rollup_tick_repr.pp - tick - | Dissection_number_of_sections_mismatch {expected; given} -> - Format.fprintf - fmt - "The number of sections must be equal to %a instead of %a" - Z.pp_print - expected - Z.pp_print - given - | Dissection_invalid_number_of_sections n -> - Format.fprintf - fmt - "A dissection with %a sections can never be valid" - Z.pp_print - n - | Dissection_start_hash_mismatch {given = None; _} -> - Format.fprintf fmt "The start hash must not be None" - | Dissection_start_hash_mismatch {given; expected} -> - Format.fprintf - fmt - "The start hash should be equal to %a, but the provided hash is %a" - pp_hash_opt - expected - pp_hash_opt - given - | Dissection_stop_hash_mismatch h -> - Format.fprintf fmt "The stop hash should not be equal to %a" pp_hash_opt h - | Dissection_edge_ticks_mismatch - { - dissection_start_tick; - dissection_stop_tick; - chunk_start_tick; - chunk_stop_tick; - } -> - Sc_rollup_tick_repr.( - Format.fprintf - fmt - "We should have dissection_start_tick(%a) = %a and \ - dissection_stop_tick(%a) = %a" - pp - dissection_start_tick - pp - chunk_start_tick - pp - dissection_stop_tick - pp - chunk_stop_tick) - | Dissection_ticks_not_increasing -> - Format.fprintf fmt "Ticks should only increase in dissection" - | Dissection_invalid_successive_states_shape -> - Format.fprintf - fmt - "Cannot return to a Some state after being at a None state" - | Dissection_invalid_distribution -> - Format.fprintf - fmt - "Maximum tick increment in a section cannot be more than half total \ - dissection length" - | Proof_unexpected_section_size n -> - Format.fprintf - fmt - "dist should be equal to 1 in a proof, but got %a" - Z.pp_print - n - | Proof_start_state_hash_mismatch {start_state_hash; start_proof} -> - Format.fprintf - fmt - "start(%a) should be equal to start_proof(%a)" - pp_hash_opt - start_state_hash - State_hash.pp - start_proof - | Proof_stop_state_hash_failed_to_refute {stop_state_hash; stop_proof} -> - Format.fprintf - fmt - "Trying to refute %a, the stop_proof must not be equal to %a" - pp_hash_opt - stop_state_hash - pp_hash_opt - stop_proof - | Proof_stop_state_hash_failed_to_validate {stop_state_hash; stop_proof} -> - Format.fprintf - fmt - "Trying to validate %a, the stop_proof must be equal to %a" - pp_hash_opt - stop_state_hash - pp_hash_opt - stop_proof - | Proof_invalid s -> Format.fprintf fmt "Invalid proof: %s" s - -let invalid_move_encoding = - let open Data_encoding in - union - ~tag_size:`Uint8 - [ - case - ~title:"sc_rollup_dissection_choice_not_found" - (Tag 0) - (obj2 - (req "kind" (constant "dissection_choice_not_found")) - (req "tick" Sc_rollup_tick_repr.encoding)) - (function - | Dissection_choice_not_found tick -> Some ((), tick) | _ -> None) - (fun ((), tick) -> Dissection_choice_not_found tick); - case - ~title:"sc_rollup_dissection_number_of_sections_mismatch" - (Tag 1) - (obj3 - (req "kind" (constant "dissection_number_of_sections_mismatch")) - (req "expected" n) - (req "given" n)) - (function - | Dissection_number_of_sections_mismatch {expected; given} -> - Some ((), expected, given) - | _ -> None) - (fun ((), expected, given) -> - Dissection_number_of_sections_mismatch {expected; given}); - case - ~title:"sc_rollup_dissection_invalid_number_of_sections" - (Tag 2) - (obj2 - (req "kind" (constant "dissection_invalid_number_of_sections")) - (req "value" n)) - (function - | Dissection_invalid_number_of_sections value -> Some ((), value) - | _ -> None) - (fun ((), value) -> Dissection_invalid_number_of_sections value); - case - ~title:"sc_rollup_dissection_unexpected_start_hash" - (Tag 3) - (obj3 - (req "kind" (constant "dissection_unexpected_start_hash")) - (req "expected" (option State_hash.encoding)) - (req "given" (option State_hash.encoding))) - (function - | Dissection_start_hash_mismatch {expected; given} -> - Some ((), expected, given) - | _ -> None) - (fun ((), expected, given) -> - Dissection_start_hash_mismatch {expected; given}); - case - ~title:"sc_rollup_dissection_stop_hash_mismatch" - (Tag 4) - (obj2 - (req "kind" (constant "dissection_stop_hash_mismatch")) - (req "hash" (option State_hash.encoding))) - (function - | Dissection_stop_hash_mismatch hopt -> Some ((), hopt) | _ -> None) - (fun ((), hopt) -> Dissection_stop_hash_mismatch hopt); - case - ~title:"sc_rollup_dissection_edge_ticks_mismatch" - (Tag 5) - (obj5 - (req "kind" (constant "dissection_edge_ticks_mismatch")) - (req "dissection_start_tick" Sc_rollup_tick_repr.encoding) - (req "dissection_stop_tick" Sc_rollup_tick_repr.encoding) - (req "chunk_start_tick" Sc_rollup_tick_repr.encoding) - (req "chunk_stop_tick" Sc_rollup_tick_repr.encoding)) - (function - | Dissection_edge_ticks_mismatch e -> - Some - ( (), - e.dissection_start_tick, - e.dissection_stop_tick, - e.chunk_start_tick, - e.chunk_stop_tick ) - | _ -> None) - (fun ( (), - dissection_start_tick, - dissection_stop_tick, - chunk_start_tick, - chunk_stop_tick ) -> - Dissection_edge_ticks_mismatch - { - dissection_start_tick; - dissection_stop_tick; - chunk_start_tick; - chunk_stop_tick; - }); - case - ~title:"sc_rollup_dissection_ticks_not_increasing" - (Tag 6) - (obj1 (req "kind" (constant "dissection_ticks_not_increasing"))) - (function Dissection_ticks_not_increasing -> Some () | _ -> None) - (fun () -> Dissection_ticks_not_increasing); - case - ~title:"sc_rollup_dissection_invalid_distribution" - (Tag 7) - (obj1 (req "kind" (constant "dissection_invalid_distribution"))) - (function Dissection_invalid_distribution -> Some () | _ -> None) - (fun () -> Dissection_invalid_distribution); - case - ~title:"sc_rollup_dissection_invalid_successive_states_shape" - (Tag 8) - (obj1 - (req "kind" (constant "dissection_invalid_successive_states_shape"))) - (function - | Dissection_invalid_successive_states_shape -> Some () | _ -> None) - (fun () -> Dissection_invalid_successive_states_shape); - case - ~title:"sc_rollup_proof_unexpected_section_size" - (Tag 9) - (obj2 - (req "kind" (constant "proof_unexpected_section_size")) - (req "value" n)) - (function Proof_unexpected_section_size n -> Some ((), n) | _ -> None) - (fun ((), n) -> Proof_unexpected_section_size n); - case - ~title:"sc_rollup_proof_start_state_hash_mismatch" - (Tag 10) - (obj3 - (req "kind" (constant "proof_start_state_hash_mismatch")) - (req "start_state_hash" (option State_hash.encoding)) - (req "start_proof" State_hash.encoding)) - (function - | Proof_start_state_hash_mismatch e -> - Some ((), e.start_state_hash, e.start_proof) - | _ -> None) - (fun ((), start_state_hash, start_proof) -> - Proof_start_state_hash_mismatch {start_state_hash; start_proof}); - case - ~title:"sc_rollup_proof_stop_state_hash_failed_to_refute" - (Tag 11) - (obj3 - (req "kind" (constant "proof_stop_state_hash_failed_to_refute")) - (req "stop_state_hash" (option State_hash.encoding)) - (req "stop_proof" (option State_hash.encoding))) - (function - | Proof_stop_state_hash_failed_to_refute e -> - Some ((), e.stop_state_hash, e.stop_proof) - | _ -> None) - (fun ((), stop_state_hash, stop_proof) -> - Proof_stop_state_hash_failed_to_refute {stop_state_hash; stop_proof}); - case - ~title:"sc_rollup_proof_stop_state_hash_failed_to_validate" - (Tag 12) - (obj3 - (req "kind" (constant "proof_stop_state_hash_failed_to_validate")) - (req "stop_state_hash" (option State_hash.encoding)) - (req "stop_proof" (option State_hash.encoding))) - (function - | Proof_stop_state_hash_failed_to_validate e -> - Some ((), e.stop_state_hash, e.stop_proof) - | _ -> None) - (fun ((), stop_state_hash, stop_proof) -> - Proof_stop_state_hash_failed_to_validate {stop_state_hash; stop_proof}); - case - ~title:"sc_rollup_proof_invalid" - (Tag 13) - (obj2 (req "kind" (constant "proof_invalid")) (req "message" string)) - (function Proof_invalid s -> Some ((), s) | _ -> None) - (fun ((), s) -> Proof_invalid s); - ] - -type reason = Conflict_resolved | Invalid_move of invalid_move | Timeout +type reason = Conflict_resolved | Timeout let pp_reason ppf reason = match reason with | Conflict_resolved -> Format.fprintf ppf "conflict resolved" - | Invalid_move mv -> Format.fprintf ppf "invalid move(%a)" pp_invalid_move mv | Timeout -> Format.fprintf ppf "timeout" let reason_encoding = @@ -758,15 +785,9 @@ let reason_encoding = (constant "conflict_resolved") (function Conflict_resolved -> Some () | _ -> None) (fun () -> Conflict_resolved); - case - ~title:"Invalid_move" - (Tag 1) - invalid_move_encoding - (function Invalid_move reason -> Some reason | _ -> None) - (fun s -> Invalid_move s); case ~title:"Timeout" - (Tag 2) + (Tag 1) (constant "timeout") (function Timeout -> Some () | _ -> None) (fun () -> Timeout); @@ -831,28 +852,20 @@ let status_encoding = (fun r -> Ended r); ] -let invalid_move reason = - let open Lwt_result_syntax in - fail (Invalid_move reason) - let find_choice dissection tick = - let open Lwt_result_syntax in + let open Tzresult_syntax in let rec traverse states = match states with | ({state_hash = _; tick = state_tick} as curr) :: next :: others -> if Sc_rollup_tick_repr.(tick = state_tick) then return (curr, next) else traverse (next :: others) - | _ -> invalid_move (Dissection_choice_not_found tick) + | _ -> fail (Dissection_choice_not_found tick) in traverse dissection -let check pred reason = - let open Lwt_result_syntax in - if pred then return () else invalid_move reason - let check_dissection ~default_number_of_sections ~start_chunk ~stop_chunk dissection = - let open Lwt_result_syntax in + let open Tzresult_syntax in let len = Z.of_int @@ List.length dissection in let dist = Sc_rollup_tick_repr.distance start_chunk.tick stop_chunk.tick in let should_be_equal_to expected = @@ -861,24 +874,24 @@ let check_dissection ~default_number_of_sections ~start_chunk ~stop_chunk let num_sections = Z.of_int @@ default_number_of_sections in let* () = if Z.geq dist num_sections then - check Z.(equal len num_sections) (should_be_equal_to num_sections) + error_unless Z.(equal len num_sections) (should_be_equal_to num_sections) else if Z.(gt dist one) then - check Z.(equal len (succ dist)) (should_be_equal_to Z.(succ dist)) - else invalid_move (Dissection_invalid_number_of_sections len) + error_unless Z.(equal len (succ dist)) (should_be_equal_to Z.(succ dist)) + else fail (Dissection_invalid_number_of_sections len) in let* () = match (List.hd dissection, List.last_opt dissection) with | Some {state_hash = a; tick = a_tick}, Some {state_hash = b; tick = b_tick} -> let* () = - check + error_unless (Option.equal State_hash.equal a start_chunk.state_hash && not (Option.is_none a)) (Dissection_start_hash_mismatch {expected = start_chunk.state_hash; given = a}) in let* () = - check + error_unless (not (Option.equal State_hash.equal b stop_chunk.state_hash)) ((* If the [b] state is equal to [stop_chunk], that means we agree on the after state of the section. But, we're trying @@ -887,7 +900,7 @@ let check_dissection ~default_number_of_sections ~start_chunk ~stop_chunk stop_chunk.state_hash) in Sc_rollup_tick_repr.( - check + error_unless (a_tick = start_chunk.tick && b_tick = stop_chunk.tick) (Dissection_edge_ticks_mismatch { @@ -899,19 +912,19 @@ let check_dissection ~default_number_of_sections ~start_chunk ~stop_chunk | _ -> (* This case is probably already handled by the [Dissection_invalid_number_of_sections] returned above *) - invalid_move (Dissection_invalid_number_of_sections len) + fail (Dissection_invalid_number_of_sections len) in let half_dist = Z.(div dist (of_int 2) |> succ) in let rec traverse states = match states with | {state_hash = None; _} :: {state_hash = Some _; _} :: _ -> - invalid_move Dissection_invalid_successive_states_shape + fail Dissection_invalid_successive_states_shape | {tick; _} :: ({tick = next_tick; state_hash = _} as next) :: others -> if Sc_rollup_tick_repr.(tick < next_tick) then let incr = Sc_rollup_tick_repr.distance tick next_tick in if Z.(leq incr half_dist) then traverse (next :: others) - else invalid_move Dissection_invalid_distribution - else invalid_move Dissection_ticks_not_increasing + else fail Dissection_invalid_distribution + else fail Dissection_ticks_not_increasing | _ -> return () in traverse dissection @@ -919,12 +932,12 @@ let check_dissection ~default_number_of_sections ~start_chunk ~stop_chunk (** Check that the chosen interval is a single tick. *) let check_proof_distance_is_one ~start_tick ~stop_tick = let dist = Sc_rollup_tick_repr.distance start_tick stop_tick in - check Z.(equal dist one) (Proof_unexpected_section_size dist) + error_unless Z.(equal dist one) (Proof_unexpected_section_size dist) (** Check the proof begins with the correct state. *) let check_proof_start_state ~start_state proof = let start_proof = Sc_rollup_proof_repr.start proof in - check + error_unless (Option.equal State_hash.equal start_state (Some start_proof)) (Proof_start_state_hash_mismatch {start_state_hash = start_state; start_proof}) @@ -945,7 +958,7 @@ let check_proof_stop_state ~stop_state input_given | None, Needs_reveal _ -> None in - check + error_unless (let b = Option.equal State_hash.equal stop_state stop_proof in if validate then b else not b) (if validate then @@ -963,6 +976,7 @@ let check_proof_validate_stop_state ~stop_state input input_request proof = let check_proof_refute_stop_state ~stop_state input input_request proof = check_proof_stop_state ~stop_state input input_request proof false +(** Returns the validity of the first final move on top of a dissection. *) let validity_final_move ~first_move ~proof ~game ~start_chunk ~stop_chunk = let open Lwt_result_syntax in let*! res = @@ -970,19 +984,19 @@ let validity_final_move ~first_move ~proof ~game ~start_chunk ~stop_chunk = let*! valid = Sc_rollup_proof_repr.valid inbox_snapshot inbox_level ~pvm_name proof in - let* () = + let*? () = if first_move then check_proof_distance_is_one ~start_tick:start_chunk.tick ~stop_tick:stop_chunk.tick - else return_unit + else ok () in - let* () = + let*? () = check_proof_start_state ~start_state:start_chunk.state_hash proof in match valid with | Ok (input, input_request) -> - let* () = + let*? () = if first_move then check_proof_refute_stop_state ~stop_state:stop_chunk.state_hash @@ -1036,27 +1050,51 @@ let loser_of_results ~alice_result ~bob_result = | true, false -> Some Bob let play ~stakers game refutation = - let open Lwt_syntax in + let open Lwt_tzresult_syntax in let mk_loser reason loser = let loser = Index.staker stakers loser in Either.Left (Loser {loser; reason}) in - let* result = - let open Lwt_result_syntax in - match (refutation.step, game.game_state) with - | Dissection states, Dissecting {dissection; default_number_of_sections} -> - let* start_chunk, stop_chunk = - find_choice dissection refutation.choice - in - let* () = - check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - states - in + match (refutation.step, game.game_state) with + | Dissection states, Dissecting {dissection; default_number_of_sections} -> + let*? start_chunk, stop_chunk = + find_choice dissection refutation.choice + in + let*? () = + check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + states + in + let new_game_state = + Dissecting {dissection = states; default_number_of_sections} + in + return + (Either.Right + { + turn = opponent game.turn; + inbox_snapshot = game.inbox_snapshot; + start_level = game.start_level; + inbox_level = game.inbox_level; + pvm_name = game.pvm_name; + game_state = new_game_state; + }) + | Dissection _, Final_move _ -> fail Dissecting_during_final_move + | Proof proof, Dissecting {dissection; default_number_of_sections = _} -> + let*? start_chunk, stop_chunk = + find_choice dissection refutation.choice + in + let*! player_result = + validity_first_final_move ~proof ~game ~start_chunk ~stop_chunk + in + if player_result then + return @@ mk_loser Conflict_resolved (opponent game.turn) + else let new_game_state = - Dissecting {dissection = states; default_number_of_sections} + let agreed_start_chunk = start_chunk in + let refuted_stop_chunk = stop_chunk in + Final_move {agreed_start_chunk; refuted_stop_chunk} in return (Either.Right @@ -1068,52 +1106,20 @@ let play ~stakers game refutation = pvm_name = game.pvm_name; game_state = new_game_state; }) - | Dissection _, Final_move _ -> - invalid_move - (Proof_invalid "Final move has started, unexpected dissection") - | Proof proof, Dissecting {dissection; default_number_of_sections = _} -> - let* start_chunk, stop_chunk = - find_choice dissection refutation.choice - in - let*! player_result = - validity_first_final_move ~proof ~game ~start_chunk ~stop_chunk - in - if player_result then - return @@ mk_loser Conflict_resolved (opponent game.turn) - else - let new_game_state = - let agreed_start_chunk = start_chunk in - let refuted_stop_chunk = stop_chunk in - Final_move {agreed_start_chunk; refuted_stop_chunk} - in - return - (Either.Right - { - turn = opponent game.turn; - inbox_snapshot = game.inbox_snapshot; - start_level = game.start_level; - inbox_level = game.inbox_level; - pvm_name = game.pvm_name; - game_state = new_game_state; - }) - | Proof proof, Final_move {agreed_start_chunk; refuted_stop_chunk} -> - let*! player_result = - validity_second_final_move - ~agreed_start_chunk - ~refuted_stop_chunk - ~game - ~proof - in - if player_result then - (* If we play when the final move started, the opponent provided - a invalid proof. So if the defender manages to provide a valid - proof, he wins. *) - return @@ mk_loser Conflict_resolved (opponent game.turn) - else return (Either.Left Draw) - in - match result with - | Ok x -> return x - | Error reason -> return @@ mk_loser reason game.turn + | Proof proof, Final_move {agreed_start_chunk; refuted_stop_chunk} -> + let*! player_result = + validity_second_final_move + ~agreed_start_chunk + ~refuted_stop_chunk + ~game + ~proof + in + if player_result then + (* If we play when the final move started, the opponent provided + a invalid proof. So if the defender manages to provide a valid + proof, he wins. *) + return @@ mk_loser Conflict_resolved (opponent game.turn) + else return (Either.Left Draw) module Internal_for_tests = struct let find_choice = find_choice diff --git a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli index a699a29ff267..beaed37722ac 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli @@ -122,6 +122,65 @@ 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. *) + | Dissection_number_of_sections_mismatch of {expected : Z.t; given : Z.t} + (** There are more or less than the expected number of sections in the + given dissection. *) + | Dissection_invalid_number_of_sections of Z.t + (** There are less than two sections in the given dissection, which is + not valid. *) + | Dissection_start_hash_mismatch of { + expected : Sc_rollup_repr.State_hash.t option; + given : Sc_rollup_repr.State_hash.t option; + } + (** The given start hash in a dissection is [None] or doesn't match the + expected one.*) + | Dissection_stop_hash_mismatch of Sc_rollup_repr.State_hash.t option + (** The given stop state hash in a dissection should not match the last + hash of the section being refuted. *) + | Dissection_edge_ticks_mismatch of { + dissection_start_tick : Sc_rollup_tick_repr.t; + dissection_stop_tick : Sc_rollup_tick_repr.t; + chunk_start_tick : Sc_rollup_tick_repr.t; + chunk_stop_tick : Sc_rollup_tick_repr.t; + } + (** The given dissection's edge ticks don't match the edge ticks of the + section being refuted. *) + | Dissection_ticks_not_increasing + (** Invalid provided dissection because ticks are not increasing between + two successive sections. *) + | Dissection_invalid_distribution + (** Invalid provided dissection because ticks split is not well balanced + across sections *) + | Dissection_invalid_successive_states_shape + (** A dissection cannot have a section with no state hash after another + section with some state hash. *) + | 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. *) @@ -308,71 +367,9 @@ val pp_refutation : Format.formatter -> refutation -> unit val refutation_encoding : refutation Data_encoding.t -(** An invalid game move during a dissection or a proof step has one of the - following values: *) -type invalid_move = - | 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. *) - | Dissection_number_of_sections_mismatch of {expected : Z.t; given : Z.t} - (** There are more or less than the expected number of sections in the - given dissection. *) - | Dissection_invalid_number_of_sections of Z.t - (** There are less than two sections in the given dissection, which is - not valid. *) - | Dissection_start_hash_mismatch of { - expected : State_hash.t option; - given : State_hash.t option; - } - (** The given start hash in a dissection is [None] or doesn't match the - expected one.*) - | Dissection_stop_hash_mismatch of State_hash.t option - (** The given stop state hash in a dissection should not match the last - hash of the section being refuted. *) - | Dissection_edge_ticks_mismatch of { - dissection_start_tick : Sc_rollup_tick_repr.t; - dissection_stop_tick : Sc_rollup_tick_repr.t; - chunk_start_tick : Sc_rollup_tick_repr.t; - chunk_stop_tick : Sc_rollup_tick_repr.t; - } - (** The given dissection's edge ticks don't match the edge ticks of the - section being refuted. *) - | Dissection_ticks_not_increasing - (** Invalid provided dissection because ticks are not increasing between - two successive sections. *) - | Dissection_invalid_distribution - (** Invalid provided dissection because ticks split is not well balanced - across sections *) - | Dissection_invalid_successive_states_shape - (** A dissection cannot have a section with no state hash after another - section with some state hash. *) - | 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 : State_hash.t option; - start_proof : 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 : State_hash.t option; - stop_proof : 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 : State_hash.t option; - stop_proof : State_hash.t option; - } - (** The given proof's ending state should match the state being - refuted. *) - | Proof_invalid of string (** The given proof is not valid. *) - -(** Pretty-printer for values of [invalid_move] type *) -val pp_invalid_move : Format.formatter -> invalid_move -> unit - -(** A game ends for one of three reasons: the conflict has been - resolved via a proof, a player has been timed out, or a player has - forfeited because of attempting to make an invalid move. *) -type reason = Conflict_resolved | Invalid_move of invalid_move | Timeout +(** 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 @@ -411,7 +408,8 @@ val loser_of_results : alice_result:bool -> bob_result:bool -> player option player and returns a [Ongoing] status. Otherwise, it returns a [Ended ] status. *) -val play : stakers:Index.t -> t -> refutation -> (game_result, t) Either.t Lwt.t +val play : + stakers:Index.t -> t -> refutation -> (game_result, t) Either.t tzresult Lwt.t (** 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 @@ -439,7 +437,7 @@ module Internal_for_tests : sig val find_choice : dissection_chunk list -> Sc_rollup_tick_repr.t -> - (dissection_chunk * dissection_chunk, reason) result Lwt.t + (dissection_chunk * dissection_chunk) tzresult (** We check firstly that [dissection] is the correct length. It must be [default_number_of_sections] values long, unless the distance between @@ -463,5 +461,5 @@ module Internal_for_tests : sig start_chunk:dissection_chunk -> stop_chunk:dissection_chunk -> dissection_chunk list -> - (unit, reason) result Lwt.t + unit tzresult end diff --git a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml index 3cd43957b15e..e01ee38e22ee 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml @@ -288,7 +288,7 @@ let game_move ctxt rollup ~player ~opponent refutation = (Sc_rollup_game_repr.Index.staker stakers game.turn)) Sc_rollup_wrong_turn in - let*! move_result = Sc_rollup_game_repr.play ~stakers game refutation in + let* move_result = Sc_rollup_game_repr.play ~stakers game refutation in match move_result with | Either.Left game_result -> return (Some game_result, ctxt) | Either.Right new_game -> diff --git a/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml b/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml index 50f1c057c4c0..e30228562e6b 100644 --- a/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml +++ b/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml @@ -1842,30 +1842,25 @@ let test_dissection_during_final_move () = in (* Player2 will play a dissection. *) - let* incr = - let dumb_dissection = - let choice = Sc_rollup.Tick.initial in - Sc_rollup.Game.{choice; step = Dissection []} - in - let* p2_op = - Op.sc_rollup_refute (B block) p2 rollup p1_pkh (Some dumb_dissection) - in - let* incr = Incremental.begin_construction block in - Incremental.add_operation incr p2_op + let dumb_dissection = + let choice = Sc_rollup.Tick.initial in + Sc_rollup.Game.{choice; step = Dissection []} in - - (* That's an obviously invalid move, player2 loses. *) - let expected_game_status : Sc_rollup.Game.status = - Ended - (Loser - { - reason = - Invalid_move - (Proof_invalid "Final move has started, unexpected dissection"); - loser = p2_pkh; - }) - in - assert_refute_result ~game_status:expected_game_status incr + let* p2_op = + Op.sc_rollup_refute (B block) p2 rollup p1_pkh (Some dumb_dissection) + in + (* Dissecting is no longer accepted. *) + let* incr = Incremental.begin_construction block in + let expect_apply_failure = function + | Environment.Ecoproto_error + (Sc_rollup_game_repr.Dissecting_during_final_move as e) + :: _ -> + Assert.test_error_encodings e ; + return_unit + | _ -> failwith "It should have failed with [Dissecting_during_final_move]" + in + let* _incr = Incremental.add_operation ~expect_apply_failure incr p2_op in + return_unit let tests = [ diff --git a/src/proto_alpha/lib_protocol/test/pbt/test_refutation_game.ml b/src/proto_alpha/lib_protocol/test/pbt/test_refutation_game.ml index 4c7807873059..0e5696eb0dee 100644 --- a/src/proto_alpha/lib_protocol/test/pbt/test_refutation_game.ml +++ b/src/proto_alpha/lib_protocol/test/pbt/test_refutation_game.ml @@ -92,18 +92,24 @@ let print_dissection = Format.asprintf "%a" Game.pp_dissection let print_our_states _ = "" -let expect_invalid_move expected = function - | Error (Game.Invalid_move reason) -> - if reason = expected then true +(** Assert that the computation fails with the given message. *) +let assert_fails_with ~__LOC__ (res : unit Environment.Error_monad.tzresult) + expected_err = + match res with + | Error trace -> + let expected_trace = + Environment.Error_monad.trace_of_error expected_err + in + if expected_trace = trace then Lwt.return true else - let pp = Game.pp_invalid_move in + let pp = Environment.Error_monad.pp_trace in QCheck2.Test.fail_reportf "@[Expected reason: %a@;Actual reason: %a@]" pp - expected + expected_trace pp - reason - | _ -> false + trace + | Ok () -> Lwt.return false let initial_of_dissection dissection = List.hd dissection |> WithExceptions.Option.get ~loc:__LOC__ @@ -127,15 +133,12 @@ let modify_start f dissection = {!Sc_rollup_game_repr.check_dissection}. *) let valid_dissection ~default_number_of_sections ~start_chunk ~stop_chunk dissection = - let open Lwt_syntax in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - dissection - in - return (Result.is_ok res) + Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + dissection + |> Result.is_ok (** [disputed_sections ~our_states dissection] returns the list of sections in the [dissection] on which the player dissecting disagree with. @@ -480,18 +483,18 @@ module Dissection = struct match new_dissection with | None -> return (final_dissection ~our_states dissection) | Some (new_dissection, start_chunk, stop_chunk) -> - valid_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - new_dissection) + return + @@ valid_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + new_dissection) (** Truncate a [dissection] and expect the {!Sc_rollup_game_repr.check_dissection} to fail with an invalid number of sections, where [expected_number_of_sections] is expected. *) let truncate_and_check_error dissection start_chunk stop_chunk default_number_of_sections expected_number_of_sections = - let open Lwt_syntax in let truncated_dissection = match dissection with | x :: _ :: z :: rst -> x :: z :: rst @@ -499,19 +502,19 @@ module Dissection = struct (* If the dissection is valid, this case can not be reached. *) assert false in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - truncated_dissection - in let expected_len = Z.of_int expected_number_of_sections in let expected_reason = Game.Dissection_number_of_sections_mismatch {expected = expected_len; given = Z.pred expected_len} in - return (expect_invalid_move expected_reason res) + assert_fails_with + ~__LOC__ + (Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + truncated_dissection) + expected_reason (** Test that if a dissection is smaller than the default number of sections, the length is equal to (distance + 1) of the dissected @@ -605,25 +608,21 @@ module Dissection = struct stop_chunk, default_number_of_sections, new_state_hash ) -> - let open Lwt_syntax in (* Check that we can not change the start hash. *) let dissection_with_different_start = modify_start (fun chunk -> Game.{chunk with state_hash = Some new_state_hash}) dissection in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - dissection_with_different_start - in - let expected_reason = - Game.Dissection_start_hash_mismatch - {expected = start_chunk.state_hash; given = Some new_state_hash} - in - return (expect_invalid_move expected_reason res)) + assert_fails_with + ~__LOC__ + (Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + dissection_with_different_start) + (Game.Dissection_start_hash_mismatch + {expected = start_chunk.state_hash; given = Some new_state_hash})) (** Test that we can not produce a dissection that agrees with the stop hash. Otherwise, there would be nothing to dispute. *) @@ -653,24 +652,14 @@ module Dissection = struct dissection in let stop_chunk = Game.{stop_chunk with state_hash = stop_hash} in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - invalid_dissection - in - let expected_reason = - Game.Dissection_stop_hash_mismatch stop_hash - (* match stop_hash with - * | None -> "The stop hash should not be None." - * | Some stop -> - * Format.asprintf - * "The stop hash should not be equal to %a" - * State_hash.pp - * stop *) - in - return (expect_invalid_move expected_reason res) + assert_fails_with + ~__LOC__ + (Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + invalid_dissection) + (Game.Dissection_stop_hash_mismatch stop_hash) in let* b1 = check_failure_on_same_stop_hash None in let* b2 = check_failure_on_same_stop_hash stop_chunk.state_hash in @@ -698,7 +687,7 @@ module Dissection = struct return (new_dissection, start_chunk, stop_chunk, number_of_sections)) (fun (dissection, start_chunk, stop_chunk, default_number_of_sections) -> let open Lwt_syntax in - let expected_reason dissection = + let expected_error dissection = match (List.hd dissection, List.last_opt dissection) with | Some Game.{tick = a_tick; _}, Some {tick = b_tick; _} -> Game.Dissection_edge_ticks_mismatch @@ -716,15 +705,15 @@ module Dissection = struct (fun chunk -> Game.{chunk with tick = Tick.next chunk.tick}) dissection in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - invalid_dissection - in - let expected_reason = expected_reason invalid_dissection in - return (expect_invalid_move expected_reason res) + let expected_error = expected_error invalid_dissection in + assert_fails_with + ~__LOC__ + (Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + invalid_dissection) + expected_error in (* We modify the start tick and expect the failure. *) let* b1 = modify_tick modify_start dissection in @@ -769,7 +758,6 @@ module Dissection = struct stop_chunk, default_number_of_sections, picked_section ) -> - let open Lwt_syntax in (* We put a distance of [1] in every section. Then, we put the distance's left in the [picked_section], it will create an invalid section. *) @@ -800,16 +788,14 @@ module Dissection = struct let invalid_dissection = replace_distances start_chunk.tick picked_section dissection in - let* res = - Game.Internal_for_tests.check_dissection - ~default_number_of_sections - ~start_chunk - ~stop_chunk - invalid_dissection - in - let expected_reason = Game.Dissection_invalid_distribution in - - return (expect_invalid_move expected_reason res)) + assert_fails_with + ~__LOC__ + (Game.Internal_for_tests.check_dissection + ~default_number_of_sections + ~start_chunk + ~stop_chunk + invalid_dissection) + Game.Dissection_invalid_distribution) let tests = ( "Dissection", @@ -1405,10 +1391,7 @@ let play_until_game_result ~refuter_client ~defender_client ~rollup block = play ~player_turn:opponent ~opponent:player_turn block | Ended (Loser {reason = _; loser}) as game_result -> let () = - Format.printf - "@,ending result: %a@," - Sc_rollup.Game.pp_status - game_result + Format.printf "@,ending result: %a@," Game.pp_status game_result in if loser = Account.pkh_of_contract_exn refuter_client.player.contract then return Defender_wins diff --git a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml index 033c0b553952..d73bc654a56f 100644 --- a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml +++ b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml @@ -40,21 +40,10 @@ module R = Sc_rollup_refutation_storage module G = Sc_rollup_game_repr module Tick = Sc_rollup_tick_repr -let check_reason ~loc (game_result : Sc_rollup_game_repr.game_result option) s = - match game_result with - | Some (Loser {reason; _}) -> ( - match reason with - | Conflict_resolved -> assert false - | Timeout -> assert false - | Invalid_move r -> - Assert.equal - ~loc - String.equal - "Compare invalid_move reasons" - Format.pp_print_string - (Format.asprintf "%a" G.pp_invalid_move r) - (Format.asprintf "%a" G.pp_invalid_move s)) - | _ -> assert false +(** Assert that the computation fails with the given error. *) +let assert_fails_with ~__LOC__ k expected_err = + let*! res = k in + Assert.proto_error ~loc:__LOC__ res (( = ) expected_err) let tick_of_int_exn n = match Tick.of_int n with None -> assert false | Some t -> t @@ -164,10 +153,10 @@ let test_poorly_distributed_dissection () = Constants_storage.sc_rollup_number_of_sections_in_dissection ctxt in let move = init_refutation ~size ~init_tick start_hash in - let* game_result, _ctxt = - T.lift @@ R.game_move ctxt rollup ~player:refuter ~opponent:defender move - in - check_reason ~loc:__LOC__ game_result Dissection_invalid_distribution + assert_fails_with + ~__LOC__ + (T.lift @@ R.game_move ctxt rollup ~player:refuter ~opponent:defender move) + Sc_rollup_game_repr.Dissection_invalid_distribution let test_single_valid_game_move () = let* ctxt, rollup, refuter, defender = two_stakers_in_conflict () in -- GitLab From 25297e3c038a6a831ed92e71129e409dd9adc994 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Tue, 20 Sep 2022 09:45:20 +0200 Subject: [PATCH 2/4] Scoru,Proto: reason is always [Conflict_resolved] during a move --- src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml index 16dff6619cc9..e0d8376cc39d 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml @@ -1051,9 +1051,9 @@ let loser_of_results ~alice_result ~bob_result = let play ~stakers game refutation = let open Lwt_tzresult_syntax in - let mk_loser reason loser = + let mk_loser loser = let loser = Index.staker stakers loser in - Either.Left (Loser {loser; reason}) + Either.Left (Loser {loser; reason = Conflict_resolved}) in match (refutation.step, game.game_state) with | Dissection states, Dissecting {dissection; default_number_of_sections} -> @@ -1088,8 +1088,7 @@ let play ~stakers game refutation = let*! player_result = validity_first_final_move ~proof ~game ~start_chunk ~stop_chunk in - if player_result then - return @@ mk_loser Conflict_resolved (opponent game.turn) + if player_result then return @@ mk_loser (opponent game.turn) else let new_game_state = let agreed_start_chunk = start_chunk in @@ -1118,7 +1117,7 @@ let play ~stakers game refutation = (* If we play when the final move started, the opponent provided a invalid proof. So if the defender manages to provide a valid proof, he wins. *) - return @@ mk_loser Conflict_resolved (opponent game.turn) + return @@ mk_loser (opponent game.turn) else return (Either.Left Draw) module Internal_for_tests = struct -- GitLab From df36e636f233857909e42c7d73834a76964152be Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Tue, 20 Sep 2022 14:41:47 +0200 Subject: [PATCH 3/4] Scoru,Test: simplify and fix unit test --- .../test/unit/test_sc_rollup_game.ml | 167 +++++++----------- 1 file changed, 67 insertions(+), 100 deletions(-) diff --git a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml index d73bc654a56f..9561dcc8ab0d 100644 --- a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml +++ b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml @@ -185,115 +185,82 @@ let test_single_valid_game_move () = in Assert.is_none ~loc:__LOC__ ~pp:Sc_rollup_game_repr.pp_game_result game_result -(* In order to test that a staker cannot play two refutation games at once (see - {!Sc_rollup_refutation_storage}), we first create a situation where a - defender is up against a refuter. This test should pass. - Then we initiate the same configuration, but with another refuter playing - against the same defender. This test should fail. - Note that the first test where everything goes right is not mandatory: we - will check that the error raised in the second test is exactly - [Sc_rollup_staker_in_game], so this should be enough to verify the property. - However, having a successful test can help us understand what went wrong if - the tests don't pass after some code modifications. - - First, the function below creates a context with three stakers: one - defender and two refuters. But the second refuter will play only if the - [refuter2_plays] boolean parameter below is [true]. - Then, the function is instantiated twice (with [refuter2_plays] set to - [false] and then to [true]) in order to create the tests described above. *) -let staker_injectivity_gen ~refuter2_plays = +(** Test that a staker can be part of at most one refutation game. *) +let test_staker_injectivity () = + let open Lwt_result_syntax in (* Create the defender and the two refuters. *) - let+ ctxt, rollup, genesis_hash, refuter1, refuter2, defender = + let* ctxt, rollup, genesis_hash, refuter1, refuter2, operator = T.originate_rollup_and_deposit_with_three_stakers () in - let res = - (* Create and publish four commits: - * [commit1]: the base commit published by [defender] and that everybody + (* Create and publish four commits: + - [commit1]: the base commit published by [operator] and that everybody agrees on; - and then three commits whose [commit1] is the predecessor and that will - be challenged in the refutation game: - * [commit2]: published by [defender]; - * [commit3]: published by [refuter1]; - * [commit4]: published by [refuter2]. *) - let hash1 = hash_string "foo" in - let hash2 = hash_string "bar" in - let hash3 = hash_string "xyz" in - let hash4 = hash_string "abc" in - let size = - Constants_storage.sc_rollup_number_of_sections_in_dissection ctxt - in - let refutation = init_refutation ~size hash1 in - let commit1 = - Commitment_repr. - { - predecessor = genesis_hash; - inbox_level = T.valid_inbox_level ctxt 1l; - number_of_ticks = T.number_of_ticks_exn 152231L; - compressed_state = hash1; - } - in - let* c1_hash, _, ctxt = - T.lift - @@ Sc_rollup_stake_storage.Internal_for_tests.refine_stake - ctxt - rollup - defender - commit1 - in - let challenging_commit compressed_state = - Commitment_repr. - { - predecessor = c1_hash; - inbox_level = T.valid_inbox_level ctxt 2l; - number_of_ticks = T.number_of_ticks_exn 10000L; - compressed_state; - } - in - let commit2 = challenging_commit hash2 in - let commit3 = challenging_commit hash3 in - let commit4 = challenging_commit hash4 in - (* Publish the commits. *) - let publish_commitment ctxt staker commit = - let+ _, _, ctxt, _ = - T.lift - @@ Sc_rollup_stake_storage.publish_commitment ctxt rollup staker commit - in - ctxt - in - let* ctxt = publish_commitment ctxt defender commit2 in - let* ctxt = publish_commitment ctxt refuter1 commit3 in - let* ctxt = publish_commitment ctxt refuter2 commit4 in - (* Start the games. [refuter2] plays only if [refuter2_plays] is [true]. *) - let game_move ctxt ~player ~opponent = - let* ctxt = T.lift @@ R.start_game ctxt rollup ~player ~opponent in - let+ _, ctxt = - T.lift @@ R.game_move ctxt rollup ~player ~opponent refutation - in + and then three commits whose [commit1] is the predecessor and that will + be challenged in the refutation game: + - [commit2]: published by [operator]; + - [commit3]: published by [refuter1]; + - [commit4]: published by [refuter2]. + *) + let hash1 = hash_string "foo" in + let hash2 = hash_string "bar" in + let hash3 = hash_string "xyz" in + let hash4 = hash_string "abc" in + let agreed_commit = + Commitment_repr. + { + predecessor = genesis_hash; + inbox_level = T.valid_inbox_level ctxt 1l; + number_of_ticks = T.number_of_ticks_exn 152231L; + compressed_state = hash1; + } + in + let* c1_hash, _, ctxt = + T.lift + @@ Sc_rollup_stake_storage.Internal_for_tests.refine_stake + ctxt + rollup + operator + agreed_commit + in + let challenging_commit compressed_state = + Commitment_repr. + { + predecessor = c1_hash; + inbox_level = T.valid_inbox_level ctxt 2l; + number_of_ticks = T.number_of_ticks_exn 10000L; + compressed_state; + } + in + let* ctxt = + List.fold_left_es + (fun ctxt (hash, player) -> + let commit = challenging_commit hash in + let* _, _, ctxt, _ = + T.lift + @@ Sc_rollup_stake_storage.publish_commitment + ctxt + rollup + player + commit + in + return ctxt) ctxt - in - let* ctxt = game_move ctxt ~player:refuter1 ~opponent:defender in - let+ _ctxt = - if refuter2_plays then game_move ctxt ~player:refuter2 ~opponent:defender - else return ctxt - in - () + [(hash2, operator); (hash3, refuter1); (hash4, refuter2)] + in + (* We start a game [operator <-> refuter1], it succeeds, neither of them + were in a game before. *) + let* ctxt = + T.lift @@ R.start_game ctxt rollup ~player:operator ~opponent:refuter1 + in + (* We start a game [operator <-> refuter2], it must fail as [operator] is + already playing against [refuter1]. *) + let*! res = + T.lift @@ R.start_game ctxt rollup ~player:operator ~opponent:refuter2 in - (refuter1, refuter2, defender, res) - -(** Test that a staker can be part of at most one refutation game. *) -let test_staker_injectivity () = - (* Test that it's OK to have three stakers where only two of them are - playing. *) - let* _ = staker_injectivity_gen ~refuter2_plays:false in - (* Test that an error is triggered if a defender plays against two refuters at - once. *) - let* _, _, defender, res = staker_injectivity_gen ~refuter2_plays:true in - let open Lwt_syntax in - let* res = res in Assert.proto_error ~loc:__LOC__ res - (( = ) (Sc_rollup_errors.Sc_rollup_staker_in_game (`Defender defender))) + (( = ) (Sc_rollup_errors.Sc_rollup_staker_in_game (`Refuter operator))) module Arith_pvm = Sc_rollup_helpers.Arith_pvm -- GitLab From eab02353551d2c77600babcff54df3f2975192b9 Mon Sep 17 00:00:00 2001 From: Valentin Chaboche Date: Wed, 21 Sep 2022 11:34:45 +0200 Subject: [PATCH 4/4] Scoru,Tezt: use timeout instead of invalid move --- ...o cement not on top of LCC or disputed.out | 42 +++++++++++++++++-- tezt/tests/sc_rollup.ml | 39 ++++++++--------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/tezt/tests/expected/sc_rollup.ml/Alpha- commitments- publish- and try to cement not on top of LCC or disputed.out b/tezt/tests/expected/sc_rollup.ml/Alpha- commitments- publish- and try to cement not on top of LCC or disputed.out index 6ec3269fb16d..715f6af59ecc 100644 --- a/tezt/tests/expected/sc_rollup.ml/Alpha- commitments- publish- and try to cement not on top of LCC or disputed.out +++ b/tezt/tests/expected/sc_rollup.ml/Alpha- commitments- publish- and try to cement not on top of LCC or disputed.out @@ -465,13 +465,47 @@ This simulation failed: Error: Parent is not the last cemented commitment. +./octez-client --wait none timeout dispute on sc rollup '[SC_ROLLUP_HASH]' with '[PUBLIC_KEY_HASH]' from bootstrap1 +Node is bootstrapped. +Estimated gas: 8210.876 units (will add 100 for safety) +Estimated storage: no bytes added +Operation successfully injected in the node. +Operation hash is '[OPERATION_HASH]' +NOT waiting for the operation to be included. +Use command + octez-client wait for [OPERATION_HASH] to be included --confirmations 1 --branch [BLOCK_HASH] +and/or an external block explorer to make sure that it has been included. +This sequence of operations was run: + Manager signed operations: + From: [PUBLIC_KEY_HASH] + Fee to the baker: ꜩ0.001139 + Expected counter: 8 + Gas limit: 8311 + Storage limit: 0 bytes + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ0.001139 + payload fees(the block proposer) ....... +ꜩ0.001139 + Smart contract rollup refutation timeout: + Address: [SC_ROLLUP_HASH] + First staker (Alice): [PUBLIC_KEY_HASH] + Second staker (Bob): [PUBLIC_KEY_HASH] + This smart contract rollup refutation timeout was successfully applied + Consumed gas: 8210.876 + Refutation game status: Game ended: [PUBLIC_KEY_HASH] lost because: timeout + Balance updates: + Frozen_bonds([PUBLIC_KEY_HASH],[SC_ROLLUP_HASH]) ... -ꜩ10000 + sc rollup refutation punishments ........................................................... +ꜩ10000 + sc rollup refutation rewards ............................................................... -ꜩ5000 + [PUBLIC_KEY_HASH] ....................................................... +ꜩ5000 + + ./octez-client --wait none cement commitment '[SC_ROLLUP_COMMITMENT_HASH]' from bootstrap1 for sc rollup '[SC_ROLLUP_HASH]' Node is bootstrapped. This simulation failed: Manager signed operations: From: [PUBLIC_KEY_HASH] Fee to the baker: ꜩ0 - Expected counter: 8 + Expected counter: 9 Gas limit: 1040000 Storage limit: 60000 bytes Smart contract rollup commitment cementing: @@ -488,7 +522,7 @@ This simulation failed: Manager signed operations: From: [PUBLIC_KEY_HASH] Fee to the baker: ꜩ0 - Expected counter: 8 + Expected counter: 9 Gas limit: 1040000 Storage limit: 60000 bytes Smart contract rollup commitment cementing: @@ -513,7 +547,7 @@ This sequence of operations was run: Manager signed operations: From: [PUBLIC_KEY_HASH] Fee to the baker: ꜩ0.000811 - Expected counter: 8 + Expected counter: 9 Gas limit: 5136 Storage limit: 0 bytes Balance updates: @@ -541,7 +575,7 @@ This sequence of operations was run: Manager signed operations: From: [PUBLIC_KEY_HASH] Fee to the baker: ꜩ0.000834 - Expected counter: 9 + Expected counter: 10 Gas limit: 5366 Storage limit: 0 bytes Balance updates: diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 44240d3c9a72..9fc78d7ed835 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -2707,6 +2707,18 @@ let test_forking_scenario ~title ~scenario protocols = let cement_commitments client sc_rollup ?fail = Lwt_list.iter_s (fun hash -> cement_commitment client ~sc_rollup ~hash ?fail) +let timeout ?expect_failure ~sc_rollup ~staker client = + let*! () = + Client.Sc_rollup.timeout + ~hooks + ~dst:sc_rollup + ~src:"bootstrap1" + ~staker + client + ?expect_failure + in + Client.bake_for_and_wait client + (** Given a commitment tree constructed by {test_forking_scenario}, this function: - tests different (failing and non-failing) cementation of commitments and checks the returned error for each situation (in case of failure); @@ -2760,18 +2772,13 @@ let test_no_cementation_if_parent_not_lcc_or_if_disputed_commit protocols = @@ M.make ~source:operator2 @@ M.sc_rollup_refute ~sc_rollup ~opponent:operator1.public_key_hash () in - (* [operator1] makes a dissection. it will lose here because the dissection - is ill-formed. *) - let refutation = M.{choice_tick = 0; refutation_step = Dissection []} in + (* [operator1] will not play and will be timeout-ed. *) + let timeout_period = constants.timeout_period_in_blocks in let* () = - bake_operation_via_rpc client - @@ M.make ~source:operator2 - @@ M.sc_rollup_refute - ~sc_rollup - ~opponent:operator1.public_key_hash - ~refutation - () + repeat (timeout_period + 1) (fun () -> Client.bake_for_and_wait client) in + (* He even timeout himself, what a shame. *) + let* () = timeout ~sc_rollup ~staker:operator1.public_key_hash client in (* Attempting to cement defeated branch will fail. *) let* () = cement ~fail:commit_doesnt_exit [c32; c321] in (* Now, we can cement c31 on top of c2 and c311 on top of c31. *) @@ -2860,18 +2867,6 @@ let test_late_rollup_node = let* _status = Sc_rollup_node.wait_for_level ~timeout:2. sc_rollup_node 95 in return () -let timeout ?expect_failure ~sc_rollup ~staker client = - let*! () = - Client.Sc_rollup.timeout - ~hooks - ~dst:sc_rollup - ~src:"bootstrap1" - ~staker - client - ?expect_failure - in - Client.bake_for_and_wait client - (* Testing the timeout to record gas consumption in a regression trace and detect when the value changes. For functional tests on timing-out a dispute, see unit tests in -- GitLab