diff --git a/src/proto_016_PtMumbai/lib_protocol/test/pbt/test_refutation_game.ml b/src/proto_016_PtMumbai/lib_protocol/test/pbt/test_refutation_game.ml index e66d71f4b6aaf541bc6e79488caf0b6a1fcdda3b..548672c2fd5a5796c8824e4900f81b71cf9821e7 100644 --- a/src/proto_016_PtMumbai/lib_protocol/test/pbt/test_refutation_game.ml +++ b/src/proto_016_PtMumbai/lib_protocol/test/pbt/test_refutation_game.ml @@ -183,7 +183,10 @@ let final_dissection ~our_states dissection = [our_states] as the state hashes for each tick. *) let build_dissection ~number_of_sections ~start_chunk ~stop_chunk ~our_states = let open Lwt_result_syntax in - let state_hash_from_tick tick = return @@ list_assoc tick our_states in + let state_of_tick ?start_state:_ tick = + return @@ list_assoc tick our_states + in + let state_hash_of_eval_state = Fun.id in let our_stop_chunk = Dissection_chunk. {stop_chunk with state_hash = list_assoc stop_chunk.tick our_states} @@ -196,7 +199,11 @@ let build_dissection ~number_of_sections ~start_chunk ~stop_chunk ~our_states = Lwt_main.run @@ let*! r = Game_helpers.( - make_dissection ~state_hash_from_tick ~start_chunk ~our_stop_chunk + make_dissection + ~state_of_tick + ~state_hash_of_eval_state + ~start_chunk + ~our_stop_chunk @@ default_new_dissection ~start_chunk ~our_stop_chunk @@ -1633,8 +1640,9 @@ let test_wasm_dissection name kind = let+ dissection = Game_helpers.( make_dissection - ~state_hash_from_tick:(fun _ -> + ~state_of_tick:(fun ?start_state:_ _ -> return_some Sc_rollup.State_hash.zero) + ~state_hash_of_eval_state:Fun.id ~start_chunk ~our_stop_chunk:stop_chunk @@ Wasm.new_dissection diff --git a/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.ml b/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.ml index 7ae2e54fda1195690d11c3d2c0cfe9611d5e1d93..b3ddddc305e12c4da450b9f45af89f5b29257023 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.ml @@ -53,17 +53,19 @@ let default_new_dissection ~default_number_of_sections in make [] Z.one (Tick.jump start_chunk.tick first_section_length) -let make_dissection ~state_hash_from_tick ~start_chunk ~our_stop_chunk ticks = - let rec make_dissection_aux ticks acc = +let make_dissection ~state_of_tick ~state_hash_of_eval_state ?start_state + ~start_chunk ~our_stop_chunk ticks = + let rec make_dissection_aux start_state ticks acc = let open Lwt_result_syntax in match ticks with | tick :: rst -> - let* state_hash = state_hash_from_tick tick in + let* eval_state = state_of_tick ?start_state tick in + let state_hash = Option.map state_hash_of_eval_state eval_state in let chunk = Dissection_chunk.{tick; state_hash} in - make_dissection_aux rst (chunk :: acc) + make_dissection_aux eval_state rst (chunk :: acc) | [] -> return @@ List.rev (our_stop_chunk :: acc) in - make_dissection_aux ticks [start_chunk] + make_dissection_aux start_state ticks [start_chunk] module Wasm = struct let new_dissection ~default_number_of_sections ~start_chunk ~our_stop_chunk = diff --git a/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.mli b/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.mli index eb59b3bc9512e71ed9ea878893fe2621566d9363..7e517f296e328ac918fddf3a50926ec2fdbc8631 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.mli +++ b/src/proto_016_PtMumbai/lib_sc_rollup/game_helpers.mli @@ -45,11 +45,13 @@ val default_new_dissection : dissection from [start_chunk] to [our_stop_chunk], and recomputes the state hash associated to each ticks. *) val make_dissection : - state_hash_from_tick:(Tick.t -> State_hash.t option tzresult Lwt.t) -> + state_of_tick:(?start_state:'a -> Tick.t -> ('a option, 'trace) result Lwt.t) -> + state_hash_of_eval_state:('a -> State_hash.t) -> + ?start_state:'a -> start_chunk:Dissection_chunk.t -> our_stop_chunk:Dissection_chunk.t -> - Tick.t trace -> - Dissection_chunk.t list tzresult Lwt.t + Tick.t list -> + (Dissection_chunk.t list, 'trace) result Lwt.t module Wasm : sig (** [new_dissection ~default_number_of_sections ~start_chunk diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/fueled_pvm.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/fueled_pvm.ml index c98b54f662e9ea937150ae3089256f06bc5c4348..3deb24ac28589ce99e5d76fe7c50cd3bb75b0259 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/fueled_pvm.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/fueled_pvm.ml @@ -34,36 +34,50 @@ module type S = sig type fuel - type eval_result = {state : PVM.state; remaining_fuel : fuel; num_ticks : Z.t} + (** Evaluation state for the PVM. *) + type eval_state = { + state : PVM.state; (** The actual PVM state. *) + state_hash : PVM.hash; (** Hash of [state]. *) + tick : Sc_rollup.Tick.t; (** Tick of [state]. *) + inbox_level : Raw_level.t; + (** Inbox level in which messages are evaluated. *) + message_counter_offset : int; + (** Offset for message index, which corresponds to the number of + messages of the inbox already evaluated. *) + remaining_fuel : fuel; + (** Fuel remaining for the evaluation of the inbox. *) + remaining_messages : Sc_rollup.Inbox_message.t list; + (** Messages of the inbox that remain to be evaluated. *) + } + + (** Evaluation result for the PVM which contains the evaluation state and + additional information. *) + type eval_result = {state : eval_state; num_ticks : Z.t; num_messages : int} (** [eval_block_inbox ~fuel node_ctxt (inbox, messages) state] evaluates the [messages] for the [inbox] in the given [state] of the PVM and returns the - evaluation results containing the new state, the number of messages, the + evaluation result containing the new state, the number of messages, the inbox level and the remaining fuel. *) val eval_block_inbox : fuel:fuel -> _ Node_context.t -> Sc_rollup.Inbox.t * Sc_rollup.Inbox_message.t list -> PVM.state -> - (PVM.state * int * Raw_level.t * fuel) Node_context.delayed_write tzresult - Lwt.t + eval_result Node_context.delayed_write tzresult Lwt.t (** [eval_messages ?reveal_map ~fuel node_ctxt ~message_counter_offset state inbox_level messages] evaluates the [messages] for inbox level [inbox_level] in the given [state] of the PVM and returns the evaluation results containing the new state, the remaining fuel, and the number of - ticks for the evaluation of these messages. [message_counter_offset] is - used when we evaluate partial inboxes, such as during simulation. When - [reveal_map] is provided, it is used as an additional source of data for - revelation ticks. *) + ticks for the evaluation of these messages. If [messages] is empty, the + PVM progresses until the next input request (within the allocated + [fuel]). [message_counter_offset] is used when we evaluate partial + inboxes, such as during simulation. When [reveal_map] is provided, it is + used as an additional source of data for revelation ticks. *) val eval_messages : ?reveal_map:string Sc_rollup_reveal_hash.Map.t -> - fuel:fuel -> _ Node_context.t -> - message_counter_offset:int -> - PVM.state -> - Raw_level.t -> - Sc_rollup.Inbox_message.t list -> + eval_state -> eval_result Node_context.delayed_write tzresult Lwt.t end @@ -74,12 +88,18 @@ module Make (PVM : Pvm.S) = struct type fuel = F.t - type eval_result = { + type eval_state = { state : PVM.state; + state_hash : PVM.hash; + tick : Sc_rollup.Tick.t; + inbox_level : Raw_level.t; + message_counter_offset : int; remaining_fuel : fuel; - num_ticks : Z.t; + remaining_messages : Sc_rollup.Inbox_message.t list; } + type eval_result = {state : eval_state; num_ticks : Z.t; num_messages : int} + let get_reveal ~data_dir reveal_map hash = let found_in_map = match reveal_map with @@ -264,7 +284,7 @@ module Make (PVM : Pvm.S) = struct {input with Sc_rollup.payload} type feed_input_completion = - | Feed_input_aborted of {state : PVM.state; fuel : fuel} + | Feed_input_aborted of {state : PVM.state; fuel : fuel; fed_input : bool} | Feed_input_completed of {state : PVM.state; fuel : fuel} (** [feed_input node_ctxt reveal_map level message_index ~fuel @@ -289,11 +309,12 @@ module Make (PVM : Pvm.S) = struct state in match res with - | Aborted {state; fuel; _} -> return (Feed_input_aborted {state; fuel}) + | Aborted {state; fuel; _} -> + return (Feed_input_aborted {state; fuel; fed_input = false}) | Completed {state; fuel; current_tick = tick; failing_ticks} -> ( let open Delayed_write_monad.Lwt_result_syntax in match F.consume F.one_tick_consumption fuel with - | None -> return (Feed_input_aborted {state; fuel}) + | None -> return (Feed_input_aborted {state; fuel; fed_input = false}) | Some fuel -> ( let>* input, failing_ticks = match failing_ticks with @@ -324,7 +345,7 @@ module Make (PVM : Pvm.S) = struct in match res with | Aborted {state; fuel; _} -> - return (Feed_input_aborted {state; fuel}) + return (Feed_input_aborted {state; fuel; fed_input = true}) | Completed {state; fuel; _} -> return (Feed_input_completed {state; fuel}))) @@ -337,10 +358,11 @@ module Make (PVM : Pvm.S) = struct let rec feed_messages (state, fuel) message_index = function | [] -> (* Fed all messages *) - return (state, fuel) - | _messages when F.is_empty fuel -> + return (state, fuel, message_index - message_counter_offset, []) + | messages when F.is_empty fuel -> (* Consumed all fuel *) - return (state, fuel) + return + (state, fuel, message_index - message_counter_offset, messages) | message :: messages -> ( let*? payload = Sc_rollup.Inbox_message.( @@ -368,20 +390,30 @@ module Make (PVM : Pvm.S) = struct match res with | Feed_input_completed {state; fuel} -> feed_messages (state, fuel) (message_index + 1) messages - | Feed_input_aborted {state; fuel} -> return (state, fuel)) + | Feed_input_aborted {state; fuel; fed_input = false} -> + return + ( state, + fuel, + message_index - message_counter_offset, + message :: messages ) + | Feed_input_aborted {state; fuel; fed_input = true} -> + return + ( state, + fuel, + message_index + 1 - message_counter_offset, + messages )) in (feed_messages [@tailcall]) (state, fuel) message_counter_offset messages let eval_block_inbox ~fuel node_ctxt (inbox, messages) (state : PVM.state) : - (PVM.state * int * Raw_level.t * fuel) Node_context.delayed_write - tzresult - Lwt.t = + eval_result Node_context.delayed_write tzresult Lwt.t = + let open Lwt_result_syntax in let open Delayed_write_monad.Lwt_result_syntax in (* Obtain inbox and its messages for this block. *) let inbox_level = Inbox.inbox_level inbox in - let num_messages = List.length messages in + let*! initial_tick = PVM.get_tick state in (* Evaluate all the messages for this level. *) - let>* state, fuel = + let>* state, remaining_fuel, num_messages, remaining_messages = eval_messages ~reveal_map:None ~fuel @@ -391,26 +423,87 @@ module Make (PVM : Pvm.S) = struct inbox_level messages in - return (state, num_messages, inbox_level, fuel) + let*! final_tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in + let num_ticks = Sc_rollup.Tick.distance initial_tick final_tick in + let eval_state = + { + state; + state_hash; + tick = final_tick; + inbox_level; + message_counter_offset = num_messages; + remaining_fuel; + remaining_messages; + } + in + return {state = eval_state; num_ticks; num_messages} - let eval_messages ?reveal_map ~fuel node_ctxt ~message_counter_offset state - inbox_level messages = + let eval_messages ?reveal_map node_ctxt + { + state; + tick = initial_tick; + inbox_level; + message_counter_offset; + remaining_fuel = fuel; + remaining_messages = messages; + _; + } = let open Lwt_result_syntax in let open Delayed_write_monad.Lwt_result_syntax in - let*! initial_tick = PVM.get_tick state in - let>* state, remaining_fuel = - eval_messages - ~reveal_map - ~fuel - node_ctxt - ~message_counter_offset - state - inbox_level - messages + let>* state, remaining_fuel, num_messages, remaining_messages = + match messages with + | [] -> + let level = Raw_level.to_int32 inbox_level |> Int32.to_int in + let message_index = message_counter_offset - 1 in + let failing_ticks = + Loser_mode.is_failure + node_ctxt.Node_context.loser_mode + ~level + ~message_index + in + let>* res = + eval_until_input + node_ctxt + reveal_map + level + message_index + ~fuel + 0L + failing_ticks + state + in + let state, remaining_fuel = + match res with + | Aborted {state; fuel; _} | Completed {state; fuel; _} -> + (state, fuel) + in + return (state, remaining_fuel, 0, []) + | _ -> + eval_messages + ~reveal_map + ~fuel + node_ctxt + ~message_counter_offset + state + inbox_level + messages in let*! final_tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in let num_ticks = Sc_rollup.Tick.distance initial_tick final_tick in - return {state; remaining_fuel; num_ticks} + let eval_state = + { + state; + state_hash; + tick = final_tick; + inbox_level; + message_counter_offset = message_counter_offset + num_messages; + remaining_fuel; + remaining_messages; + } + in + return {state = eval_state; num_ticks; num_messages} end module Free = Make_fueled (Fuel.Free) diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.ml index f74691dd333627f70222a95456528b04a351ed23..41301dd9a88da1e2f46889afe42a37ab57240788 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.ml @@ -45,9 +45,10 @@ module type S = sig val state_of_tick : _ Node_context.t -> + ?start_state:Accounted_pvm.eval_state -> Sc_rollup.Tick.t -> Raw_level.t -> - (PVM.state * PVM.hash) option tzresult Lwt.t + Accounted_pvm.eval_state option tzresult Lwt.t val state_of_head : 'a Node_context.t -> @@ -143,25 +144,24 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct inbox_messages predecessor_state in - let* state, num_messages, inbox_level, _fuel = + let* { + state = {state; state_hash; inbox_level; tick; _}; + num_messages; + num_ticks; + } = Delayed_write_monad.apply node_ctxt eval_result in let*! ctxt = PVM.State.set ctxt state in let*! initial_tick = PVM.get_tick predecessor_state in - let*! last_tick = PVM.get_tick state in - let num_ticks = - Sc_rollup.Tick.distance initial_tick last_tick |> Z.to_int64 - in (* Produce events. *) - let*! state_hash = PVM.state_hash state in let*! () = Interpreter_event.transitioned_pvm inbox_level state_hash - last_tick + tick num_messages in - return (ctxt, num_messages, num_ticks, initial_tick) + return (ctxt, num_messages, Z.to_int64 num_ticks, initial_tick) (** [process_head node_ctxt ctxt ~predecessor head] runs the PVM for the given head. *) @@ -184,11 +184,11 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct return (ctxt, 0, 0L, Sc_rollup.Tick.initial) else return (ctxt, 0, 0L, Sc_rollup.Tick.initial) - (** [run_for_ticks node_ctxt block tick_distance] starts the - evaluation of the inbox at [block] for at most [tick_distance]. *) - let run_for_ticks node_ctxt (block : Sc_rollup_block.t) tick_distance = + (** Returns the starting evaluation before the evaluation of the block. It + contains the PVM state at the end of the execution of the previous block + and the messages the block ([remaining_messages]). *) + let start_state_of_block node_ctxt (block : Sc_rollup_block.t) = let open Lwt_result_syntax in - let open Delayed_write_monad.Lwt_result_syntax in let pred_level = Raw_level.to_int32 block.header.level |> Int32.pred in let* ctxt = Node_context.checkout_context node_ctxt block.header.predecessor @@ -203,39 +203,108 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct let* {predecessor; predecessor_timestamp; messages} = Node_context.get_messages node_ctxt block.header.inbox_witness in + let inbox_level = Sc_rollup.Inbox.inbox_level inbox in + let*! tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in let messages = Sc_rollup.Inbox_message.Internal Start_of_level :: Internal (Info_per_level {predecessor; predecessor_timestamp}) :: messages @ [Internal End_of_level] in - let>* state, _counter, _level, _fuel = - Accounted_pvm.eval_block_inbox - ~fuel:(Fuel.Accounted.of_ticks tick_distance) + return + Accounted_pvm. + { + state; + state_hash; + inbox_level; + tick; + message_counter_offset = 0; + remaining_fuel = Fuel.Accounted.of_ticks 0L; + remaining_messages = messages; + } + + (** [run_for_ticks node_ctxt start_state tick_distance] starts the evaluation + of messages in the [start_state] for at most [tick_distance]. *) + let run_to_tick node_ctxt start_state tick = + let open Delayed_write_monad.Lwt_result_syntax in + let tick_distance = + Sc_rollup.Tick.distance tick start_state.Accounted_pvm.tick |> Z.to_int64 + in + let>+ eval_result = + Accounted_pvm.eval_messages node_ctxt - (inbox, messages) - state + { + start_state with + remaining_fuel = Fuel.Accounted.of_ticks tick_distance; + } in - return state + eval_result.state + + let state_of_tick_aux node_ctxt ~start_state (event : Sc_rollup_block.t) tick + = + let open Lwt_result_syntax in + let* start_state = + match start_state with + | Some start_state + when Raw_level.( + start_state.Accounted_pvm.inbox_level = event.header.level) -> + return start_state + | _ -> + (* Recompute start state on level change or if we don't have a + starting state on hand. *) + start_state_of_block node_ctxt event + in + (* TODO: #3384 + We should test that we always have enough blocks to find the tick + because [state_of_tick] is a critical function. *) + let* result_state = run_to_tick node_ctxt start_state tick in + let result_state = Delayed_write_monad.ignore result_state in + return result_state + + (* The cache allows cache intermediate states of the PVM in e.g. dissections. *) + module Tick_state_cache = + Aches_lwt.Lache.Make + (Aches.Rache.Transfer + (Aches.Rache.LRU) + (struct + type t = Sc_rollup.Tick.t * Block_hash.t + + let equal (t1, b1) (t2, b2) = + Sc_rollup.Tick.(t1 = t2) && Block_hash.(b1 = b2) + + let hash (tick, block) = + ((Sc_rollup.Tick.to_z tick |> Z.hash) * 13) + Block_hash.hash block + end)) + + let tick_state_cache = Tick_state_cache.create 64 (* size of 2 dissections *) + + (* Memoized version of [state_of_tick_aux]. *) + let memo_state_of_tick_aux node_ctxt ~start_state (event : Sc_rollup_block.t) + tick = + Tick_state_cache.bind_or_put + tick_state_cache + (tick, event.header.block_hash) + (fun (tick, _hash) -> state_of_tick_aux node_ctxt ~start_state event tick) + Lwt.return - (** [state_of_tick node_ctxt tick level] returns [Some (state, hash)] for a - given [tick] if this [tick] happened before [level]. Otherwise, returns - [None].*) - let state_of_tick node_ctxt tick level = + (** [state_of_tick node_ctxt ?start_state tick level] returns [Some end_state] + for a given [tick] if this [tick] happened before [level]. Otherwise, + returns [None].*) + let state_of_tick node_ctxt ?start_state tick level = let open Lwt_result_syntax in let* event = Node_context.block_with_tick node_ctxt ~max_level:level tick in match event with | None -> return_none | Some event -> assert (Raw_level.(event.header.level <= level)) ; - let tick_distance = - Sc_rollup.Tick.distance tick event.initial_tick |> Z.to_int64 + let* result_state = + if Node_context.is_loser node_ctxt then + (* TODO: https://gitlab.com/tezos/tezos/-/issues/5253 + The failures/loser mode does not work properly when restarting + from intermediate states. *) + state_of_tick_aux node_ctxt ~start_state:None event tick + else memo_state_of_tick_aux node_ctxt ~start_state event tick in - (* TODO: #3384 - We should test that we always have enough blocks to find the tick - because [state_of_tick] is a critical function. *) - let* state = run_for_ticks node_ctxt event tick_distance in - let state = Delayed_write_monad.ignore state in - let*! hash = PVM.state_hash state in - return (Some (state, hash)) + return_some result_state end diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.mli b/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.mli index 71cdbcabd249910063015f09d45fc8b4219353f3..3bc4a0aa7aeeb0ec00405fe6dd0dc0fd8d3d1e08 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.mli +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/interpreter.mli @@ -50,14 +50,16 @@ module type S = sig Sc_rollup.Inbox.t * Sc_rollup.Inbox_message.t list -> ('a Context.t * int * int64 * Sc_rollup.Tick.t) tzresult Lwt.t - (** [state_of_tick node_ctxt tick level] returns [Some (state, hash)] - for a given [tick] if this [tick] happened before - [level]. Otherwise, returns [None].*) + (** [state_of_tick node_ctxt ?start_state tick level] returns [Some (state, + hash)] for a given [tick] if this [tick] happened before + [level]. Otherwise, returns [None]. If provided, the evaluation is resumed + from [start_state]. *) val state_of_tick : _ Node_context.t -> + ?start_state:Accounted_pvm.eval_state -> Sc_rollup.Tick.t -> Raw_level.t -> - (PVM.state * PVM.hash) option tzresult Lwt.t + Accounted_pvm.eval_state option tzresult Lwt.t (** [state_of_head node_ctxt ctxt head] returns the state corresponding to the block [head], or the state at rollup genesis if the block is before the diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.ml index 6f99993ff62faa058ae77230879bf57b6f10ee74..d2692f61778ae6db33b139893d143bba167a3d4d 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.ml @@ -66,6 +66,8 @@ let is_operator node_ctxt pkh = let is_accuser {mode; _} = mode = Accuser +let is_loser {loser_mode; _} = loser_mode <> Loser_mode.no_failures + let get_fee_parameter node_ctxt purpose = Configuration.Operator_purpose_map.find purpose node_ctxt.fee_parameters |> Option.value ~default:(Configuration.default_fee_parameter ~purpose ()) diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.mli b/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.mli index 1f976a65d56062dbc8c1b4df207a3791d3b5f25c..4016ced93b150e36099eaea93d2c8c792d73dc9b 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.mli +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/node_context.mli @@ -94,6 +94,10 @@ val is_operator : _ t -> Tezos_crypto.Signature.Public_key_hash.t -> bool mode. *) val is_accuser : _ t -> bool +(** [is_loser node_ctxt] returns [true] if the rollup node runs has some + failures planned. *) +val is_loser : _ t -> bool + (** [get_fee_parameter cctxt purpose] returns the fee parameter to inject an operation for a given [purpose]. If no specific fee parameters were configured for this purpose, returns the default fee parameter for this diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/refutation_game.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/refutation_game.ml index b54d624d64a6315e40c4ee88eda5b81fa7b83e12..bac47d38003207ecffd44382d99015dc9685d695 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/refutation_game.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/refutation_game.ml @@ -298,25 +298,43 @@ module Make (Interpreter : Interpreter.S) : in if Result.is_ok res then return proof else assert false + type pvm_intermediate_state = + | Hash of PVM.hash + | Evaluated of Interpreter.Accounted_pvm.eval_state + let new_dissection ~opponent ~default_number_of_sections node_ctxt last_level ok our_view = let open Lwt_result_syntax in - let state_hash_from_tick tick = - let* r = Interpreter.state_of_tick node_ctxt tick last_level in - return (Option.map snd r) + let state_of_tick ?start_state tick = + Interpreter.state_of_tick node_ctxt ?start_state tick last_level + in + let state_hash_of_eval_state Interpreter.Accounted_pvm.{state_hash; _} = + state_hash + in + let start_hash, start_tick, start_state = + match ok with + | Hash hash, tick -> (hash, tick, None) + | Evaluated ({state_hash; _} as state), tick -> + (state_hash, tick, Some state) in - let start_hash, start_tick = ok in let start_chunk = Sc_rollup.Dissection_chunk. {state_hash = Some start_hash; tick = start_tick} in - let start_hash, start_tick = our_view in + let our_state, our_tick = our_view in + let our_state_hash = + Option.map + (fun Interpreter.Accounted_pvm.{state_hash; _} -> state_hash) + our_state + in let our_stop_chunk = - Sc_rollup.Dissection_chunk.{state_hash = start_hash; tick = start_tick} + Sc_rollup.Dissection_chunk.{state_hash = our_state_hash; tick = our_tick} in let* dissection = Game_helpers.make_dissection - ~state_hash_from_tick + ~state_of_tick + ~state_hash_of_eval_state + ?start_state ~start_chunk ~our_stop_chunk @@ PVM.new_dissection @@ -327,8 +345,8 @@ module Make (Interpreter : Interpreter.S) : let*! () = Refutation_game_event.computed_dissection ~opponent - ~start_tick:(snd ok) - ~end_tick:(snd our_view) + ~start_tick + ~end_tick:our_tick dissection in return dissection @@ -351,26 +369,32 @@ module Make (Interpreter : Interpreter.S) : .Unreliable_tezos_node_returning_inconsistent_game | Sc_rollup.Dissection_chunk.{state_hash = their_hash; tick} :: dissection -> ( - let open Lwt_result_syntax in + let start_state = + match ok with + | Hash _, _ -> None + | Evaluated ok_state, _ -> Some ok_state + in let* our = - Interpreter.state_of_tick node_ctxt tick game.inbox_level + Interpreter.state_of_tick + node_ctxt + ?start_state + tick + game.inbox_level in match (their_hash, our) with | None, None -> (* This case is absurd since: [None] can only occur at the end and the two players disagree about the end. *) assert false - | Some _, None | None, Some _ -> - return (ok, (Option.map snd our, tick)) - | Some their_hash, Some (_, our_hash) -> + | Some _, None | None, Some _ -> return (ok, (our, tick)) + | Some their_hash, Some ({state_hash = our_hash; _} as our_state) -> if Sc_rollup.State_hash.equal our_hash their_hash then - traverse (their_hash, tick) dissection - else return (ok, (Some our_hash, tick))) + traverse (Evaluated our_state, tick) dissection + else return (ok, (our, tick))) in match dissection with | Sc_rollup.Dissection_chunk.{state_hash = Some hash; tick} :: dissection -> - let* ok, ko = traverse (hash, tick) dissection in - let choice = snd ok in + let* ok, ko = traverse (Hash hash, tick) dissection in let* dissection = new_dissection ~opponent @@ -380,7 +404,9 @@ module Make (Interpreter : Interpreter.S) : ok ko in - let chosen_section_len = Sc_rollup.Tick.distance (snd ko) choice in + let _, choice = ok in + let _, ko_tick = ko in + let chosen_section_len = Sc_rollup.Tick.distance ko_tick choice in return (choice, chosen_section_len, dissection) | [] | {state_hash = None; _} :: _ -> (* @@ -403,7 +429,7 @@ module Make (Interpreter : Interpreter.S) : tzfail Sc_rollup_node_errors .Unreliable_tezos_node_returning_inconsistent_game - | Some (start_state, _start_hash) -> + | Some {state = start_state; _} -> let* proof = generate_proof node_ctxt game start_state in let*? pvm_step = Sc_rollup.Proof.serialize_pvm_step ~pvm:(module PVM) proof.pvm_step diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/sc_rollup_node_errors.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/sc_rollup_node_errors.ml index ea53d100398426c95e0e1f2a039e763a8399adec..c2b5d2a399469978bc5d8c776637d9e909043b3a 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/sc_rollup_node_errors.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/sc_rollup_node_errors.ml @@ -93,16 +93,19 @@ let () = (fun (inbox_level, ours, on_l1) -> Disagree_with_cemented {inbox_level; ours; on_l1}) ; + let description = + "Internal error: The game invariant states that the dissection from the \ + opponent must contain a tick we disagree with. If the retrieved game does \ + not respect this, we cannot trust the Tezos node we are connected to and \ + prefer to stop here." + in register_error_kind `Permanent ~id:"internal.unreliable_tezos_node" ~title:"Internal error: Tezos node seems unreliable" - ~description: - "Internal error: The game invariant states that the dissection from the \ - opponent must contain a tick we disagree with. If the retrieved game \ - does not respect this, we cannot trust the Tezos node we are connected \ - to and prefer to stop here." - ~pp:(fun _ppf () -> ()) + ~description + ~pp:(fun ppf () -> + Format.fprintf ppf "Unreliable Tezos node. %s" description) Data_encoding.unit (function | Unreliable_tezos_node_returning_inconsistent_game -> Some () | _ -> None) diff --git a/src/proto_016_PtMumbai/lib_sc_rollup_node/simulation.ml b/src/proto_016_PtMumbai/lib_sc_rollup_node/simulation.ml index c6facce09a1140a1e46adeb9006d92d9c16c37fe..58d0fbdebe064a6025c499d2e594cf8e7bd66666 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup_node/simulation.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup_node/simulation.ml @@ -133,23 +133,29 @@ module Make (Interpreter : Interpreter.S) : info_per_level = _; } as sim) messages = let open Lwt_result_syntax in + let*! state_hash = PVM.state_hash state in + let*! tick = PVM.get_tick state in + let eval_state = + Fueled_pvm. + { + state; + state_hash; + tick; + inbox_level; + message_counter_offset = nb_messages_inbox; + remaining_fuel = Fuel.Free.of_ticks 0L; + remaining_messages = messages; + } + in (* Build new state *) let* eval_result = - Fueled_pvm.eval_messages - ?reveal_map - ~fuel:(Fuel.Free.of_ticks 0L) - node_ctxt - ~message_counter_offset:nb_messages_inbox - state - inbox_level - messages + Fueled_pvm.eval_messages ?reveal_map node_ctxt eval_state in - let Fueled_pvm.{state; num_ticks; _} = + let Fueled_pvm.{state = {state; _}; num_ticks; num_messages; _} = Delayed_write_monad.ignore eval_result in let*! ctxt = PVM.State.set ctxt state in - let nb_messages = List.length messages in - let nb_messages_inbox = nb_messages_inbox + nb_messages in + let nb_messages_inbox = nb_messages_inbox + num_messages in return ({sim with ctxt; state; nb_messages_inbox}, num_ticks) let simulate_messages (node_ctxt : Node_context.ro) sim messages = 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 b0c89afb746e7f02f25e0c4056f6eaf97118dd37..4df70d6f9b942336b9247f1cc47578928cca2207 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 @@ -183,7 +183,10 @@ let final_dissection ~our_states dissection = [our_states] as the state hashes for each tick. *) let build_dissection ~number_of_sections ~start_chunk ~stop_chunk ~our_states = let open Lwt_result_syntax in - let state_hash_from_tick tick = return @@ list_assoc tick our_states in + let state_of_tick ?start_state:_ tick = + return @@ list_assoc tick our_states + in + let state_hash_of_eval_state = Fun.id in let our_stop_chunk = Dissection_chunk. {stop_chunk with state_hash = list_assoc stop_chunk.tick our_states} @@ -196,7 +199,11 @@ let build_dissection ~number_of_sections ~start_chunk ~stop_chunk ~our_states = Lwt_main.run @@ let*! r = Game_helpers.( - make_dissection ~state_hash_from_tick ~start_chunk ~our_stop_chunk + make_dissection + ~state_of_tick + ~state_hash_of_eval_state + ~start_chunk + ~our_stop_chunk @@ default_new_dissection ~start_chunk ~our_stop_chunk @@ -1604,8 +1611,9 @@ let test_wasm_dissection name kind = let+ dissection = Game_helpers.( make_dissection - ~state_hash_from_tick:(fun _ -> + ~state_of_tick:(fun ?start_state:_ _ -> return_some Sc_rollup.State_hash.zero) + ~state_hash_of_eval_state:Fun.id ~start_chunk ~our_stop_chunk:stop_chunk @@ Wasm.new_dissection diff --git a/src/proto_alpha/lib_sc_rollup/game_helpers.ml b/src/proto_alpha/lib_sc_rollup/game_helpers.ml index 7ae2e54fda1195690d11c3d2c0cfe9611d5e1d93..b3ddddc305e12c4da450b9f45af89f5b29257023 100644 --- a/src/proto_alpha/lib_sc_rollup/game_helpers.ml +++ b/src/proto_alpha/lib_sc_rollup/game_helpers.ml @@ -53,17 +53,19 @@ let default_new_dissection ~default_number_of_sections in make [] Z.one (Tick.jump start_chunk.tick first_section_length) -let make_dissection ~state_hash_from_tick ~start_chunk ~our_stop_chunk ticks = - let rec make_dissection_aux ticks acc = +let make_dissection ~state_of_tick ~state_hash_of_eval_state ?start_state + ~start_chunk ~our_stop_chunk ticks = + let rec make_dissection_aux start_state ticks acc = let open Lwt_result_syntax in match ticks with | tick :: rst -> - let* state_hash = state_hash_from_tick tick in + let* eval_state = state_of_tick ?start_state tick in + let state_hash = Option.map state_hash_of_eval_state eval_state in let chunk = Dissection_chunk.{tick; state_hash} in - make_dissection_aux rst (chunk :: acc) + make_dissection_aux eval_state rst (chunk :: acc) | [] -> return @@ List.rev (our_stop_chunk :: acc) in - make_dissection_aux ticks [start_chunk] + make_dissection_aux start_state ticks [start_chunk] module Wasm = struct let new_dissection ~default_number_of_sections ~start_chunk ~our_stop_chunk = diff --git a/src/proto_alpha/lib_sc_rollup/game_helpers.mli b/src/proto_alpha/lib_sc_rollup/game_helpers.mli index eb59b3bc9512e71ed9ea878893fe2621566d9363..7e517f296e328ac918fddf3a50926ec2fdbc8631 100644 --- a/src/proto_alpha/lib_sc_rollup/game_helpers.mli +++ b/src/proto_alpha/lib_sc_rollup/game_helpers.mli @@ -45,11 +45,13 @@ val default_new_dissection : dissection from [start_chunk] to [our_stop_chunk], and recomputes the state hash associated to each ticks. *) val make_dissection : - state_hash_from_tick:(Tick.t -> State_hash.t option tzresult Lwt.t) -> + state_of_tick:(?start_state:'a -> Tick.t -> ('a option, 'trace) result Lwt.t) -> + state_hash_of_eval_state:('a -> State_hash.t) -> + ?start_state:'a -> start_chunk:Dissection_chunk.t -> our_stop_chunk:Dissection_chunk.t -> - Tick.t trace -> - Dissection_chunk.t list tzresult Lwt.t + Tick.t list -> + (Dissection_chunk.t list, 'trace) result Lwt.t module Wasm : sig (** [new_dissection ~default_number_of_sections ~start_chunk diff --git a/src/proto_alpha/lib_sc_rollup_node/fueled_pvm.ml b/src/proto_alpha/lib_sc_rollup_node/fueled_pvm.ml index af9dc1d5b7ae94fe79a8aec30abba0bcb6d28bef..1f58df04ac5c78048f816221818caaf3d173f8ae 100644 --- a/src/proto_alpha/lib_sc_rollup_node/fueled_pvm.ml +++ b/src/proto_alpha/lib_sc_rollup_node/fueled_pvm.ml @@ -34,36 +34,50 @@ module type S = sig type fuel - type eval_result = {state : PVM.state; remaining_fuel : fuel; num_ticks : Z.t} + (** Evaluation state for the PVM. *) + type eval_state = { + state : PVM.state; (** The actual PVM state. *) + state_hash : PVM.hash; (** Hash of [state]. *) + tick : Sc_rollup.Tick.t; (** Tick of [state]. *) + inbox_level : Raw_level.t; + (** Inbox level in which messages are evaluated. *) + message_counter_offset : int; + (** Offset for message index, which corresponds to the number of + messages of the inbox already evaluated. *) + remaining_fuel : fuel; + (** Fuel remaining for the evaluation of the inbox. *) + remaining_messages : Sc_rollup.Inbox_message.t list; + (** Messages of the inbox that remain to be evaluated. *) + } + + (** Evaluation result for the PVM which contains the evaluation state and + additional information. *) + type eval_result = {state : eval_state; num_ticks : Z.t; num_messages : int} (** [eval_block_inbox ~fuel node_ctxt (inbox, messages) state] evaluates the [messages] for the [inbox] in the given [state] of the PVM and returns the - evaluation results containing the new state, the number of messages, the + evaluation result containing the new state, the number of messages, the inbox level and the remaining fuel. *) val eval_block_inbox : fuel:fuel -> _ Node_context.t -> Sc_rollup.Inbox.t * Sc_rollup.Inbox_message.t list -> PVM.state -> - (PVM.state * int * Raw_level.t * fuel) Node_context.delayed_write tzresult - Lwt.t + eval_result Node_context.delayed_write tzresult Lwt.t (** [eval_messages ?reveal_map ~fuel node_ctxt ~message_counter_offset state inbox_level messages] evaluates the [messages] for inbox level [inbox_level] in the given [state] of the PVM and returns the evaluation results containing the new state, the remaining fuel, and the number of - ticks for the evaluation of these messages. [message_counter_offset] is - used when we evaluate partial inboxes, such as during simulation. When - [reveal_map] is provided, it is used as an additional source of data for - revelation ticks. *) + ticks for the evaluation of these messages. If [messages] is empty, the + PVM progresses until the next input request (within the allocated + [fuel]). [message_counter_offset] is used when we evaluate partial + inboxes, such as during simulation. When [reveal_map] is provided, it is + used as an additional source of data for revelation ticks. *) val eval_messages : ?reveal_map:string Sc_rollup_reveal_hash.Map.t -> - fuel:fuel -> _ Node_context.t -> - message_counter_offset:int -> - PVM.state -> - Raw_level.t -> - Sc_rollup.Inbox_message.t list -> + eval_state -> eval_result Node_context.delayed_write tzresult Lwt.t end @@ -74,12 +88,18 @@ module Make (PVM : Pvm.S) = struct type fuel = F.t - type eval_result = { + type eval_state = { state : PVM.state; + state_hash : PVM.hash; + tick : Sc_rollup.Tick.t; + inbox_level : Raw_level.t; + message_counter_offset : int; remaining_fuel : fuel; - num_ticks : Z.t; + remaining_messages : Sc_rollup.Inbox_message.t list; } + type eval_result = {state : eval_state; num_ticks : Z.t; num_messages : int} + let get_reveal ~data_dir reveal_map hash = let found_in_map = match reveal_map with @@ -264,7 +284,7 @@ module Make (PVM : Pvm.S) = struct {input with Sc_rollup.payload} type feed_input_completion = - | Feed_input_aborted of {state : PVM.state; fuel : fuel} + | Feed_input_aborted of {state : PVM.state; fuel : fuel; fed_input : bool} | Feed_input_completed of {state : PVM.state; fuel : fuel} (** [feed_input node_ctxt reveal_map level message_index ~fuel @@ -289,11 +309,12 @@ module Make (PVM : Pvm.S) = struct state in match res with - | Aborted {state; fuel; _} -> return (Feed_input_aborted {state; fuel}) + | Aborted {state; fuel; _} -> + return (Feed_input_aborted {state; fuel; fed_input = false}) | Completed {state; fuel; current_tick = tick; failing_ticks} -> ( let open Delayed_write_monad.Lwt_result_syntax in match F.consume F.one_tick_consumption fuel with - | None -> return (Feed_input_aborted {state; fuel}) + | None -> return (Feed_input_aborted {state; fuel; fed_input = false}) | Some fuel -> ( let>* input, failing_ticks = match failing_ticks with @@ -324,7 +345,7 @@ module Make (PVM : Pvm.S) = struct in match res with | Aborted {state; fuel; _} -> - return (Feed_input_aborted {state; fuel}) + return (Feed_input_aborted {state; fuel; fed_input = true}) | Completed {state; fuel; _} -> return (Feed_input_completed {state; fuel}))) @@ -337,10 +358,11 @@ module Make (PVM : Pvm.S) = struct let rec feed_messages (state, fuel) message_index = function | [] -> (* Fed all messages *) - return (state, fuel) - | _messages when F.is_empty fuel -> + return (state, fuel, message_index - message_counter_offset, []) + | messages when F.is_empty fuel -> (* Consumed all fuel *) - return (state, fuel) + return + (state, fuel, message_index - message_counter_offset, messages) | message :: messages -> ( let*? payload = Sc_rollup.Inbox_message.( @@ -368,20 +390,30 @@ module Make (PVM : Pvm.S) = struct match res with | Feed_input_completed {state; fuel} -> feed_messages (state, fuel) (message_index + 1) messages - | Feed_input_aborted {state; fuel} -> return (state, fuel)) + | Feed_input_aborted {state; fuel; fed_input = false} -> + return + ( state, + fuel, + message_index - message_counter_offset, + message :: messages ) + | Feed_input_aborted {state; fuel; fed_input = true} -> + return + ( state, + fuel, + message_index + 1 - message_counter_offset, + messages )) in (feed_messages [@tailcall]) (state, fuel) message_counter_offset messages let eval_block_inbox ~fuel node_ctxt (inbox, messages) (state : PVM.state) : - (PVM.state * int * Raw_level.t * fuel) Node_context.delayed_write - tzresult - Lwt.t = + eval_result Node_context.delayed_write tzresult Lwt.t = + let open Lwt_result_syntax in let open Delayed_write_monad.Lwt_result_syntax in (* Obtain inbox and its messages for this block. *) let inbox_level = Inbox.inbox_level inbox in - let num_messages = List.length messages in + let*! initial_tick = PVM.get_tick state in (* Evaluate all the messages for this level. *) - let>* state, fuel = + let>* state, remaining_fuel, num_messages, remaining_messages = eval_messages ~reveal_map:None ~fuel @@ -391,26 +423,87 @@ module Make (PVM : Pvm.S) = struct inbox_level messages in - return (state, num_messages, inbox_level, fuel) + let*! final_tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in + let num_ticks = Sc_rollup.Tick.distance initial_tick final_tick in + let eval_state = + { + state; + state_hash; + tick = final_tick; + inbox_level; + message_counter_offset = num_messages; + remaining_fuel; + remaining_messages; + } + in + return {state = eval_state; num_ticks; num_messages} - let eval_messages ?reveal_map ~fuel node_ctxt ~message_counter_offset state - inbox_level messages = + let eval_messages ?reveal_map node_ctxt + { + state; + tick = initial_tick; + inbox_level; + message_counter_offset; + remaining_fuel = fuel; + remaining_messages = messages; + _; + } = let open Lwt_result_syntax in let open Delayed_write_monad.Lwt_result_syntax in - let*! initial_tick = PVM.get_tick state in - let>* state, remaining_fuel = - eval_messages - ~reveal_map - ~fuel - node_ctxt - ~message_counter_offset - state - inbox_level - messages + let>* state, remaining_fuel, num_messages, remaining_messages = + match messages with + | [] -> + let level = Raw_level.to_int32 inbox_level |> Int32.to_int in + let message_index = message_counter_offset - 1 in + let failing_ticks = + Loser_mode.is_failure + node_ctxt.Node_context.loser_mode + ~level + ~message_index + in + let>* res = + eval_until_input + node_ctxt + reveal_map + level + message_index + ~fuel + 0L + failing_ticks + state + in + let state, remaining_fuel = + match res with + | Aborted {state; fuel; _} | Completed {state; fuel; _} -> + (state, fuel) + in + return (state, remaining_fuel, 0, []) + | _ -> + eval_messages + ~reveal_map + ~fuel + node_ctxt + ~message_counter_offset + state + inbox_level + messages in let*! final_tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in let num_ticks = Sc_rollup.Tick.distance initial_tick final_tick in - return {state; remaining_fuel; num_ticks} + let eval_state = + { + state; + state_hash; + tick = final_tick; + inbox_level; + message_counter_offset = message_counter_offset + num_messages; + remaining_fuel; + remaining_messages; + } + in + return {state = eval_state; num_ticks; num_messages} end module Free = Make_fueled (Fuel.Free) diff --git a/src/proto_alpha/lib_sc_rollup_node/interpreter.ml b/src/proto_alpha/lib_sc_rollup_node/interpreter.ml index b3f5f911fe2f71b1c16981571af35cada149483a..5276c655db4c073485a0da6f3848c68756bf6052 100644 --- a/src/proto_alpha/lib_sc_rollup_node/interpreter.ml +++ b/src/proto_alpha/lib_sc_rollup_node/interpreter.ml @@ -45,9 +45,10 @@ module type S = sig val state_of_tick : _ Node_context.t -> + ?start_state:Accounted_pvm.eval_state -> Sc_rollup.Tick.t -> Raw_level.t -> - (PVM.state * PVM.hash) option tzresult Lwt.t + Accounted_pvm.eval_state option tzresult Lwt.t val state_of_head : 'a Node_context.t -> @@ -143,25 +144,24 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct inbox_messages predecessor_state in - let* state, num_messages, inbox_level, _fuel = + let* { + state = {state; state_hash; inbox_level; tick; _}; + num_messages; + num_ticks; + } = Delayed_write_monad.apply node_ctxt eval_result in let*! ctxt = PVM.State.set ctxt state in let*! initial_tick = PVM.get_tick predecessor_state in - let*! last_tick = PVM.get_tick state in - let num_ticks = - Sc_rollup.Tick.distance initial_tick last_tick |> Z.to_int64 - in (* Produce events. *) - let*! state_hash = PVM.state_hash state in let*! () = Interpreter_event.transitioned_pvm inbox_level state_hash - last_tick + tick num_messages in - return (ctxt, num_messages, num_ticks, initial_tick) + return (ctxt, num_messages, Z.to_int64 num_ticks, initial_tick) (** [process_head node_ctxt ctxt ~predecessor head] runs the PVM for the given head. *) @@ -184,11 +184,11 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct return (ctxt, 0, 0L, Sc_rollup.Tick.initial) else return (ctxt, 0, 0L, Sc_rollup.Tick.initial) - (** [run_for_ticks node_ctxt block tick_distance] starts the - evaluation of the inbox at [block] for at most [tick_distance]. *) - let run_for_ticks node_ctxt (block : Sc_rollup_block.t) tick_distance = + (** Returns the starting evaluation before the evaluation of the block. It + contains the PVM state at the end of the execution of the previous block + and the messages the block ([remaining_messages]). *) + let start_state_of_block node_ctxt (block : Sc_rollup_block.t) = let open Lwt_result_syntax in - let open Delayed_write_monad.Lwt_result_syntax in let pred_level = Raw_level.to_int32 block.header.level |> Int32.pred in let* ctxt = Node_context.checkout_context node_ctxt block.header.predecessor @@ -203,6 +203,9 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct let* {is_migration_block; predecessor; predecessor_timestamp; messages} = Node_context.get_messages node_ctxt block.header.inbox_witness in + let inbox_level = Sc_rollup.Inbox.inbox_level inbox in + let*! tick = PVM.get_tick state in + let*! state_hash = PVM.state_hash state in let messages = let open Sc_rollup.Inbox_message in Internal Start_of_level @@ -214,33 +217,99 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct :: messages @ [Internal End_of_level] in - let>* state, _counter, _level, _fuel = - Accounted_pvm.eval_block_inbox - ~fuel:(Fuel.Accounted.of_ticks tick_distance) + return + Accounted_pvm. + { + state; + state_hash; + inbox_level; + tick; + message_counter_offset = 0; + remaining_fuel = Fuel.Accounted.of_ticks 0L; + remaining_messages = messages; + } + + (** [run_for_ticks node_ctxt start_state tick_distance] starts the evaluation + of messages in the [start_state] for at most [tick_distance]. *) + let run_to_tick node_ctxt start_state tick = + let open Delayed_write_monad.Lwt_result_syntax in + let tick_distance = + Sc_rollup.Tick.distance tick start_state.Accounted_pvm.tick |> Z.to_int64 + in + let>+ eval_result = + Accounted_pvm.eval_messages node_ctxt - (inbox, messages) - state + { + start_state with + remaining_fuel = Fuel.Accounted.of_ticks tick_distance; + } in - return state + eval_result.state + + let state_of_tick_aux node_ctxt ~start_state (event : Sc_rollup_block.t) tick + = + let open Lwt_result_syntax in + let* start_state = + match start_state with + | Some start_state + when Raw_level.( + start_state.Accounted_pvm.inbox_level = event.header.level) -> + return start_state + | _ -> + (* Recompute start state on level change or if we don't have a + starting state on hand. *) + start_state_of_block node_ctxt event + in + (* TODO: #3384 + We should test that we always have enough blocks to find the tick + because [state_of_tick] is a critical function. *) + let* result_state = run_to_tick node_ctxt start_state tick in + let result_state = Delayed_write_monad.ignore result_state in + return result_state + + (* The cache allows cache intermediate states of the PVM in e.g. dissections. *) + module Tick_state_cache = + Aches_lwt.Lache.Make + (Aches.Rache.Transfer + (Aches.Rache.LRU) + (struct + type t = Sc_rollup.Tick.t * Block_hash.t + + let equal (t1, b1) (t2, b2) = + Sc_rollup.Tick.(t1 = t2) && Block_hash.(b1 = b2) + + let hash (tick, block) = + ((Sc_rollup.Tick.to_z tick |> Z.hash) * 13) + Block_hash.hash block + end)) + + let tick_state_cache = Tick_state_cache.create 64 (* size of 2 dissections *) + + (* Memoized version of [state_of_tick_aux]. *) + let memo_state_of_tick_aux node_ctxt ~start_state (event : Sc_rollup_block.t) + tick = + Tick_state_cache.bind_or_put + tick_state_cache + (tick, event.header.block_hash) + (fun (tick, _hash) -> state_of_tick_aux node_ctxt ~start_state event tick) + Lwt.return - (** [state_of_tick node_ctxt tick level] returns [Some (state, hash)] for a - given [tick] if this [tick] happened before [level]. Otherwise, returns - [None].*) - let state_of_tick node_ctxt tick level = + (** [state_of_tick node_ctxt ?start_state tick level] returns [Some end_state] + for a given [tick] if this [tick] happened before [level]. Otherwise, + returns [None].*) + let state_of_tick node_ctxt ?start_state tick level = let open Lwt_result_syntax in let* event = Node_context.block_with_tick node_ctxt ~max_level:level tick in match event with | None -> return_none | Some event -> assert (Raw_level.(event.header.level <= level)) ; - let tick_distance = - Sc_rollup.Tick.distance tick event.initial_tick |> Z.to_int64 + let* result_state = + if Node_context.is_loser node_ctxt then + (* TODO: https://gitlab.com/tezos/tezos/-/issues/5253 + The failures/loser mode does not work properly when restarting + from intermediate states. *) + state_of_tick_aux node_ctxt ~start_state:None event tick + else memo_state_of_tick_aux node_ctxt ~start_state event tick in - (* TODO: #3384 - We should test that we always have enough blocks to find the tick - because [state_of_tick] is a critical function. *) - let* state = run_for_ticks node_ctxt event tick_distance in - let state = Delayed_write_monad.ignore state in - let*! hash = PVM.state_hash state in - return (Some (state, hash)) + return_some result_state end diff --git a/src/proto_alpha/lib_sc_rollup_node/interpreter.mli b/src/proto_alpha/lib_sc_rollup_node/interpreter.mli index 71cdbcabd249910063015f09d45fc8b4219353f3..3bc4a0aa7aeeb0ec00405fe6dd0dc0fd8d3d1e08 100644 --- a/src/proto_alpha/lib_sc_rollup_node/interpreter.mli +++ b/src/proto_alpha/lib_sc_rollup_node/interpreter.mli @@ -50,14 +50,16 @@ module type S = sig Sc_rollup.Inbox.t * Sc_rollup.Inbox_message.t list -> ('a Context.t * int * int64 * Sc_rollup.Tick.t) tzresult Lwt.t - (** [state_of_tick node_ctxt tick level] returns [Some (state, hash)] - for a given [tick] if this [tick] happened before - [level]. Otherwise, returns [None].*) + (** [state_of_tick node_ctxt ?start_state tick level] returns [Some (state, + hash)] for a given [tick] if this [tick] happened before + [level]. Otherwise, returns [None]. If provided, the evaluation is resumed + from [start_state]. *) val state_of_tick : _ Node_context.t -> + ?start_state:Accounted_pvm.eval_state -> Sc_rollup.Tick.t -> Raw_level.t -> - (PVM.state * PVM.hash) option tzresult Lwt.t + Accounted_pvm.eval_state option tzresult Lwt.t (** [state_of_head node_ctxt ctxt head] returns the state corresponding to the block [head], or the state at rollup genesis if the block is before the diff --git a/src/proto_alpha/lib_sc_rollup_node/node_context.ml b/src/proto_alpha/lib_sc_rollup_node/node_context.ml index 93b1838964d106b0780d765fa60b7f8f8d3636b5..f722a271d21143f55baf52628b919636ab221da2 100644 --- a/src/proto_alpha/lib_sc_rollup_node/node_context.ml +++ b/src/proto_alpha/lib_sc_rollup_node/node_context.ml @@ -66,6 +66,8 @@ let is_operator node_ctxt pkh = let is_accuser {mode; _} = mode = Accuser +let is_loser {loser_mode; _} = loser_mode <> Loser_mode.no_failures + let get_fee_parameter node_ctxt purpose = Configuration.Operator_purpose_map.find purpose node_ctxt.fee_parameters |> Option.value ~default:(Configuration.default_fee_parameter ~purpose ()) diff --git a/src/proto_alpha/lib_sc_rollup_node/node_context.mli b/src/proto_alpha/lib_sc_rollup_node/node_context.mli index 20a8070e8bde3f115f54f08f3fddbef18d8ada0d..c2bbebd6f04dd31159d55d3c9d06e41318103d59 100644 --- a/src/proto_alpha/lib_sc_rollup_node/node_context.mli +++ b/src/proto_alpha/lib_sc_rollup_node/node_context.mli @@ -92,6 +92,10 @@ val is_operator : _ t -> Signature.Public_key_hash.t -> bool mode. *) val is_accuser : _ t -> bool +(** [is_loser node_ctxt] returns [true] if the rollup node runs has some + failures planned. *) +val is_loser : _ t -> bool + (** [get_fee_parameter cctxt purpose] returns the fee parameter to inject an operation for a given [purpose]. If no specific fee parameters were configured for this purpose, returns the default fee parameter for this diff --git a/src/proto_alpha/lib_sc_rollup_node/refutation_game.ml b/src/proto_alpha/lib_sc_rollup_node/refutation_game.ml index 6e0e114562e77ddd5c5fdbd3e39335b8bfe6916e..9e145022763ec3ca33f343abbd8b255a10a2b8d4 100644 --- a/src/proto_alpha/lib_sc_rollup_node/refutation_game.ml +++ b/src/proto_alpha/lib_sc_rollup_node/refutation_game.ml @@ -303,25 +303,43 @@ module Make (Interpreter : Interpreter.S) : in if Result.is_ok res then return proof else assert false + type pvm_intermediate_state = + | Hash of PVM.hash + | Evaluated of Interpreter.Accounted_pvm.eval_state + let new_dissection ~opponent ~default_number_of_sections node_ctxt last_level ok our_view = let open Lwt_result_syntax in - let state_hash_from_tick tick = - let* r = Interpreter.state_of_tick node_ctxt tick last_level in - return (Option.map snd r) + let state_of_tick ?start_state tick = + Interpreter.state_of_tick node_ctxt ?start_state tick last_level + in + let state_hash_of_eval_state Interpreter.Accounted_pvm.{state_hash; _} = + state_hash + in + let start_hash, start_tick, start_state = + match ok with + | Hash hash, tick -> (hash, tick, None) + | Evaluated ({state_hash; _} as state), tick -> + (state_hash, tick, Some state) in - let start_hash, start_tick = ok in let start_chunk = Sc_rollup.Dissection_chunk. {state_hash = Some start_hash; tick = start_tick} in - let start_hash, start_tick = our_view in + let our_state, our_tick = our_view in + let our_state_hash = + Option.map + (fun Interpreter.Accounted_pvm.{state_hash; _} -> state_hash) + our_state + in let our_stop_chunk = - Sc_rollup.Dissection_chunk.{state_hash = start_hash; tick = start_tick} + Sc_rollup.Dissection_chunk.{state_hash = our_state_hash; tick = our_tick} in let* dissection = Game_helpers.make_dissection - ~state_hash_from_tick + ~state_of_tick + ~state_hash_of_eval_state + ?start_state ~start_chunk ~our_stop_chunk @@ PVM.new_dissection @@ -332,8 +350,8 @@ module Make (Interpreter : Interpreter.S) : let*! () = Refutation_game_event.computed_dissection ~opponent - ~start_tick:(snd ok) - ~end_tick:(snd our_view) + ~start_tick + ~end_tick:our_tick dissection in return dissection @@ -356,26 +374,32 @@ module Make (Interpreter : Interpreter.S) : .Unreliable_tezos_node_returning_inconsistent_game | Sc_rollup.Dissection_chunk.{state_hash = their_hash; tick} :: dissection -> ( - let open Lwt_result_syntax in + let start_state = + match ok with + | Hash _, _ -> None + | Evaluated ok_state, _ -> Some ok_state + in let* our = - Interpreter.state_of_tick node_ctxt tick game.inbox_level + Interpreter.state_of_tick + node_ctxt + ?start_state + tick + game.inbox_level in match (their_hash, our) with | None, None -> (* This case is absurd since: [None] can only occur at the end and the two players disagree about the end. *) assert false - | Some _, None | None, Some _ -> - return (ok, (Option.map snd our, tick)) - | Some their_hash, Some (_, our_hash) -> + | Some _, None | None, Some _ -> return (ok, (our, tick)) + | Some their_hash, Some ({state_hash = our_hash; _} as our_state) -> if Sc_rollup.State_hash.equal our_hash their_hash then - traverse (their_hash, tick) dissection - else return (ok, (Some our_hash, tick))) + traverse (Evaluated our_state, tick) dissection + else return (ok, (our, tick))) in match dissection with | Sc_rollup.Dissection_chunk.{state_hash = Some hash; tick} :: dissection -> - let* ok, ko = traverse (hash, tick) dissection in - let choice = snd ok in + let* ok, ko = traverse (Hash hash, tick) dissection in let* dissection = new_dissection ~opponent @@ -385,7 +409,9 @@ module Make (Interpreter : Interpreter.S) : ok ko in - let chosen_section_len = Sc_rollup.Tick.distance (snd ko) choice in + let _, choice = ok in + let _, ko_tick = ko in + let chosen_section_len = Sc_rollup.Tick.distance ko_tick choice in return (choice, chosen_section_len, dissection) | [] | {state_hash = None; _} :: _ -> (* @@ -408,7 +434,7 @@ module Make (Interpreter : Interpreter.S) : tzfail Sc_rollup_node_errors .Unreliable_tezos_node_returning_inconsistent_game - | Some (start_state, _start_hash) -> + | Some {state = start_state; _} -> let* proof = generate_proof node_ctxt game start_state in let*? pvm_step = Sc_rollup.Proof.serialize_pvm_step ~pvm:(module PVM) proof.pvm_step diff --git a/src/proto_alpha/lib_sc_rollup_node/sc_rollup_node_errors.ml b/src/proto_alpha/lib_sc_rollup_node/sc_rollup_node_errors.ml index ea53d100398426c95e0e1f2a039e763a8399adec..c2b5d2a399469978bc5d8c776637d9e909043b3a 100644 --- a/src/proto_alpha/lib_sc_rollup_node/sc_rollup_node_errors.ml +++ b/src/proto_alpha/lib_sc_rollup_node/sc_rollup_node_errors.ml @@ -93,16 +93,19 @@ let () = (fun (inbox_level, ours, on_l1) -> Disagree_with_cemented {inbox_level; ours; on_l1}) ; + let description = + "Internal error: The game invariant states that the dissection from the \ + opponent must contain a tick we disagree with. If the retrieved game does \ + not respect this, we cannot trust the Tezos node we are connected to and \ + prefer to stop here." + in register_error_kind `Permanent ~id:"internal.unreliable_tezos_node" ~title:"Internal error: Tezos node seems unreliable" - ~description: - "Internal error: The game invariant states that the dissection from the \ - opponent must contain a tick we disagree with. If the retrieved game \ - does not respect this, we cannot trust the Tezos node we are connected \ - to and prefer to stop here." - ~pp:(fun _ppf () -> ()) + ~description + ~pp:(fun ppf () -> + Format.fprintf ppf "Unreliable Tezos node. %s" description) Data_encoding.unit (function | Unreliable_tezos_node_returning_inconsistent_game -> Some () | _ -> None) diff --git a/src/proto_alpha/lib_sc_rollup_node/simulation.ml b/src/proto_alpha/lib_sc_rollup_node/simulation.ml index c6facce09a1140a1e46adeb9006d92d9c16c37fe..58d0fbdebe064a6025c499d2e594cf8e7bd66666 100644 --- a/src/proto_alpha/lib_sc_rollup_node/simulation.ml +++ b/src/proto_alpha/lib_sc_rollup_node/simulation.ml @@ -133,23 +133,29 @@ module Make (Interpreter : Interpreter.S) : info_per_level = _; } as sim) messages = let open Lwt_result_syntax in + let*! state_hash = PVM.state_hash state in + let*! tick = PVM.get_tick state in + let eval_state = + Fueled_pvm. + { + state; + state_hash; + tick; + inbox_level; + message_counter_offset = nb_messages_inbox; + remaining_fuel = Fuel.Free.of_ticks 0L; + remaining_messages = messages; + } + in (* Build new state *) let* eval_result = - Fueled_pvm.eval_messages - ?reveal_map - ~fuel:(Fuel.Free.of_ticks 0L) - node_ctxt - ~message_counter_offset:nb_messages_inbox - state - inbox_level - messages + Fueled_pvm.eval_messages ?reveal_map node_ctxt eval_state in - let Fueled_pvm.{state; num_ticks; _} = + let Fueled_pvm.{state = {state; _}; num_ticks; num_messages; _} = Delayed_write_monad.ignore eval_result in let*! ctxt = PVM.State.set ctxt state in - let nb_messages = List.length messages in - let nb_messages_inbox = nb_messages_inbox + nb_messages in + let nb_messages_inbox = nb_messages_inbox + num_messages in return ({sim with ctxt; state; nb_messages_inbox}, num_ticks) let simulate_messages (node_ctxt : Node_context.ro) sim messages = diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 576b03c1552093bbbe8ffb2130640124b66cce8b..5e7162ad85a202aad329df51e53b67b329f7b8dd 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -209,7 +209,7 @@ let wait_for_conflict_detected sc_node = let wait_for_timeout_detected sc_node = Sc_rollup_node.wait_for sc_node "sc_rollup_node_timeout_detected.v0" @@ fun json -> - let other = JSON.(json |-> "other" |> as_string) in + let other = JSON.(json |> as_string) in Some other (** Wait for the rollup node to compute a dissection *)