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 5381bfbd8195865e526674c2fd2f1a526ca56c4c..7077617212618c55660f17e75fbcacc3053740d8 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml @@ -410,7 +410,13 @@ let check_dissection start start_tick stop stop_tick dissection = game_error "Cannot return to a Some state after being at a None state" | (_, tick) :: (next_state, next_tick) :: others -> if Sc_rollup_tick_repr.(tick < next_tick) then - traverse ((next_state, next_tick) :: others) + let incr = Sc_rollup_tick_repr.distance tick next_tick in + if Z.(leq incr (div dist (of_int 2))) then + traverse ((next_state, next_tick) :: others) + else + game_error + "Maximum tick increment in dissection must be less than half \ + total dissection length" else game_error "Ticks should only increase in dissection" | _ -> return () in 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 08b55eca9bd5fd17a9c1e51af121acaf5861bbe0..9da98f97f764db363762e9f67703b633bee4d6f9 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml @@ -200,11 +200,17 @@ let init_game ctxt rollup ~refuter ~defender = | Some refuter, Some defender -> fail (Sc_rollup_staker_in_game (`Both (refuter, defender))) in - let* ( ( {hash = _parent; commitment = parent_info}, - {hash = _child; commitment = child_info} ), + let* ( ( {hash = _refuter_commit; commitment = _info}, + {hash = _defender_commit; commitment = child_info} ), ctxt ) = get_conflict_point ctxt rollup refuter defender in + let* parent_info, ctxt = + Commitment_storage.get_commitment_unsafe + ctxt + rollup + child_info.predecessor + in let* ctxt, inbox = Store.Inbox.get ctxt rollup in let* kind = Store.PVM_kind.get ctxt rollup in let game = 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 91927e3f6a91fd4f86d478a399263b94e0c2aa6a..a4354a0d3839a88b02a5b91de56d2922a82a3084 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 @@ -106,7 +106,7 @@ let random_dissection start_at start_hash stop_at stop_hash : let stop_int = tick_to_int_exn stop_at in let dist = stop_int - start_int in let branch = min (dist + 1) 32 in - let size = (dist + 1) / branch in + let size = (dist + 1) / (branch - 1) in if dist = 1 then return None else @@ -505,7 +505,7 @@ module Strategies (PVM : TestPVM with type hash = State_hash.t) = struct if dist = 1 then return None else let branch = min (dist + 1) 32 in - let size = (dist + 1) / branch in + let size = (dist + 1) / (branch - 1) in let tick_list = Result.to_option @@ List.init branch ~when_negative_length:"error" (fun i -> @@ -902,25 +902,20 @@ let testDissection = [ Test.make ~name:"randomVPN" - (Gen.quad - (Gen.list_size Gen.small_int (Gen.int_range 0 100)) - Gen.small_int - Gen.small_int - Gen.small_int) - (fun (initial_prog, start_at, length, branching) -> + Gen.(triple (list_size small_int (int_range 0 100)) small_int small_int) + (fun (initial_prog, start_at, length) -> assume (start_at >= 0 && length > 1 - && List.length initial_prog > start_at + length - && 1 < branching) ; + && List.length initial_prog > start_at + length) ; let module P = MakeRandomPVM (struct let initial_prog = initial_prog end) in Lwt_main.run @@ test_random_dissection (module P) start_at length); Test.make ~name:"count" - (Gen.quad Gen.small_int Gen.small_int Gen.small_int Gen.small_int) - (fun (target, start_at, length, branching) -> - assume (start_at >= 0 && length > 1 && 1 < branching) ; + Gen.(triple small_int small_int small_int) + (fun (target, start_at, length) -> + assume (start_at >= 0 && length > 1) ; let module P = MakeCountingPVM (struct let target = target end) in @@ -932,7 +927,7 @@ let testRandomDissection = [ Test.make ~name:"randomdissection" - (Gen.pair Gen.small_int Gen.small_int) + Gen.(pair small_int small_int) (fun (start_int, length) -> assume (start_int > 0 && length >= 10) ; let testing_lwt = @@ -970,7 +965,7 @@ let () = "Refutation Game" [ ("Dissection tests", qcheck_wrap testDissection); - ("Random disection", qcheck_wrap testRandomDissection); + ("Random dissection", qcheck_wrap testRandomDissection); ( "RandomPVM", qcheck_wrap [ diff --git a/src/proto_alpha/lib_protocol/test/unit/main.ml b/src/proto_alpha/lib_protocol/test/unit/main.ml index 8e1f03660c1257ef48dbc8aab8aa728a9e106467..b99d723fcd8cca9fdcd10fc23d9e773e45d2650f 100644 --- a/src/proto_alpha/lib_protocol/test/unit/main.ml +++ b/src/proto_alpha/lib_protocol/test/unit/main.ml @@ -69,6 +69,7 @@ let () = Unit_test.spec "saturation arithmetic" Test_saturation.tests; Unit_test.spec "gas monad" Test_gas_monad.tests; Unit_test.spec "Sc_rollup_storage.ml" Test_sc_rollup_storage.tests; + Unit_test.spec "sc rollup game" Test_sc_rollup_game.tests; Unit_test.spec "tx rollup l2" Test_tx_rollup_l2.tests; Unit_test.spec "tx rollup l2 apply" Test_tx_rollup_l2_apply.tests; Unit_test.spec "liquidity baking" Test_liquidity_baking_repr.tests; 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 new file mode 100644 index 0000000000000000000000000000000000000000..b94a83f2583feda91294a3ff102bc4b014780402 --- /dev/null +++ b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_game.ml @@ -0,0 +1,186 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Trili Tech, *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Testing + ------- + Component: Protocol Sc_rollup_refutation_storage + Invocation: dune exec src/proto_alpha/lib_protocol/test/unit/main.exe \ + -- test "^\[Unit\] sc rollup game$" + Subject: Tests for the SCORU refutation game +*) + +open Protocol +open Lwt_result_syntax +module Commitment_repr = Sc_rollup_commitment_repr +module T = Test_sc_rollup_storage +module R = Sc_rollup_refutation_storage +module Tick = Sc_rollup_tick_repr + +let check_reason ~loc (outcome : Sc_rollup_game_repr.outcome option) s = + match outcome with + | None -> assert false + | Some o -> ( + match o.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 + r + s) + +let tick_of_int_exn n = + match Tick.of_int n with None -> assert false | Some t -> t + +let hash_int n = Sc_rollup_repr.State_hash.hash_string [Format.sprintf "%d" n] + +let two_stakers_in_conflict () = + let* ctxt, rollup, refuter, defender = + T.originate_rollup_and_deposit_with_two_stakers () + in + let hash1 = Sc_rollup_repr.State_hash.hash_string ["foo"] in + let hash2 = Sc_rollup_repr.State_hash.hash_string ["bar"] in + let hash3 = Sc_rollup_repr.State_hash.hash_string ["xyz"] in + let parent_commit = + Commitment_repr. + { + predecessor = Commitment_repr.Hash.zero; + inbox_level = T.valid_inbox_level ctxt 1l; + number_of_messages = T.number_of_messages_exn 5l; + number_of_ticks = T.number_of_ticks_exn 152231l; + compressed_state = hash1; + } + in + let* parent, _, ctxt = + T.lift + @@ Sc_rollup_stake_storage.Internal_for_tests.refine_stake + ctxt + rollup + defender + parent_commit + in + let child1 = + Commitment_repr. + { + predecessor = parent; + inbox_level = T.valid_inbox_level ctxt 2l; + number_of_messages = T.number_of_messages_exn 2l; + number_of_ticks = T.number_of_ticks_exn 10000l; + compressed_state = hash2; + } + in + let child2 = + Commitment_repr. + { + predecessor = parent; + inbox_level = T.valid_inbox_level ctxt 2l; + number_of_messages = T.number_of_messages_exn 2l; + number_of_ticks = T.number_of_ticks_exn 10000l; + compressed_state = hash3; + } + in + let* _, _, ctxt, _ = + T.lift + @@ Sc_rollup_stake_storage.publish_commitment ctxt rollup defender child1 + in + let* _, _, ctxt, _ = + T.lift + @@ Sc_rollup_stake_storage.publish_commitment ctxt rollup refuter child2 + in + return (ctxt, rollup, refuter, defender) + +(** A dissection is 'poorly distributed' if its tick counts are not + very evenly spread through the total tick-duration. Formally, the + maximum tick-distance between two consecutive states in a dissection + may not be more than half of the total tick-duration. *) +let test_poorly_distributed_dissection () = + let* ctxt, rollup, refuter, defender = two_stakers_in_conflict () in + let start_hash = Sc_rollup_repr.State_hash.hash_string ["foo"] in + let dissection = + Stdlib.List.init 32 (fun i -> + if i = 0 then (Some start_hash, tick_of_int_exn 0) + else if i = 31 then (None, tick_of_int_exn 10000) + else (Some (hash_int i), tick_of_int_exn i)) + in + let move = + Sc_rollup_game_repr. + {choice = Sc_rollup_tick_repr.initial; step = Dissection dissection} + in + let* outcome, _ctxt = + T.lift + @@ R.game_move + ctxt + rollup + ~player:refuter + ~opponent:defender + move + ~is_opening_move:true + in + let expected_reason = + "Maximum tick increment in dissection must be less than half total \ + dissection length" + in + check_reason ~loc:__LOC__ outcome expected_reason + +let test_single_valid_game_move () = + let* ctxt, rollup, refuter, defender = two_stakers_in_conflict () in + let start_hash = Sc_rollup_repr.State_hash.hash_string ["foo"] in + let dissection = + Stdlib.List.init 32 (fun i -> + if i = 0 then (Some start_hash, tick_of_int_exn 0) + else if i = 31 then (None, tick_of_int_exn 10000) + else (Some (hash_int i), tick_of_int_exn (i * 200))) + in + let move = + Sc_rollup_game_repr. + {choice = Sc_rollup_tick_repr.initial; step = Dissection dissection} + in + let* outcome, _ctxt = + T.lift + @@ R.game_move + ctxt + rollup + ~player:refuter + ~opponent:defender + move + ~is_opening_move:true + in + Assert.is_none ~loc:__LOC__ ~pp:Sc_rollup_game_repr.pp_outcome outcome + +let tests = + [ + Tztest.tztest + "A badly distributed dissection is an invalid move" + `Quick + test_poorly_distributed_dissection; + Tztest.tztest + "A single game move with a valid dissection" + `Quick + test_single_valid_game_move; + ]