diff --git a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml index 6f7245fb711641598c6c97f7e922a48d7d54916e..b716a3a793d0e0ee56c7e56279ac615f872994dd 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml @@ -108,34 +108,31 @@ let () = busy staker) Data_encoding.( - obj1 - (req - "staker_in_game" - (union - [ - case - (Tag 0) - ~title:"Refuter" - Signature.Public_key_hash.encoding - (function `Refuter sc -> Some sc | _ -> None) - (fun sc -> `Refuter sc); - case - (Tag 1) - ~title:"Defender" - Signature.Public_key_hash.encoding - (function `Defender sc -> Some sc | _ -> None) - (fun sc -> `Defender sc); - case - (Tag 2) - ~title:"Both" - (obj2 - (req "refuter" Signature.Public_key_hash.encoding) - (req "defender" Signature.Public_key_hash.encoding)) - (function - | `Both (refuter, defender) -> Some (refuter, defender) - | _ -> None) - (fun (refuter, defender) -> `Both (refuter, defender)); - ]))) + union + [ + case + (Tag 0) + ~title:"Refuter" + (obj1 (req "refuter" Signature.Public_key_hash.encoding)) + (function `Refuter sc -> Some sc | _ -> None) + (fun sc -> `Refuter sc); + case + (Tag 1) + ~title:"Defender" + (obj1 (req "defender" Signature.Public_key_hash.encoding)) + (function `Defender sc -> Some sc | _ -> None) + (fun sc -> `Defender sc); + case + (Tag 2) + ~title:"Both" + (obj2 + (req "refuter" Signature.Public_key_hash.encoding) + (req "defender" Signature.Public_key_hash.encoding)) + (function + | `Both (refuter, defender) -> Some (refuter, defender) + | _ -> None) + (fun (refuter, defender) -> `Both (refuter, defender)); + ]) (function Sc_rollup_staker_in_game x -> Some x | _ -> None) (fun x -> Sc_rollup_staker_in_game x) ; register_error_kind 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 9da98f97f764db363762e9f67703b633bee4d6f9..66e00aa3cca1cc8639b3544cb1822f66d3a93bce 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml @@ -193,11 +193,11 @@ let init_game ctxt rollup ~refuter ~defender = let* _ = match (opp_1, opp_2) with | None, None -> return () - | Some refuter, None -> + | Some _refuter_opponent, None -> fail (Sc_rollup_staker_in_game (`Refuter refuter)) - | None, Some defender -> + | None, Some _defender_opponent -> fail (Sc_rollup_staker_in_game (`Defender defender)) - | Some refuter, Some defender -> + | Some _refuter_opponent, Some _defender_opponent -> fail (Sc_rollup_staker_in_game (`Both (refuter, defender))) in let* ( ( {hash = _refuter_commit; commitment = _info}, 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 b94a83f2583feda91294a3ff102bc4b014780402..ebb14a1fa84a685cd774f3766414bc1ab451bdb0 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 @@ -37,6 +37,7 @@ open Lwt_result_syntax module Commitment_repr = Sc_rollup_commitment_repr module T = Test_sc_rollup_storage module R = Sc_rollup_refutation_storage +module G = Sc_rollup_game_repr module Tick = Sc_rollup_tick_repr let check_reason ~loc (outcome : Sc_rollup_game_repr.outcome option) s = @@ -60,6 +61,29 @@ let tick_of_int_exn n = let hash_int n = Sc_rollup_repr.State_hash.hash_string [Format.sprintf "%d" n] +let init_dissection ?(size = 32) ?init_tick start_hash = + let default_init_tick i = + let hash = + if i = size - 1 then None + else Some (if i = 0 then start_hash else hash_int i) + in + (hash, tick_of_int_exn i) + in + let init_tick = + Option.fold + ~none:default_init_tick + ~some:(fun init_tick -> init_tick size) + init_tick + in + Stdlib.List.init size init_tick + +let init_refutation ?size ?init_tick start_hash = + G. + { + choice = Sc_rollup_tick_repr.initial; + step = Dissection (init_dissection ?size ?init_tick start_hash); + } + let two_stakers_in_conflict () = let* ctxt, rollup, refuter, defender = T.originate_rollup_and_deposit_with_two_stakers () @@ -122,16 +146,11 @@ let two_stakers_in_conflict () = 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} + let init_tick size i = + if i = size - 1 then (None, tick_of_int_exn 10000) + else (Some (if i = 0 then start_hash else hash_int i), tick_of_int_exn i) in + let move = init_refutation ~init_tick start_hash in let* outcome, _ctxt = T.lift @@ R.game_move @@ -173,14 +192,133 @@ let test_single_valid_game_move () = in Assert.is_none ~loc:__LOC__ ~pp:Sc_rollup_game_repr.pp_outcome outcome +(* 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 = + (* Create the defender and the two refuters. *) + let+ ctxt, rollup, refuter1, refuter2, defender = + 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 + 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 = 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 hash4 = Sc_rollup_repr.State_hash.hash_string ["abc"] in + let refutation = init_refutation hash1 in + let commit1 = + 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* c1hash, _, 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 = c1hash; + inbox_level = T.valid_inbox_level ctxt 2l; + number_of_messages = T.number_of_messages_exn 4l; + 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.game_move + ctxt + rollup + ~player + ~opponent + refutation + ~is_opening_move:true + in + 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 + () + 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))) + let tests = [ Tztest.tztest - "A badly distributed dissection is an invalid move" + "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; + Tztest.tztest + "Staker can be in at most one game (injectivity)." + `Quick + test_staker_injectivity; ] diff --git a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_storage.ml b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_storage.ml index f21a079ffba19e83b84e44ef2bf430828936e90e..306ecf4be37f14d9566c8cd59bb21c3368054fdb 100644 --- a/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_storage.ml +++ b/src/proto_alpha/lib_protocol/test/unit/test_sc_rollup_storage.ml @@ -39,16 +39,32 @@ module Commitment_repr = Sc_rollup_commitment_repr (** Lift a computation using using environment errors to use shell errors. *) let lift k = Lwt.map Environment.wrap_tzresult k -let initial_staker_balance = Tez_repr.of_mutez_exn 100_000_000_000L - -let new_context () = - let* b, _contract = Context.init1 () in - Incremental.begin_construction b >>=? fun inc -> +(** [new_context_with_stakers n] creates a context with [n] stakers initially + credited with 100 000 tez. *) +let new_context_with_stakers nb_stakers = + let initial_balance = Int64.of_string "100_000_000_000" in + let*? initial_balances = + List.init ~when_negative_length:[] nb_stakers (fun _ -> initial_balance) + in + let* b, contracts = Context.init_n ~initial_balances nb_stakers () in + let+ inc = Incremental.begin_construction b in let state = Incremental.validation_state inc in let ctxt = state.ctxt in (* Necessary to originate rollups. *) let ctxt = Alpha_context.Origination_nonce.init ctxt Operation_hash.zero in let ctxt = Alpha_context.Internal_for_tests.to_raw ctxt in + let stakers = + List.map + (function + | Alpha_context.Contract.Implicit key -> key | _ -> assert false) + contracts + in + (ctxt, stakers) + +let initial_staker_balance = Tez_repr.of_mutez_exn 100_000_000_000L + +let new_context () = + let* ctxt, _stakers = new_context_with_stakers 1 in (* Mint some tez for staker accounts. *) let mint_tez_for ctxt pkh_str = let pkh = Signature.Public_key_hash.of_b58check_exn pkh_str in @@ -60,7 +76,8 @@ let new_context () = ctxt in let* ctxt = mint_tez_for ctxt "tz1SdKt9kjPp1HRQFkBmXtBhgMfvdgFhSjmG" in - mint_tez_for ctxt "tz1RikjCkrEde1QQmuesp796jCxeiyE6t3Vo" + let* ctxt = mint_tez_for ctxt "tz1RikjCkrEde1QQmuesp796jCxeiyE6t3Vo" in + mint_tez_for ctxt "tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" let new_sc_rollup ctxt = let+ rollup, _size, ctxt = @@ -76,6 +93,16 @@ let new_sc_rollup ctxt = in (rollup, ctxt) +let new_context_with_stakers_and_rollup nb_stakers = + let* ctxt, stakers = new_context_with_stakers nb_stakers in + let+ rollup, ctxt = lift @@ new_sc_rollup ctxt in + (ctxt, rollup, stakers) + +let new_context_with_rollup () = + let* ctxt = new_context () in + let+ rollup, ctxt = lift @@ new_sc_rollup ctxt in + (ctxt, rollup) + let equal_tez ~loc = Assert.equal ~loc Tez_repr.( = ) "Tez aren't equal" Tez_repr.pp @@ -119,7 +146,17 @@ let deposit_stake_and_check_balances ctxt rollup staker = let+ () = assert_balance_increased ctxt ctxt' bonds_account stake in ctxt') -(** Originate a rollup with one staker and make a deposit to the initial LCC *) +(** Originate a rollup with [nb_stakers] stakers and make a deposit to the + initial LCC. *) +let originate_rollup_and_deposit_with_n_stakers nb_stakers = + let* ctxt, rollup, stakers = new_context_with_stakers_and_rollup nb_stakers in + let deposit ctxt staker = + deposit_stake_and_check_balances ctxt rollup staker + in + let+ ctxt = List.fold_left_es deposit ctxt stakers in + (ctxt, rollup, stakers) + +(** Originate a rollup with one staker and make a deposit to the initial LCC. *) let originate_rollup_and_deposit_with_one_staker () = let* ctxt = new_context () in let* rollup, ctxt = lift @@ new_sc_rollup ctxt in @@ -129,7 +166,8 @@ let originate_rollup_and_deposit_with_one_staker () = let+ ctxt = deposit_stake_and_check_balances ctxt rollup staker in (ctxt, rollup, staker) -(** Originate a rollup with two stakers and make a deposit to the initial LCC *) +(** Originate a rollup with two stakers and make a deposit to the initial LCC. +*) let originate_rollup_and_deposit_with_two_stakers () = let* ctxt = new_context () in let* rollup, ctxt = lift @@ new_sc_rollup ctxt in @@ -143,6 +181,14 @@ let originate_rollup_and_deposit_with_two_stakers () = let+ ctxt = deposit_stake_and_check_balances ctxt rollup staker2 in (ctxt, rollup, staker1, staker2) +(** Originate a rollup with three stakers and make a deposit to the initial LCC. +*) +let originate_rollup_and_deposit_with_three_stakers () = + let+ ctxt, rollup, stakers = originate_rollup_and_deposit_with_n_stakers 3 in + match stakers with + | [staker1; staker2; staker3] -> (ctxt, rollup, staker1, staker2, staker3) + | _ -> assert false + (** Trivial assertion. By convention, context is passed linearly as [ctxt]. This takes a context @@ -197,8 +243,7 @@ let test_deposit_to_missing_rollup () = Sc_rollup_repr.Staker.zero) let test_deposit_by_underfunded_staker () = - let* ctxt = new_context () in - let* sc_rollup, ctxt = lift @@ new_sc_rollup ctxt in + let* ctxt, sc_rollup = new_context_with_rollup () in let staker = Sc_rollup_repr.Staker.of_b58check_exn "tz1hhNZvjed6McQQLWtR7MRzPHpgSFZTXxdW" in @@ -234,21 +279,16 @@ let test_deposit_by_underfunded_staker () = {staker; sc_rollup; staker_balance; min_expected_balance = stake}) let test_initial_state_is_pre_boot () = - let* ctxt = new_context () in - let* rollup, ctxt = lift @@ new_sc_rollup ctxt in + let* ctxt, rollup = new_context_with_rollup () in let* lcc, ctxt = lift @@ Sc_rollup_commitment_storage.last_cemented_commitment ctxt rollup in assert_commitment_hash_equal ~loc:__LOC__ ctxt lcc Commitment_repr.Hash.zero let test_deposit_to_existing_rollup () = - let* ctxt = new_context () in - let* rollup, ctxt = lift @@ new_sc_rollup ctxt in - let staker = - Signature.Public_key_hash.of_b58check_exn - "tz1SdKt9kjPp1HRQFkBmXtBhgMfvdgFhSjmG" + let* ctxt, _rollup, _staker = + originate_rollup_and_deposit_with_one_staker () in - let* ctxt = deposit_stake_and_check_balances ctxt rollup staker in assert_true ctxt let assert_balance_unchanged ctxt ctxt' account = @@ -274,13 +314,7 @@ let remove_staker_and_check_balances ctxt rollup staker = ctxt') let test_removing_staker_from_lcc_fails () = - let* ctxt = new_context () in - let* rollup, ctxt = lift @@ new_sc_rollup ctxt in - let staker = - Signature.Public_key_hash.of_b58check_exn - "tz1SdKt9kjPp1HRQFkBmXtBhgMfvdgFhSjmG" - in - let* ctxt = deposit_stake_and_check_balances ctxt rollup staker in + let* ctxt, rollup, staker = originate_rollup_and_deposit_with_one_staker () in assert_fails_with ~loc:__LOC__ (Sc_rollup_stake_storage.remove_staker ctxt rollup staker)