diff --git a/src/proto_alpha/lib_client/operation_result.ml b/src/proto_alpha/lib_client/operation_result.ml index cb3f1d2682de16bad735b166e8bf7e29d5e28837..fec54f0f6169cc3e80ce4779ffe011c0dd151a12 100644 --- a/src/proto_alpha/lib_client/operation_result.ml +++ b/src/proto_alpha/lib_client/operation_result.ml @@ -309,17 +309,18 @@ let pp_manager_operation_content (type kind) source pp_result ppf commitment Sc_rollup.Address.pp rollup - | Sc_rollup_refute {rollup; opponent; refutation} -> + | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} -> Format.fprintf ppf "Refute staker %a in the smart contract rollup at address %a using \ - refutation %a" + refutation %a %s@]" Sc_rollup.Staker.pp opponent Sc_rollup.Address.pp rollup Sc_rollup.Game.pp_refutation refutation + (if is_opening_move then "(opening move of game)" else "") | Sc_rollup_timeout {rollup; stakers} -> Format.fprintf ppf diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index f2916423eb7063b2da3e4e66a43b0824c38932eb..a50a584081df62f66823dd553396e441a807cc90 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -2883,12 +2883,13 @@ module Sc_rollup : sig module Refutation_storage : sig type conflict_point = Commitment.Hash.t * Commitment.Hash.t - val update_game : + val game_move : context -> t -> player:Staker.t -> opponent:Staker.t -> Game.refutation -> + is_opening_move:bool -> (Game.outcome option * context) tzresult Lwt.t val timeout : @@ -3430,6 +3431,7 @@ and _ manager_operation = rollup : Sc_rollup.t; opponent : Sc_rollup.Staker.t; refutation : Sc_rollup.Game.refutation; + is_opening_move : bool; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { diff --git a/src/proto_alpha/lib_protocol/apply.ml b/src/proto_alpha/lib_protocol/apply.ml index 9bfb36220003988bf64002a562e52d86656e02ca..8aaa58486a0b484b1bf8ccb93820886de0f913e2 100644 --- a/src/proto_alpha/lib_protocol/apply.ml +++ b/src/proto_alpha/lib_protocol/apply.ml @@ -1805,13 +1805,14 @@ let apply_external_manager_operation_content : {staked_hash; consumed_gas; published_at_level; balance_updates} in return (ctxt, result, []) - | Sc_rollup_refute {rollup; opponent; refutation} -> - Sc_rollup.Refutation_storage.update_game + | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} -> + Sc_rollup.Refutation_storage.game_move ctxt rollup ~player:source ~opponent refutation + ~is_opening_move >>=? fun (outcome, ctxt) -> (match outcome with | None -> return (Sc_rollup.Game.Ongoing, ctxt, []) diff --git a/src/proto_alpha/lib_protocol/operation_repr.ml b/src/proto_alpha/lib_protocol/operation_repr.ml index 616000da1368cca3aa11e93c327e4ab7973e832f..52bbeda71d5f20382f63f909291d931e2feee7d4 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.ml +++ b/src/proto_alpha/lib_protocol/operation_repr.ml @@ -406,6 +406,7 @@ and _ manager_operation = rollup : Sc_rollup_repr.t; opponent : Sc_rollup_repr.Staker.t; refutation : Sc_rollup_game_repr.refutation; + is_opening_move : bool; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { @@ -1041,20 +1042,22 @@ module Encoding = struct tag = sc_rollup_operation_refute_tag; name = "sc_rollup_refute"; encoding = - obj3 + obj4 (req "rollup" Sc_rollup_repr.encoding) (req "opponent" Sc_rollup_repr.Staker.encoding) - (req "refutation" Sc_rollup_game_repr.refutation_encoding); + (req "refutation" Sc_rollup_game_repr.refutation_encoding) + (req "is_opening_move" bool); select = (function | Manager (Sc_rollup_refute _ as op) -> Some op | _ -> None); proj = (function - | Sc_rollup_refute {rollup; opponent; refutation} -> - (rollup, opponent, refutation)); + | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} + -> + (rollup, opponent, refutation, is_opening_move)); inj = - (fun (rollup, opponent, refutation) -> - Sc_rollup_refute {rollup; opponent; refutation}); + (fun (rollup, opponent, refutation, is_opening_move) -> + Sc_rollup_refute {rollup; opponent; refutation; is_opening_move}); } let[@coq_axiom_with_reason "gadt"] sc_rollup_timeout_case = diff --git a/src/proto_alpha/lib_protocol/operation_repr.mli b/src/proto_alpha/lib_protocol/operation_repr.mli index 7805ce8ec19d61ce60e9ef4e1fa12d0396068f1d..d17d5d3d5b6ab17c77d54d11e7fdf23c441df4f5 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.mli +++ b/src/proto_alpha/lib_protocol/operation_repr.mli @@ -472,6 +472,7 @@ and _ manager_operation = rollup : Sc_rollup_repr.t; opponent : Sc_rollup_repr.Staker.t; refutation : Sc_rollup_game_repr.refutation; + is_opening_move : bool; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { diff --git a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml index 61bae4116c434ef4bee245356100b98699b20a1b..9509f468a14dd6acc4cbc8e7b84f62fb75bf6e53 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml @@ -41,6 +41,7 @@ type error += Sc_rollup_commitment_repr.Hash.t | (* `Temporary *) Sc_rollup_bad_inbox_level | (* `Temporary *) Sc_rollup_max_number_of_available_messages_reached + | (* `Temporary *) Sc_rollup_game_already_started | (* `Temporary *) Sc_rollup_wrong_turn | (* `Temporary *) Sc_rollup_no_game | (* `Temporary *) Sc_rollup_staker_in_game @@ -85,11 +86,25 @@ let () = Data_encoding.unit (function Sc_rollup_timeout_level_not_reached -> Some () | _ -> None) (fun () -> Sc_rollup_timeout_level_not_reached) ; + let description = + "Refutation game already started, must play with is_opening_move = false." + in + register_error_kind + `Temporary + ~id:"Sc_rollup_game_already_started" + ~title:"Refutation game already started" + ~description + ~pp:(fun ppf () -> Format.fprintf ppf "%s" description) + Data_encoding.unit + (function Sc_rollup_game_already_started -> Some () | _ -> None) + (fun () -> Sc_rollup_game_already_started) ; + let description = "Refutation game does not exist" in register_error_kind `Temporary ~id:"Sc_rollup_no_game" ~title:"Refutation game does not exist" - ~description:"Refutation game does not exist" + ~description + ~pp:(fun ppf () -> Format.fprintf ppf "%s" description) Data_encoding.unit (function Sc_rollup_no_game -> Some () | _ -> None) (fun () -> Sc_rollup_no_game) ; 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 0cd38e45b8adfe849ddc8c9ab3421f360ddf45e0..c07fc2b390adb3230eba1a5b411ab49649284690 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml @@ -136,14 +136,20 @@ let get_conflict_point ctxt rollup staker1 staker2 = in traverse_in_parallel ctxt commit1 commit2 -(** [get_or_init_game ctxt rollup refuter defender] returns the current - game between the two stakers [refuter] and [defender] if it exists. +let get_game ctxt rollup stakers = + let open Lwt_tzresult_syntax in + let stakers = Sc_rollup_game_repr.Index.normalize stakers in + let* ctxt, game = Store.Game.find (ctxt, rollup) stakers in + match game with Some g -> return (g, ctxt) | None -> fail Sc_rollup_no_game + +(** [init_game ctxt rollup refuter defender] initialises the game or + if it already exists fails with `Sc_rollup_game_already_started`. - If it does not already exist, it creates one with [refuter] as the - first player to move. The initial state of the game will be obtained - from the commitment pair belonging to [defender] at the conflict - point. See [Sc_rollup_game_repr.initial] for documentation on how a - pair of commitments is turned into an initial game state. + The game is created with `refuter` as the first player to move. The + initial state of the game will be obtained from the commitment pair + belonging to [defender] at the conflict point. See + [Sc_rollup_game_repr.initial] for documentation on how a pair of + commitments is turned into an initial game state. This also deals with the other bits of data in the storage around the game. It checks neither staker is already in a game (and also @@ -168,12 +174,12 @@ let get_conflict_point ctxt rollup staker1 staker2 = {li [Sc_rollup_staker_in_game] if one of the [refuter] or [defender] is already playing a game} } *) -let get_or_init_game ctxt rollup ~refuter ~defender = +let init_game ctxt rollup ~refuter ~defender = let open Lwt_tzresult_syntax in let stakers = Sc_rollup_game_repr.Index.normalize (refuter, defender) in let* ctxt, game = Store.Game.find (ctxt, rollup) stakers in match game with - | Some g -> return (g, ctxt) + | Some _ -> fail Sc_rollup_game_already_started | None -> let* ctxt, opp_1 = Store.Opponent.find (ctxt, rollup) refuter in let* ctxt, opp_2 = Store.Opponent.find (ctxt, rollup) defender in @@ -208,11 +214,13 @@ let get_or_init_game ctxt rollup ~refuter ~defender = let* ctxt, _ = Store.Opponent.init (ctxt, rollup) defender refuter in return (game, ctxt) -let update_game ctxt rollup ~player ~opponent refutation = +let game_move ctxt rollup ~player ~opponent refutation ~is_opening_move = let open Lwt_tzresult_syntax in let alice, bob = Sc_rollup_game_repr.Index.normalize (player, opponent) in let* game, ctxt = - get_or_init_game ctxt rollup ~refuter:player ~defender:opponent + if is_opening_move then + init_game ctxt rollup ~refuter:player ~defender:opponent + else get_game ctxt rollup (alice, bob) in let* () = fail_unless diff --git a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli index d0cb532463ef0d2cb42938da35c87597ba951aae..44a9bdcd1e96199dc42a2a0581b6d429f82d3b2b 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli @@ -28,11 +28,12 @@ module Commitment_hash = Sc_rollup_commitment_repr.Hash type conflict_point = Commitment_hash.t * Commitment_hash.t -(** [update_game ctxt rollup player opponent refutation] handles the - storage-side logic for when one of the players makes a move in the - game. It initializes the game if necessary (the first move looks - much like any other). It checks that [player] is the player whose - turn it is; if so, it applies [refutation] using the [play] function. +(** [game_move ctxt rollup player opponent refutation is_opening_move] + handles the storage-side logic for when one of the players makes a + move in the game. It initializes the game if [is_opening_move] is + [true]. Otherwise, it checks the game already exists. Then it checks + that [player] is the player whose turn it is; if so, it applies + [refutation] using the [play] function. If the result is a new game, this is stored and the timeout level is updated. @@ -42,6 +43,10 @@ type conflict_point = Commitment_hash.t * Commitment_hash.t May fail with: {ul {li [Sc_rollup_does_not_exist] if [rollup] does not exist} + {li [Sc_rollup_no_game] if [is_opening_move] is [false] but the + game does not exist} + {li [Sc_rollup_game_already_started] if [is_opening_move] is [true] + but the game already exists} {li [Sc_rollup_no_conflict] if [player] is staked on an ancestor of the commitment staked on by [opponent], or vice versa} {li [Sc_rollup_not_staked] if one of the [player] or [opponent] is @@ -50,13 +55,23 @@ type conflict_point = Commitment_hash.t * Commitment_hash.t is already playing a game} {li [Sc_rollup_wrong_turn] if a player is trying to move out of turn} - } *) -val update_game : + } + + The [is_opening_move] argument is included here to make sure that an + operation intended to start a refutation game is never mistaken for + an operation to play the second move of the game---this may + otherwise happen due to non-deterministic ordering of L1 operations. + With the [is_opening_move] parameter, the worst case is that the + operation simply fails. Without it, the operation would be mistaken + for an invalid move in the game and the staker would lose their + stake! *) +val game_move : Raw_context.t -> Sc_rollup_repr.t -> player:Sc_rollup_repr.Staker.t -> opponent:Sc_rollup_repr.Staker.t -> Sc_rollup_game_repr.refutation -> + is_opening_move:bool -> (Sc_rollup_game_repr.outcome option * Raw_context.t) tzresult Lwt.t (* TODO: #2902 update reference to timeout period in doc-string *) @@ -88,7 +103,7 @@ val timeout : (Sc_rollup_game_repr.outcome * Raw_context.t) tzresult Lwt.t (** [apply_outcome ctxt rollup outcome] takes an [outcome] produced - by [timeout] or [update_game] and performs the necessary end-of-game + by [timeout] or [game_move] and performs the necessary end-of-game cleanup: remove the game itself from the store and punish the losing player by removing their stake. @@ -98,7 +113,7 @@ val timeout : This is mostly just calling [remove_staker], so it can fail with the same errors as that. However, if it is called on an [outcome] - generated by [update_game] or [timeout] it should not fail. + generated by [game_move] or [timeout] it should not fail. Note: this function takes the two stakers as a pair rather than separate arguments. This reflects the fact that for this function diff --git a/tezt/tests/expected/RPC_test.ml/Alpha- (mode client) RPC regression tests- mempool.out b/tezt/tests/expected/RPC_test.ml/Alpha- (mode client) RPC regression tests- mempool.out index 8b6d3fb8929e555161ec7b0b8ab8e22bb28530fb..a7cc3cb3d923bfa41163900949098971653b7efd 100644 --- a/tezt/tests/expected/RPC_test.ml/Alpha- (mode client) RPC regression tests- mempool.out +++ b/tezt/tests/expected/RPC_test.ml/Alpha- (mode client) RPC regression tests- mempool.out @@ -5194,9 +5194,13 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "choice" ], "additionalProperties": false + }, + "is_opening_move": { + "type": "boolean" } }, "required": [ + "is_opening_move", "refutation", "opponent", "rollup", @@ -9318,6 +9322,17 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "kind": "Dynamic" }, "kind": "named" + }, + { + "name": "is_opening_move", + "layout": { + "kind": "Bool" + }, + "data_kind": { + "size": 1, + "kind": "Float" + }, + "kind": "named" } ], "name": "Sc_rollup_refute" @@ -19627,9 +19642,13 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "choice" ], "additionalProperties": false + }, + "is_opening_move": { + "type": "boolean" } }, "required": [ + "is_opening_move", "refutation", "opponent", "rollup", diff --git a/tezt/tests/expected/RPC_test.ml/Alpha- (mode proxy) RPC regression tests- mempool.out b/tezt/tests/expected/RPC_test.ml/Alpha- (mode proxy) RPC regression tests- mempool.out index fb565a721fbe05b3a9c754d7a824714eac494dc4..f053ea818a28d24709d449533a79f0888dde7c84 100644 --- a/tezt/tests/expected/RPC_test.ml/Alpha- (mode proxy) RPC regression tests- mempool.out +++ b/tezt/tests/expected/RPC_test.ml/Alpha- (mode proxy) RPC regression tests- mempool.out @@ -5215,9 +5215,13 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "choice" ], "additionalProperties": false + }, + "is_opening_move": { + "type": "boolean" } }, "required": [ + "is_opening_move", "refutation", "opponent", "rollup", @@ -9339,6 +9343,17 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "kind": "Dynamic" }, "kind": "named" + }, + { + "name": "is_opening_move", + "layout": { + "kind": "Bool" + }, + "data_kind": { + "size": 1, + "kind": "Float" + }, + "kind": "named" } ], "name": "Sc_rollup_refute" @@ -19648,9 +19663,13 @@ curl -s 'http://localhost:[PORT]/describe/chains/main/mempool?recurse=yes' "choice" ], "additionalProperties": false + }, + "is_opening_move": { + "type": "boolean" } }, "required": [ + "is_opening_move", "refutation", "opponent", "rollup",