Source file sc_rollup_refutation_storage.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
open Sc_rollup_errors
module Store = Storage.Sc_rollup
module Commitment = Sc_rollup_commitment_repr
module Commitment_storage = Sc_rollup_commitment_storage
module Commitment_hash = Commitment.Hash
module Stake_storage = Sc_rollup_stake_storage
type point = {
commitment : Sc_rollup_commitment_repr.t;
hash : Commitment_hash.t;
}
type conflict_point = point * point
(** [initial_timeout ctxt] set the initial timeout of players. The initial
timeout of each player is equal to [sc_rollup_timeout_period_in_blocks]. *)
let initial_timeout ctxt =
let last_turn_level = (Raw_context.current_level ctxt).level in
let timeout_period_in_blocks =
Constants_storage.sc_rollup_timeout_period_in_blocks ctxt
in
Sc_rollup_game_repr.
{
alice = timeout_period_in_blocks;
bob = timeout_period_in_blocks;
last_turn_level;
}
(** [update_timeout ctxt rollup game idx] update the timeout left for the
current player [game.turn]. Her new timeout is equal to [nb_of_block_left -
(current_level - last_turn_level)] where [nb_of_block_left] is her current
timeout. *)
let update_timeout ctxt rollup (game : Sc_rollup_game_repr.t) idx =
let open Lwt_tzresult_syntax in
let* ctxt, timeout = Store.Game_timeout.get (ctxt, rollup) idx in
let current_level = (Raw_context.current_level ctxt).level in
let sub_block_left nb_of_block_left =
nb_of_block_left
- Int32.to_int (Raw_level_repr.diff current_level timeout.last_turn_level)
in
let new_timeout =
match game.turn with
| Alice ->
let nb_of_block_left = sub_block_left timeout.alice in
{timeout with last_turn_level = current_level; alice = nb_of_block_left}
| Bob ->
let nb_of_block_left = sub_block_left timeout.bob in
{timeout with last_turn_level = current_level; bob = nb_of_block_left}
in
let* ctxt, _ = Store.Game_timeout.update (ctxt, rollup) idx new_timeout in
return ctxt
let get_ongoing_game ctxt rollup staker1 staker2 =
let open Lwt_tzresult_syntax in
let stakers = Sc_rollup_game_repr.Index.make staker1 staker2 in
let* ctxt, game = Store.Game.find (ctxt, rollup) stakers in
let answer = Option.map (fun game -> (game, stakers)) game in
return (answer, ctxt)
let get_ongoing_game_for_staker ctxt rollup staker =
let open Lwt_tzresult_syntax in
let* ctxt, opponent = Store.Opponent.find (ctxt, rollup) staker in
match opponent with
| Some opponent -> get_ongoing_game ctxt rollup staker opponent
| None -> return (None, ctxt)
(** [goto_inbox_level ctxt rollup inbox_level commit] Follows the predecessors of [commit] until it
arrives at the exact [inbox_level]. The result is the commit hash at the given inbox level. *)
let goto_inbox_level ctxt rollup inbox_level commit =
let open Lwt_tzresult_syntax in
let rec go ctxt commit =
let* info, ctxt =
Commitment_storage.get_commitment_unsafe ctxt rollup commit
in
if Raw_level_repr.(info.Commitment.inbox_level <= inbox_level) then (
assert (Raw_level_repr.(info.inbox_level = inbox_level)) ;
return (commit, ctxt))
else (go [@ocaml.tailcall]) ctxt info.predecessor
in
go ctxt commit
let get_conflict_point ctxt rollup staker1 staker2 =
let open Lwt_tzresult_syntax in
let* lcc, ctxt = Commitment_storage.last_cemented_commitment ctxt rollup in
let* commit1, ctxt = Stake_storage.find_staker ctxt rollup staker1 in
let* commit2, ctxt = Stake_storage.find_staker ctxt rollup staker2 in
let* () =
fail_when
Commitment_hash.(
commit1 = zero || commit2 = zero
|| commit1 = lcc
|| commit2 = lcc)
Sc_rollup_no_conflict
in
let* commit1_info, ctxt =
Commitment_storage.get_commitment_unsafe ctxt rollup commit1
in
let* commit2_info, ctxt =
Commitment_storage.get_commitment_unsafe ctxt rollup commit2
in
let target_inbox_level =
Raw_level_repr.min commit1_info.inbox_level commit2_info.inbox_level
in
let* commit1, ctxt =
goto_inbox_level ctxt rollup target_inbox_level commit1
in
let* commit2, ctxt =
goto_inbox_level ctxt rollup target_inbox_level commit2
in
let rec traverse_in_parallel ctxt commit1 commit2 =
let* commit1_info, ctxt =
Commitment_storage.get_commitment_unsafe ctxt rollup commit1
in
let* commit2_info, ctxt =
Commitment_storage.get_commitment_unsafe ctxt rollup commit2
in
assert (Raw_level_repr.(commit1_info.inbox_level = commit2_info.inbox_level)) ;
if Commitment_hash.(commit1_info.predecessor = commit2_info.predecessor)
then
return
( ( {hash = commit1; commitment = commit1_info},
{hash = commit2; commitment = commit2_info} ),
ctxt )
else
(traverse_in_parallel [@ocaml.tailcall])
ctxt
commit1_info.predecessor
commit2_info.predecessor
in
let* () =
fail_when
Commitment_hash.(commit1 = commit2)
Sc_rollup_no_conflict
in
traverse_in_parallel ctxt commit1 commit2
let get_game ctxt rollup stakers =
let open Lwt_tzresult_syntax 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
(** [start_game ctxt rollup refuter defender] initialises the game or
if it already exists fails with `Sc_rollup_game_already_started`.
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
marks them as in a game once the new game is created). The reason we
only allow a staker to play one game at a time is to keep the
end-of-game logic simple---this way, a game can't end suddenly in
the middle because one player lost their stake in another game, it
can only end due to it's own moves or timeouts.
It also initialises the timeout level to the current level plus
[timeout_period_in_blocks] (which will become a protocol constant
soon) to mark the block level at which it becomes possible for
anyone to end the game by timeout.
May fail with:
{ul
{li [Sc_rollup_does_not_exist] if [rollup] does not exist}
{li [Sc_rollup_no_conflict] if [refuter] is staked on an ancestor of
the commitment staked on by [defender], or vice versa}
{li [Sc_rollup_not_staked] if one of the [refuter] or [defender] is
not actually staked}
{li [Sc_rollup_staker_in_game] if one of the [refuter] or [defender]
is already playing a game}
} *)
let start_game ctxt rollup ~player:refuter ~opponent:defender =
let open Lwt_tzresult_syntax in
let stakers = Sc_rollup_game_repr.Index.make refuter defender in
let* ctxt, game_exists = Store.Game.mem (ctxt, rollup) stakers in
let* () = fail_when game_exists Sc_rollup_game_already_started in
let* ctxt, opp_1 = Store.Opponent.find (ctxt, rollup) refuter in
let* ctxt, opp_2 = Store.Opponent.find (ctxt, rollup) defender in
let* () =
match (opp_1, opp_2) with
| None, None -> return ()
| Some _refuter_opponent, None ->
fail (Sc_rollup_staker_in_game (`Refuter refuter))
| None, Some _defender_opponent ->
fail (Sc_rollup_staker_in_game (`Defender defender))
| Some _refuter_opponent, Some _defender_opponent ->
fail (Sc_rollup_staker_in_game (`Both (refuter, defender)))
in
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* ctxt, kind = Store.PVM_kind.get ctxt rollup in
let default_number_of_sections =
Constants_storage.sc_rollup_number_of_sections_in_dissection ctxt
in
let game =
Sc_rollup_game_repr.initial
(Sc_rollup_inbox_repr.take_snapshot inbox)
~pvm_name:(Sc_rollups.Kind.name_of kind)
~parent:parent_info
~child:child_info
~refuter
~defender
~default_number_of_sections
in
let* ctxt, _ = Store.Game.init (ctxt, rollup) stakers game in
let* ctxt, _ =
Store.Game_timeout.init (ctxt, rollup) stakers (initial_timeout ctxt)
in
let* ctxt, _ = Store.Opponent.init (ctxt, rollup) refuter defender in
let* ctxt, _ = Store.Opponent.init (ctxt, rollup) defender refuter in
return ctxt
let game_move ctxt rollup ~player ~opponent refutation =
let open Lwt_tzresult_syntax in
let stakers = Sc_rollup_game_repr.Index.make player opponent in
let* game, ctxt = get_game ctxt rollup stakers in
let* () =
fail_unless
(Sc_rollup_repr.Staker.equal
player
(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
match move_result with
| Either.Left game_result -> return (Some game_result, ctxt)
| Either.Right new_game ->
let* ctxt, _ = Store.Game.update (ctxt, rollup) stakers new_game in
let* ctxt = update_timeout ctxt rollup game stakers in
return (None, ctxt)
let get_timeout ctxt rollup stakers =
let open Lwt_tzresult_syntax in
let* ctxt, timeout_opt =
Storage.Sc_rollup.Game_timeout.find (ctxt, rollup) stakers
in
match timeout_opt with
| Some timeout -> return (timeout, ctxt)
| None -> fail Sc_rollup_no_game
let timeout ctxt rollup stakers =
let open Lwt_tzresult_syntax in
let level = (Raw_context.current_level ctxt).level in
let* game, ctxt = get_game ctxt rollup stakers in
let* ctxt, timeout = Store.Game_timeout.get (ctxt, rollup) stakers in
let* () =
let block_left_before_timeout =
match game.turn with Alice -> timeout.alice | Bob -> timeout.bob
in
let level_of_timeout =
Raw_level_repr.add timeout.last_turn_level block_left_before_timeout
in
fail_unless
Raw_level_repr.(level > level_of_timeout)
(let blocks_left = Raw_level_repr.(diff level_of_timeout level) in
let staker =
match game.turn with Alice -> stakers.alice | Bob -> stakers.bob
in
Sc_rollup_timeout_level_not_reached (blocks_left, staker))
in
let game_result =
match game.game_state with
| Dissecting _ ->
let loser = Sc_rollup_game_repr.Index.staker stakers game.turn in
Sc_rollup_game_repr.(Loser {loser; reason = Timeout})
| Final_move {agreed_start_chunk = _; refuted_stop_chunk = _} ->
Sc_rollup_game_repr.Draw
in
return (game_result, ctxt)
let reward ctxt winner =
let open Lwt_tzresult_syntax in
let winner_contract = Contract_repr.Implicit winner in
let stake = Constants_storage.sc_rollup_stake_amount ctxt in
let*? reward = Tez_repr.(stake /? 2L) in
Token.transfer
ctxt
`Sc_rollup_refutation_rewards
(`Contract winner_contract)
reward
let apply_game_result ctxt rollup (stakers : Sc_rollup_game_repr.Index.t)
(game_result : Sc_rollup_game_repr.game_result) =
let open Lwt_tzresult_syntax in
let status = Sc_rollup_game_repr.Ended game_result in
let* ctxt, balances_updates =
match game_result with
| Loser {loser; reason = _} ->
let losing_staker = loser in
let winning_staker =
let Sc_rollup_game_repr.Index.{alice; bob} = stakers in
if Signature.Public_key_hash.(alice = loser) then bob else alice
in
let* ctxt, balance_updates_winner = reward ctxt winning_staker in
let* ctxt, _, _ = Store.Game.remove (ctxt, rollup) stakers in
let* ctxt, balance_updates_loser =
Stake_storage.remove_staker ctxt rollup losing_staker
in
let balances_updates = balance_updates_loser @ balance_updates_winner in
return (ctxt, balances_updates)
| Draw -> return (ctxt, [])
in
let* ctxt, _, _ = Store.Game_timeout.remove (ctxt, rollup) stakers in
let* ctxt, _, _ = Store.Opponent.remove (ctxt, rollup) stakers.alice in
let* ctxt, _, _ = Store.Opponent.remove (ctxt, rollup) stakers.bob in
return (status, ctxt, balances_updates)
module Internal_for_tests = struct
let get_conflict_point = get_conflict_point
end
type conflict = {
other : Sc_rollup_repr.Staker.t;
their_commitment : Sc_rollup_commitment_repr.t;
our_commitment : Sc_rollup_commitment_repr.t;
parent_commitment : Sc_rollup_commitment_repr.Hash.t;
}
let conflict_encoding =
Data_encoding.(
conv
(fun {other; their_commitment; our_commitment; parent_commitment} ->
(other, their_commitment, our_commitment, parent_commitment))
(fun (other, their_commitment, our_commitment, parent_commitment) ->
{other; their_commitment; our_commitment; parent_commitment})
(obj4
(req "other" Sc_rollup_repr.Staker.encoding)
(req "their_commitment" Sc_rollup_commitment_repr.encoding)
(req "our_commitment" Sc_rollup_commitment_repr.encoding)
(req "parent_commitment" Sc_rollup_commitment_repr.Hash.encoding)))
let conflicting_stakers_uncarbonated ctxt rollup staker =
let open Lwt_tzresult_syntax in
let make_conflict ctxt rollup other (our_point, their_point) =
let our_hash = our_point.hash and their_hash = their_point.hash in
let get = Sc_rollup_commitment_storage.get_commitment_unsafe ctxt rollup in
let* our_commitment, _ = get our_hash in
let* their_commitment, _ = get their_hash in
let parent_commitment = our_commitment.predecessor in
return {other; their_commitment; our_commitment; parent_commitment}
in
let* _ctxt, stakers = Store.stakers ctxt rollup in
List.fold_left_es
(fun conflicts (other_staker, _) ->
let*! res = get_conflict_point ctxt rollup staker other_staker in
match res with
| Ok (conflict_point, _) ->
let* conflict =
make_conflict ctxt rollup other_staker conflict_point
in
return (conflict :: conflicts)
| Error _ -> return conflicts)
[]
stakers