diff --git a/src/proto_alpha/bin_sc_rollup_node/arith_pvm.ml b/src/proto_alpha/bin_sc_rollup_node/arith_pvm.ml index 2728d785fee1c1a4a2014cb0767f55fc63c45405..459fb8b16df8b1061c12e92502150f7b0f9ade83 100644 --- a/src/proto_alpha/bin_sc_rollup_node/arith_pvm.ml +++ b/src/proto_alpha/bin_sc_rollup_node/arith_pvm.ml @@ -36,6 +36,11 @@ module Arith_proof_format = struct type proof = IStoreProof.Proof.tree IStoreProof.Proof.t + let context_hash_to_state_hash h = + Sc_rollup.State_hash.hash_bytes [Context_hash.to_bytes h] + + let hash_tree tree = context_hash_to_state_hash (IStoreTree.hash tree) + let verify_proof proof step = (* The rollup node is not supposed to verify proof. We keep this part in case this changes in the future. *) @@ -50,6 +55,12 @@ module Arith_proof_format = struct let produce_proof context tree step = let open Lwt_syntax in + let time = Time.Protocol.epoch in + let info () = + IStore.Info.v ~author:"Tezos" (Time.Protocol.to_seconds time) ~message:"" + in + let* _ = IStore.set_tree ~info context [] tree in + let* _commit_key = Store.commit ~time context in match IStoreTree.kinded_key tree with | Some k -> let* p = IStoreProof.produce_tree_proof (IStore.repo context) k step in @@ -58,8 +69,7 @@ module Arith_proof_format = struct let kinded_hash_to_state_hash : IStoreProof.Proof.kinded_hash -> Sc_rollup.State_hash.t = function - | `Value hash | `Node hash -> - Sc_rollup.State_hash.hash_bytes [Context_hash.to_bytes hash] + | `Value hash | `Node hash -> context_hash_to_state_hash hash let proof_before proof = kinded_hash_to_state_hash proof.IStoreProof.Proof.before diff --git a/src/proto_alpha/bin_sc_rollup_node/commitment.ml b/src/proto_alpha/bin_sc_rollup_node/commitment.ml index a246eb5dd4ff80a927d0f56cc46fc020b801a7cb..03a309b47b9394133121547cae4ff6cf7a4c7c50 100644 --- a/src/proto_alpha/bin_sc_rollup_node/commitment.ml +++ b/src/proto_alpha/bin_sc_rollup_node/commitment.ml @@ -51,6 +51,11 @@ module type Mutable_level_store = node, as only finalized heads are processed to build commitments. *) +(* FIXME: #3203 + + Using these global variables is fragile considering chain + reorganizations and interruptions. We should use a more persistent + representations for this piece of information. *) module Mutable_counter = struct module Make () = struct let x = ref Z.zero @@ -225,7 +230,9 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct let update_ticks_and_messages store block_hash = let open Lwt_result_syntax in - let*! {num_messages; num_ticks} = Store.StateInfo.get store block_hash in + let*! {num_messages; num_ticks; initial_tick = _} = + Store.StateInfo.get store block_hash + in let () = Number_of_messages.add num_messages in return @@ Number_of_ticks.add num_ticks @@ -277,7 +284,7 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct commitment.predecessor lcc_hash in - exit 1 + Lwt_exit.exit_and_raise 1 else Lwt.return () in let* source, src_pk, src_sk = Node_context.get_operator_keys node_ctxt in diff --git a/src/proto_alpha/bin_sc_rollup_node/components.ml b/src/proto_alpha/bin_sc_rollup_node/components.ml index fd412ff4aef56357b420d327fa59d5b6997e68d2..79c4e5ea5f76126bb39724ca369f20e317f20f57 100644 --- a/src/proto_alpha/bin_sc_rollup_node/components.ml +++ b/src/proto_alpha/bin_sc_rollup_node/components.ml @@ -32,6 +32,8 @@ module type S = sig module Commitment : Commitment_sig.S with module PVM = PVM module RPC_server : RPC_server.S with module PVM = PVM + + module Refutation_game : Refutation_game.S with module PVM = PVM end module Make (PVM : Pvm.S) : S with module PVM = PVM = struct @@ -39,6 +41,7 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct module Interpreter = Interpreter.Make (PVM) module Commitment = Commitment.Make (PVM) module RPC_server = RPC_server.Make (PVM) + module Refutation_game = Refutation_game.Make (PVM) end let pvm_of_kind : Protocol.Alpha_context.Sc_rollup.Kind.t -> (module Pvm.S) = diff --git a/src/proto_alpha/bin_sc_rollup_node/configuration.ml b/src/proto_alpha/bin_sc_rollup_node/configuration.ml index af7f25564bbd73ad9522c1b9a1f5256e46146c32..a6b674343eed4dd9c95115f3fe45df5a94e545c5 100644 --- a/src/proto_alpha/bin_sc_rollup_node/configuration.ml +++ b/src/proto_alpha/bin_sc_rollup_node/configuration.ml @@ -33,6 +33,7 @@ type t = { rpc_addr : string; rpc_port : int; fee_parameter : Injection.fee_parameter; + loser_mode : Loser_mode.t; } let default_data_dir = @@ -147,19 +148,22 @@ let encoding : t Data_encoding.t = rpc_addr; rpc_port; fee_parameter; + loser_mode; } -> ( data_dir, sc_rollup_address, sc_rollup_node_operator, rpc_addr, rpc_port, - fee_parameter )) + fee_parameter, + loser_mode )) (fun ( data_dir, sc_rollup_address, sc_rollup_node_operator, rpc_addr, rpc_port, - fee_parameter ) -> + fee_parameter, + loser_mode ) -> { data_dir; sc_rollup_address; @@ -167,8 +171,9 @@ let encoding : t Data_encoding.t = rpc_addr; rpc_port; fee_parameter; + loser_mode; }) - (obj6 + (obj7 (dft "data-dir" ~description:"Location of the data dir" @@ -189,9 +194,27 @@ let encoding : t Data_encoding.t = "fee-parameter" ~description:"The fee parameter used when injecting operations in L1" fee_parameter_encoding - default_fee_parameter)) + default_fee_parameter) + (dft + "loser-mode" + ~description: + "If enabled, the rollup node will issue wrong commitments (for \ + test only!)" + Loser_mode.encoding + Loser_mode.no_failures)) + +let loser_warning_message config = + if config.loser_mode <> Loser_mode.no_failures then + Format.printf + {| +************ WARNING ************* +This rollup node is in loser mode. +This should be used for test only! +************ WARNING ************* +|} let save config = + loser_warning_message config ; let open Lwt_syntax in let json = Data_encoding.Json.construct encoding config in let* () = Lwt_utils_unix.create_dir config.data_dir in @@ -200,4 +223,6 @@ let save config = let load ~data_dir = let open Lwt_result_syntax in let+ json = Lwt_utils_unix.Json.read_file (relative_filename data_dir) in - Data_encoding.Json.destruct encoding json + let config = Data_encoding.Json.destruct encoding json in + loser_warning_message config ; + config diff --git a/src/proto_alpha/bin_sc_rollup_node/configuration.mli b/src/proto_alpha/bin_sc_rollup_node/configuration.mli index 23bb4576d681ad4a6bbe55911e573a3bd2f29692..82fd3d2557ebf51479307debedb7eac54b09dee1 100644 --- a/src/proto_alpha/bin_sc_rollup_node/configuration.mli +++ b/src/proto_alpha/bin_sc_rollup_node/configuration.mli @@ -31,6 +31,7 @@ type t = { rpc_addr : string; rpc_port : int; fee_parameter : Injection.fee_parameter; + loser_mode : Loser_mode.t; } (** [default_data_dir] is the default value for [data_dir]. *) diff --git a/src/proto_alpha/bin_sc_rollup_node/daemon.ml b/src/proto_alpha/bin_sc_rollup_node/daemon.ml index 7be120b5f4f25665686c7fc60744f64dd071c8b3..561923cafe5779620f7a9c51f57234f6698243bd 100644 --- a/src/proto_alpha/bin_sc_rollup_node/daemon.ml +++ b/src/proto_alpha/bin_sc_rollup_node/daemon.ml @@ -71,25 +71,53 @@ module Make (PVM : Pvm.S) = struct *) let open Lwt_result_syntax in let {finalized; seen_before; head} = head_state in - let* () = + let cctxt = node_ctxt.Node_context.cctxt in + let* _operations = let*! () = emit_head_processing_event head_state in - if seen_before then return_unit + (* Avoid processing inbox again if it has been processed before for this head *) + if seen_before then return None else - (* Avoid processing inbox again if it has been processed before for this head *) - let* () = Inbox.process_head node_ctxt store head in + let* operations = + let open Layer1_services in + let (Head {level; _}) = head in + (* It would be a security problem to ignore operation + receipt because metadata are too large. For this reason, + we set [force_metadata] to [true] to make sure the + receipts are always accessible to the rollup node. *) + Operations.operations + ~force_metadata:true + cctxt + ~chain:`Main + ~block:(`Level level) + () + in + let*! () = emit_head_processing_event head_state in + let* () = Inbox.process_head node_ctxt store head operations in (* Avoid storing and publishing commitments if the head is not final *) (* Avoid triggering the pvm execution if this has been done before for this head *) - Components.Interpreter.process_head node_ctxt store head + let* () = Components.Interpreter.process_head node_ctxt store head in + return_some operations in let* () = - if finalized then Components.Commitment.process_head node_ctxt store head - else return_unit + when_ finalized @@ fun () -> + Components.Commitment.process_head node_ctxt store head in (* Publishing a commitment when one is available does not depend on the state of the current head, but we still need to ensure that the node only published one commitment per block. *) let* () = Components.Commitment.publish_commitment node_ctxt store in - Components.Commitment.cement_commitment_if_possible node_ctxt store head + let* () = + Components.Commitment.cement_commitment_if_possible node_ctxt store head + in + let* () = + (* At each block, there may be some refutation related actions to + be performed. *) + when_ finalized @@ fun () -> + Components.Refutation_game.process head node_ctxt store + in + when_ finalized (fun () -> + let*! () = Layer1.mark_processed_head store head in + return ()) (* [on_layer_1_chain_event node_ctxt store chain_event old_heads] processes a list of heads, coming from either a list of [old_heads] or from the current @@ -187,9 +215,12 @@ module Make (PVM : Pvm.S) = struct @@ iter_stream layer_1_chain_events @@ on_layer_1_chain_event node_ctxt store - let install_finalizer store rpc_server = + let install_finalizer store rpc_server heads stopper = let open Lwt_syntax in Lwt_exit.register_clean_up_callback ~loc:__LOC__ @@ fun exit_status -> + stopper () ; + let* () = Lwt_stream.closed heads in + let* () = Layer1.shutdown store in let* () = Components.RPC_server.shutdown rpc_server in let* () = Store.close store in let* () = Event.shutdown_node exit_status in @@ -201,13 +232,13 @@ module Make (PVM : Pvm.S) = struct let* rpc_server = Components.RPC_server.start node_ctxt store configuration in - let* tezos_heads = + let* tezos_heads, stopper = Layer1.start configuration node_ctxt.Node_context.cctxt store in let*! () = Inbox.start () in let*! () = Components.Commitment.start () in - let _ = install_finalizer store rpc_server in + let _ = install_finalizer store rpc_server tezos_heads stopper in let*! () = Event.node_is_ready ~rpc_addr:configuration.rpc_addr @@ -223,16 +254,14 @@ let run ~data_dir (cctxt : Protocol_client_context.full) = let*! () = Event.starting_node () in let* configuration = Configuration.load ~data_dir in let open Configuration in - let {sc_rollup_address; sc_rollup_node_operator; fee_parameter; _} = - configuration - in let*! store = Store.load configuration in let* node_ctxt = Node_context.init cctxt - sc_rollup_address - sc_rollup_node_operator - fee_parameter + configuration.sc_rollup_address + configuration.sc_rollup_node_operator + configuration.fee_parameter + ~loser_mode:configuration.loser_mode in let* _pkh, _pk, _skh = Node_context.get_operator_keys node_ctxt in (* Check that the public key hash is valid. *) diff --git a/src/proto_alpha/bin_sc_rollup_node/inbox.ml b/src/proto_alpha/bin_sc_rollup_node/inbox.ml index a51e99ba6f721d78c27500ada5370279a150fce6..c3e1136762f06a3163fc73d493b7d394f400397b 100644 --- a/src/proto_alpha/bin_sc_rollup_node/inbox.ml +++ b/src/proto_alpha/bin_sc_rollup_node/inbox.ml @@ -28,17 +28,9 @@ *) open Protocol open Alpha_context -module Block_services = Block_services.Make (Protocol) (Protocol) let lift = Lwt.map Environment.wrap_tzresult -let head_processing_failure e = - Format.eprintf - "Error during head processing: @[%a@]" - Error_monad.(TzTrace.pp_print_top pp) - e ; - Lwt_exit.exit_and_raise 1 - module State = struct let add_messages = Store.Messages.add @@ -68,65 +60,56 @@ module State = struct let set_message_tree = Store.MessageTrees.set end -let get_messages cctxt head rollup = - let open Lwt_result_syntax in - let open Block_services in - let+ operations = - Operations.operations cctxt ~chain:`Main ~block:(`Level (snd head)) () - in - let is_add_message = function - | Contents - (Manager_operation - {operation = Sc_rollup_add_messages {rollup = rollup'; messages}; _}) +let get_messages rollup operations = + let apply (type kind) accu ~source:_ (operation : kind manager_operation) + _result = + match operation with + | Sc_rollup_add_messages {rollup = rollup'; messages} when Sc_rollup.Address.(rollup' = rollup) -> - messages - | _ -> [] + messages :: accu + | _ -> accu in - let process_contents {protocol_data = Operation_data {contents; _}; _} = - let operations = Operation.to_list (Contents_list contents) in - List.concat_map is_add_message operations + let apply_internal (type kind) accu ~source:_ + (_operation : kind Apply_results.internal_manager_operation) _result = + accu in - let process_operations operations = - List.concat_map process_contents operations - in - List.concat_map process_operations operations - -let process_head Node_context.({cctxt; rollup_address; _} as node_ctxt) store - Layer1.(Head {level; hash = head_hash} as head) = + Layer1_services.process_applied_operations + [] + operations + {apply; apply_internal} + |> List.rev |> List.flatten + +let process_head (Node_context.{rollup_address; _} as node_ctxt) store + Layer1.(Head {level; hash = head_hash} as head) operations = let open Lwt_result_syntax in - let*! res = get_messages cctxt (head_hash, level) rollup_address in - match res with - | Error e -> head_processing_failure e - | Ok messages -> - let*! () = - Inbox_event.get_messages head_hash level (List.length messages) - in - let*! () = State.add_messages store head_hash messages in - (* + let messages = get_messages rollup_address operations in + let*! () = Inbox_event.get_messages head_hash level (List.length messages) in + let*! () = State.add_messages store head_hash messages in + (* We compute the inbox of this block using the inbox of its predecessor. That way, the computation of inboxes is robust to chain reorganization. *) - let*! predecessor = Layer1.predecessor store head in - let*! inbox = State.inbox_of_hash node_ctxt store predecessor in - lift - @@ let*! history = State.history_of_hash store predecessor in - let*! messages_tree = State.get_message_tree store predecessor in - let*? level = Raw_level.of_int32 level in - let* messages_tree, history, inbox = - Store.Inbox.add_external_messages - history - inbox - level - messages - messages_tree - in - let*! () = State.set_message_tree store head_hash messages_tree in - let*! () = State.add_inbox store head_hash inbox in - let*! () = State.add_history store head_hash history in - return_unit + let*! predecessor = Layer1.predecessor store head in + let*! inbox = State.inbox_of_hash node_ctxt store predecessor in + lift + @@ let*! history = State.history_of_hash store predecessor in + let*! messages_tree = State.get_message_tree store predecessor in + let*? level = Raw_level.of_int32 level in + let* messages_tree, history, inbox = + Store.Inbox.add_external_messages + history + inbox + level + messages + messages_tree + in + let*! () = State.set_message_tree store head_hash messages_tree in + let*! () = State.add_inbox store head_hash inbox in + let*! () = State.add_history store head_hash history in + return_unit let inbox_of_hash = State.inbox_of_hash diff --git a/src/proto_alpha/bin_sc_rollup_node/inbox.mli b/src/proto_alpha/bin_sc_rollup_node/inbox.mli index 162ebf02e44fbb422b2fecd88e187637ea2ffced..f0622e64ce0b204925858925f7a1c2f1b5c29542 100644 --- a/src/proto_alpha/bin_sc_rollup_node/inbox.mli +++ b/src/proto_alpha/bin_sc_rollup_node/inbox.mli @@ -34,13 +34,15 @@ *) open Protocol -(** [process_head node_ctxt store head] changes the state of the inbox to react to - [head]. In particular, this process requests the tezos node - through the context client [node_ctxt.cctxt] to retrieve the messages published - at the level indicated by [head]. *) - +(** [process_head node_ctxt store head operations] changes the state + of the inbox to react to [head]. In particular, this process + filters the provided [operations] of the [head] block. *) val process_head : - Node_context.t -> Store.t -> Layer1.head -> unit tzresult Lwt.t + Node_context.t -> + Store.t -> + Layer1.head -> + Layer1_services.operation list list -> + unit tzresult Lwt.t (** [inbox_of_hash node_ctxt store block_hash] returns the rollup inbox at the end of the given validation of [block_hash]. *) diff --git a/src/proto_alpha/bin_sc_rollup_node/interpreter.ml b/src/proto_alpha/bin_sc_rollup_node/interpreter.ml index a1a9cb4a25c57759e391dec6b72d350f4dc439df..4abb239fcf1c08ffa268398fada1e09e843443d6 100644 --- a/src/proto_alpha/bin_sc_rollup_node/interpreter.ml +++ b/src/proto_alpha/bin_sc_rollup_node/interpreter.ml @@ -28,102 +28,214 @@ open Alpha_context module Inbox = Store.Inbox module type S = sig + module PVM : Pvm.S + (** [process_head node_ctxt store head] interprets the messages associated with a [head] from a chain [event]. This requires the inbox to be updated beforehand. *) val process_head : Node_context.t -> Store.t -> Layer1.head -> unit tzresult Lwt.t + + (** [state_of_tick node_ctxt store tick level] returns [Some (state, hash)] + for a given [tick] if this [tick] happened before + [level]. Otherwise, returns [None].*) + val state_of_tick : + Node_context.t -> + Store.t -> + Sc_rollup.Tick.t -> + Raw_level.t -> + (PVM.state * PVM.hash) option tzresult Lwt.t end -module Make (PVM : Pvm.S) : S = struct +module Make (PVM : Pvm.S) : S with module PVM = PVM = struct module PVM = PVM - (** [eval_until_input state] advances a PVM [state] until it wants more inputs. *) - let eval_until_input state = + let consume_fuel = Option.map pred + + let continue_with_fuel fuel state f = let open Lwt_syntax in - let rec go state = + match fuel with + | Some 0 -> return (state, fuel) + | _ -> f (consume_fuel fuel) state + + (** [eval_until_input level message_index ?fuel start_tick + failing_ticks state] advances a PVM [state] until it wants more + inputs or there are no more [fuel] (if [Some fuel] is + specified). The evaluation is running under the processing of + some [message_index] at a given [level] and this is the + [start_tick] of this message processing. If some [failing_ticks] + are planned by the loser mode, they will be made. *) + let eval_until_input level message_index ?fuel start_tick failing_ticks state + = + let open Lwt_syntax in + let eval_tick tick failing_ticks state = + let normal_eval state = + let* state = PVM.eval state in + return (state, failing_ticks) + in + let failure_insertion_eval state failing_ticks' = + let* () = + Interpreter_event.intended_failure + ~level + ~message_index + ~message_tick:tick + ~internal:true + in + let* state = PVM.Internal_for_tests.insert_failure state in + return (state, failing_ticks') + in + match failing_ticks with + | xtick :: failing_ticks' -> + if xtick = tick then failure_insertion_eval state failing_ticks' + else normal_eval state + | _ -> normal_eval state + in + let rec go fuel tick failing_ticks state = let* input_request = PVM.is_input_state state in - match input_request with - | No_input_required -> - let* next_state = PVM.eval state in - go next_state - | _ -> return state + match fuel with + | Some 0 -> return (state, fuel, tick, failing_ticks) + | None | Some _ -> ( + match input_request with + | No_input_required -> + let* next_state, failing_ticks = + eval_tick tick failing_ticks state + in + go (consume_fuel fuel) (tick + 1) failing_ticks next_state + | _ -> return (state, fuel, tick, failing_ticks)) in - go state + go fuel start_tick failing_ticks state - (** [feed_input state input] feeds [input] to the PVM in order to advance [state] to the next step - that requires an input. *) - let feed_input state input = + let mutate input = {input with Sc_rollup.payload = "0xC0C0"} + + (** [feed_input state input] feeds [input] to the PVM in order to + advance [state] to the next step that requires an input. *) + let feed_input level message_index ?fuel ~failing_ticks state input = let open Lwt_syntax in - let* state = eval_until_input state in + let* state, fuel, tick, failing_ticks = + eval_until_input level message_index ?fuel 0 failing_ticks state + in + continue_with_fuel fuel state @@ fun fuel state -> + let* input, failing_ticks = + match failing_ticks with + | xtick :: failing_ticks' -> + if xtick = tick then + let* () = + Interpreter_event.intended_failure + ~level + ~message_index + ~message_tick:tick + ~internal:false + in + return (mutate input, failing_ticks') + else return (input, failing_ticks) + | _ -> return (input, failing_ticks) + in let* state = PVM.set_input input state in let* state = PVM.eval state in - let* state = eval_until_input state in - return state - - (** [transition_pvm node_ctxt store predecessor_hash hash] runs a PVM at the previous state from block - [predecessor_hash] by consuming as many messages as possible from block [hash]. *) - let transition_pvm node_ctxt store predecessor_hash hash = - let open Node_context in - let open Lwt_result_syntax in - (* Retrieve the previous PVM state from store. *) - let*! predecessor_state = Store.PVMState.find store predecessor_hash in - let* predecessor_state = - match predecessor_state with - | None -> - (* The predecessor is before the origination. - Here we use an RPC to get the boot sector instead of doing this - before and packing it into the [node_ctxt] because the bootsector - might be very large and we don't want to keep that in memory for - ever. - *) - let* boot_sector = - Plugin.RPC.Sc_rollup.boot_sector - node_ctxt.cctxt - (node_ctxt.cctxt#chain, node_ctxt.cctxt#block) - node_ctxt.rollup_address - in - let*! initial_state = PVM.initial_state store boot_sector in - return initial_state - | Some predecessor_state -> return predecessor_state + let* state, fuel, _, _ = + eval_until_input level message_index ?fuel tick failing_ticks state in + return (state, fuel) + let eval_level ?fuel failures store hash state = + let open Lwt_result_syntax in (* Obtain inbox and its messages for this block. *) let*! inbox = Store.Inboxes.get store hash in let inbox_level = Inbox.inbox_level inbox in let*! messages = Store.Messages.get store hash in (* Iterate the PVM state with all the messages for this level. *) - let*! state = - List.fold_left_i_s - (fun message_counter state payload -> + let* state, fuel = + List.fold_left_i_es + (fun message_counter (state, fuel) payload -> let input = Sc_rollup. {inbox_level; message_counter = Z.of_int message_counter; payload} in - feed_input state input) - predecessor_state + let level = Raw_level.to_int32 inbox_level |> Int32.to_int in + let failing_ticks = + Loser_mode.is_failure failures ~level ~message_index:message_counter + in + let*! state, fuel = + feed_input level message_counter ?fuel ~failing_ticks state input + in + return (state, fuel)) + (state, fuel) messages in + return (state, inbox_level, fuel) + + let state_of_hash node_ctxt store hash = + let open Node_context in + let open Lwt_result_syntax in + let*! state = Store.PVMState.find store hash in + match state with + | None -> + (* The predecessor is before the origination. + Here we use an RPC to get the boot sector instead of doing this + before and packing it into the [node_ctxt] because the bootsector + might be very large and we don't want to keep that in memory for + ever. + *) + let* boot_sector = + Plugin.RPC.Sc_rollup.boot_sector + node_ctxt.cctxt + (node_ctxt.cctxt#chain, node_ctxt.cctxt#block) + node_ctxt.rollup_address + in + let*! initial_state = PVM.initial_state store boot_sector in + return initial_state + | Some state -> return state + + (** [transition_pvm node_ctxt store predecessor_hash hash] runs a PVM at the previous state from block + [predecessor_hash] by consuming as many messages as possible from block [hash]. *) + let transition_pvm node_ctxt store predecessor_hash hash = + let open Lwt_result_syntax in + (* Retrieve the previous PVM state from store. *) + let* predecessor_state = state_of_hash node_ctxt store predecessor_hash in + + let* state, inbox_level, _ = + eval_level node_ctxt.loser_mode store hash predecessor_state + in + let*! messages = Store.Messages.get store hash in (* Write final state to store. *) let*! () = Store.PVMState.set store hash state in (* Compute extra information about the state. *) let*! initial_tick = PVM.get_tick predecessor_state in + + let*! () = + let open Store.StateHistoryRepr in + let event = + { + tick = initial_tick; + block_hash = hash; + predecessor_hash; + level = inbox_level; + } + in + Store.StateHistory.insert store event + in + let*! last_tick = PVM.get_tick state in (* TODO: #2717 - The number of ticks should not be an arbitrarily-sized integer. + The number of ticks should not be an arbitrarily-sized integer or + the difference between two ticks should be made an arbitrarily-sized + integer too. *) let num_ticks = Sc_rollup.Tick.distance initial_tick last_tick in (* TODO: #2717 The length of messages here can potentially overflow the [int] returned from [List.length]. *) let num_messages = Z.of_int (List.length messages) in - let*! () = Store.StateInfo.add store hash {num_messages; num_ticks} in - + let*! () = + Store.StateInfo.add store hash {num_messages; num_ticks; initial_tick} + in (* Produce events. *) - let*! () = Interpreter_event.transitioned_pvm state num_messages in + let*! () = + Interpreter_event.transitioned_pvm inbox_level state num_messages + in return_unit @@ -132,4 +244,41 @@ module Make (PVM : Pvm.S) : S = struct let open Lwt_result_syntax in let*! predecessor_hash = Layer1.predecessor store head in transition_pvm node_ctxt store predecessor_hash hash + + (** [run_until_tick tick] *) + let run_until_tick node_ctxt store predecessor_hash hash tick_distance = + let open Lwt_result_syntax in + let* state = state_of_hash node_ctxt store predecessor_hash in + let* state, _, _ = + eval_level node_ctxt.loser_mode ~fuel:tick_distance store hash state + in + return state + + (** [state_of_tick node_ctxt store 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 store tick level = + let open Lwt_result_syntax in + let* closest_event = + Store.StateHistory.event_of_largest_tick_before store tick + in + match closest_event with + | None -> return None + | Some event -> + if Raw_level.(event.level > level) then return None + else + let tick_distance = + Sc_rollup.Tick.distance tick event.tick |> Z.to_int + in + let hash = event.block_hash in + let* state = + run_until_tick + node_ctxt + store + event.predecessor_hash + hash + tick_distance + in + let*! hash = PVM.state_hash state in + return (Some (state, hash)) end diff --git a/src/proto_alpha/bin_sc_rollup_node/interpreter_event.ml b/src/proto_alpha/bin_sc_rollup_node/interpreter_event.ml index d177dd810f3a434b18e9d3d9530a52c7894720b9..2c13a77c89039856579ee28d7510982d8799fdbc 100644 --- a/src/proto_alpha/bin_sc_rollup_node/interpreter_event.ml +++ b/src/proto_alpha/bin_sc_rollup_node/interpreter_event.ml @@ -31,21 +31,39 @@ module Simple = struct let section = ["sc_rollup_node"; "interpreter"] let transitioned_pvm = - declare_3 + declare_4 ~section ~name:"sc_rollup_node_interpreter_transitioned_pvm" ~msg: - "Transitioned PVM to {state_hash} at tick {ticks} with {num_messages} \ - messages" + "Transitioned PVM at inbox level {inbox_level} to {state_hash} at tick \ + {ticks} with {num_messages} messages" ~level:Notice + ("inbox_level", Protocol.Alpha_context.Raw_level.encoding) ("state_hash", State_hash.encoding) ("ticks", Tick.encoding) ("num_messages", Data_encoding.z) + + let intended_failure = + declare_4 + ~section + ~name:"sc_rollup_node_interpreter_intended_failure" + ~msg: + "Intended failure at level {level} for message indexed {message_index} \ + and at the tick {message_tick} of message processing (internal = \ + {internal})." + ~level:Notice + ("level", Data_encoding.int31) + ("message_index", Data_encoding.int31) + ("message_tick", Data_encoding.int31) + ("internal", Data_encoding.bool) end -let transitioned_pvm state num_messages = +let transitioned_pvm inbox_level state num_messages = let open Lwt_syntax in (* TODO (#3094): is this code path taken for Wasm_2_0_0_pvm. Do we need to abstract? *) let* hash = Arith_pvm.state_hash state in let* ticks = Arith_pvm.get_tick state in - Simple.(emit transitioned_pvm (hash, ticks, num_messages)) + Simple.(emit transitioned_pvm (inbox_level, hash, ticks, num_messages)) + +let intended_failure ~level ~message_index ~message_tick ~internal = + Simple.(emit intended_failure (level, message_index, message_tick, internal)) diff --git a/src/proto_alpha/bin_sc_rollup_node/interpreter_event.mli b/src/proto_alpha/bin_sc_rollup_node/interpreter_event.mli index 2c7cd5106921ca8110d7664730d8ecd700f183a2..ca3c3ac70099c56cd9a79f584e44850173ed58c7 100644 --- a/src/proto_alpha/bin_sc_rollup_node/interpreter_event.mli +++ b/src/proto_alpha/bin_sc_rollup_node/interpreter_event.mli @@ -26,6 +26,19 @@ (** This module defines functions that emit the events used when running a PVM transition (see {!Interpreter}). *) -(** [transition_pvm hash n] emits the event that a PVM transition is leading to - the state of the given [hash] by processing [n] messages. *) -val transitioned_pvm : Arith_pvm.state -> Z.t -> unit Lwt.t +(** [transition_pvm inbox_level hash n] emits the event that a PVM + transition is leading to the state of the given [hash] by + processing [n] messages. *) +val transitioned_pvm : + Protocol.Alpha_context.Raw_level.t -> Arith_pvm.state -> Z.t -> unit Lwt.t + +(** [intended_failure level message_index message_tick] emits the event that an + intended failure has been injected at some given [level], during the processing + of a given [message_index] and at tick [message_tick] during this message + processing. *) +val intended_failure : + level:int -> + message_index:int -> + message_tick:int -> + internal:bool -> + unit Lwt.t diff --git a/src/proto_alpha/bin_sc_rollup_node/layer1.ml b/src/proto_alpha/bin_sc_rollup_node/layer1.ml index 75374bc0ffd876117bc155ccc432a0aad120c018..1d4d57b324fe147537e7de9aba4132ce477c51d4 100644 --- a/src/proto_alpha/bin_sc_rollup_node/layer1.ml +++ b/src/proto_alpha/bin_sc_rollup_node/layer1.ml @@ -94,17 +94,68 @@ module State = struct let value_encoding = head_encoding end) - end - let already_seen = Store.Blocks.mem + module Levels = Store.Make_updatable_map (struct + let path = ["tezos"; "levels"] + + let keep_last_n_entries_in_memory = reorganization_window_length + + type key = int32 + + let string_of_key = Int32.to_string + + type value = block_hash + + let value_encoding = Block_hash.encoding + end) + + module ProcessedHashes = Store.Make_append_only_map (struct + let path = ["tezos"; "processed_blocks"] + + let keep_last_n_entries_in_memory = reorganization_window_length + + type key = block_hash + + let string_of_key = Block_hash.to_b58check + + type value = unit + + let value_encoding = Data_encoding.unit + end) + + module LastProcessedHead = Store.Make_mutable_value (struct + let path = ["tezos"; "processed_head"] + + type value = head + + let value_encoding = head_encoding + end) + end let last_seen_head = Store.Head.find let set_new_head = Store.Head.set - let store_block = Store.Blocks.add + let store_block store hash block = + let open Lwt_syntax in + let* already_exists = Store.Blocks.mem store hash in + if not already_exists then Store.Blocks.add store hash block else return () let block_of_hash = Store.Blocks.get + + let mark_processed_head store (Head {hash; _} as head) = + let open Lwt_syntax in + let* () = Store.ProcessedHashes.add store hash () in + let* () = Store.LastProcessedHead.set store head in + return () + + let is_processed = Store.ProcessedHashes.mem + + let last_processed_head = Store.LastProcessedHead.find + + let hash_of_level = Store.Levels.get + + let set_hash_of_level = Store.Levels.add end (** @@ -156,7 +207,9 @@ let store_chain_event store base = let* () = Layer1_event.setting_new_head hash level in let* () = State.set_new_head store head in blocks_of_heads base (intermediate_heads @ [head]) - |> List.iter_s (fun (hash, block) -> State.store_block store hash block) + |> List.iter_s (fun (hash, (Block {level; _} as block)) -> + let* () = State.store_block store hash block in + State.set_hash_of_level store level hash) | Rollback {new_head = Head {hash; level} as base} -> let* () = Layer1_event.rollback hash level in State.set_new_head store base @@ -236,7 +289,7 @@ let catch_up cctxt store chain last_seen_head new_head = (* We have reconnected to the last seen head. *) Lwt.return (ancestor_hash, [same_branch new_head heads]) else - State.already_seen store ancestor_hash >>= function + State.is_processed store ancestor_hash >>= function | true -> (* We have reconnected to a previously known head. [new_head] and [last_seen_head] are not the same branch. *) @@ -274,8 +327,10 @@ let chain_events cctxt store chain = let*! () = List.iter_s (store_chain_event store base) events in Lwt.return events in - let+ heads, _ = Tezos_shell_services.Monitor_services.heads cctxt chain in - Lwt_stream.map_list_s on_head heads + let+ heads, stopper = + Tezos_shell_services.Monitor_services.heads cctxt chain + in + (Lwt_stream.map_list_s on_head heads, stopper) let check_sc_rollup_address_exists sc_rollup_address (cctxt : Protocol_client_context.full) = @@ -324,9 +379,9 @@ let start configuration (cctxt : Protocol_client_context.full) store = let* () = check_sc_rollup_address_exists configuration.sc_rollup_address cctxt in - let* event_stream = chain_events cctxt store `Main in + let* event_stream, stopper = chain_events cctxt store `Main in let* info = gather_info cctxt configuration.sc_rollup_address in - return (discard_pre_origination_blocks info event_stream) + return (discard_pre_origination_blocks info event_stream, stopper) let current_head_hash store = let open Lwt_syntax in @@ -338,6 +393,8 @@ let current_level store = let+ head = State.last_seen_head store in Option.map (fun (Head {level; _}) -> level) head +let hash_of_level = State.hash_of_level + let predecessor store (Head {hash; _}) = let open Lwt_syntax in let+ (Block {predecessor; _}) = State.block_of_hash store hash in @@ -350,3 +407,23 @@ let processed = function | SameBranch {new_head; intermediate_heads} -> List.iter_s processed_head (intermediate_heads @ [new_head]) | Rollback {new_head} -> processed_head new_head + +let mark_processed_head store head = State.mark_processed_head store head + +(* We forget about the last seen heads that are not processed so that + the rollup node can process them when restarted. Notice that this + does prevent skipping heads when the node is interrupted in a bad + way. *) + +(* FIXME: https://gitlab.com/tezos/tezos/-/issues/3205 + + More generally, The rollup node should be able to restart properly + after an abnormal interruption at every point of its process. + Currently, the state is not persistent enough and the processing is + not idempotent enough to achieve that property. *) +let shutdown store = + let open Lwt_syntax in + let* last_processed_head = State.last_processed_head store in + match last_processed_head with + | None -> return_unit + | Some head -> State.set_new_head store head diff --git a/src/proto_alpha/bin_sc_rollup_node/layer1.mli b/src/proto_alpha/bin_sc_rollup_node/layer1.mli index 27100c6a4030d65e41a1a81d29df2ac6a64b5afe..5c52b8cb620277a3df8443e8ce3f8c89e7d43c45 100644 --- a/src/proto_alpha/bin_sc_rollup_node/layer1.mli +++ b/src/proto_alpha/bin_sc_rollup_node/layer1.mli @@ -54,7 +54,7 @@ val start : Configuration.t -> Protocol_client_context.full -> Store.t -> - chain_event Lwt_stream.t tzresult Lwt.t + (chain_event Lwt_stream.t * RPC_context.stopper) tzresult Lwt.t (** [current_head_hash store] is the current hash of the head of the Tezos chain as far as the smart-contract rollup node knows from the @@ -68,6 +68,10 @@ val current_head_hash : Store.t -> Block_hash.t option Lwt.t made. *) val current_level : Store.t -> int32 option Lwt.t +(** [hash_of_level level] returns the current block hash for a given + [level]. *) +val hash_of_level : Store.t -> int32 -> Block_hash.t Lwt.t + (** [predecessor store head] returns the hash of the head's predecessor block] *) val predecessor : Store.t -> head -> Block_hash.t Lwt.t @@ -77,3 +81,11 @@ val genesis_hash : Block_hash.t (** [processed chain_event] emits a log event to officialize the processing of some layer 1 [chain_event]. *) val processed : chain_event -> unit Lwt.t + +(** [mark_process_head store head] remembers that the [head] + is processed. The system should not have to come back to + it. *) +val mark_processed_head : Store.t -> head -> unit Lwt.t + +(** [shutdown store] properly shut the layer 1 down. *) +val shutdown : Store.t -> unit Lwt.t diff --git a/src/proto_alpha/bin_sc_rollup_node/layer1_services.ml b/src/proto_alpha/bin_sc_rollup_node/layer1_services.ml new file mode 100644 index 0000000000000000000000000000000000000000..acd7a2704364246e5521cbd2acb5a6721972ef82 --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/layer1_services.ml @@ -0,0 +1,102 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +open Protocol +open Alpha_context +open Apply_results +include Block_services.Make (Protocol) (Protocol) + +type 'accu operation_processor = { + apply : + 'kind. + 'accu -> + source:public_key_hash -> + 'kind manager_operation -> + 'kind Apply_results.successful_manager_operation_result -> + 'accu; + apply_internal : + 'kind. + 'accu -> + source:public_key_hash -> + 'kind internal_manager_operation -> + 'kind Apply_results.successful_internal_manager_operation_result -> + 'accu; +} + +let process_applied_operations operations accu f = + let rec on_applied_operation_and_result : + type kind. _ -> kind Apply_results.contents_and_result_list -> _ = + fun accu -> function + | Single_and_result + ( Manager_operation {operation; source; _}, + Manager_operation_result + { + operation_result = Applied operation_result; + internal_operation_results; + _; + } ) -> + let accu = f.apply accu ~source operation operation_result in + on_applied_internal_operations accu source internal_operation_results + | Single_and_result (_, _) -> accu + | Cons_and_result + ( Manager_operation {operation; source; _}, + Manager_operation_result + { + operation_result = Applied operation_result; + internal_operation_results; + _; + }, + rest ) -> + let accu = f.apply accu ~source operation operation_result in + let accu = on_applied_operation_and_result accu rest in + on_applied_internal_operations accu source internal_operation_results + | Cons_and_result (_, _, rest) -> on_applied_operation_and_result accu rest + and on_applied_internal_operations accu source internal_operation_results = + List.fold_left + (fun accu (Internal_manager_operation_result ({operation; _}, result)) -> + match result with + | Applied result -> f.apply_internal accu ~source operation result + | _ -> accu) + accu + internal_operation_results + in + let process_contents accu + ({protocol_data = Operation_data {contents; _}; receipt; _} : operation) = + match receipt with + | Empty | Too_large | Receipt No_operation_metadata -> + (* This should case should not happen between [operations] is supposed + to be retrieved with `force_metadata:true`. *) + assert false + | Receipt (Operation_metadata {contents = results; _}) -> ( + match Apply_results.kind_equal_list contents results with + | Some Eq -> + on_applied_operation_and_result accu + @@ Apply_results.pack_contents_list contents results + | None -> + (* Should not happen *) + assert false) + in + let process_operations = List.fold_left process_contents in + List.fold_left process_operations operations accu diff --git a/src/proto_alpha/bin_sc_rollup_node/loser_mode.ml b/src/proto_alpha/bin_sc_rollup_node/loser_mode.ml new file mode 100644 index 0000000000000000000000000000000000000000..3522f726a960f375768f2314274d321d9209ca34 --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/loser_mode.ml @@ -0,0 +1,74 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +type failure = {level : int; message_index : int; message_tick : int} + +let failure_encoding = + let open Data_encoding in + conv + (fun {level; message_index; message_tick} -> + (level, message_index, message_tick)) + (fun (level, message_index, message_tick) -> + {level; message_index; message_tick}) + (obj3 + (req "level" int31) + (req "message_index" int31) + (req "message_tick" int31)) + +let compare_failure f1 f2 = + let open Compare.Int in + match compare f1.level f2.level with + | 0 -> ( + match compare f1.message_index f2.message_index with + | 0 -> compare f1.message_tick f2.message_tick + | n -> n) + | n -> n + +type t = failure list + +let encoding = Data_encoding.list failure_encoding + +let no_failures = [] + +let make s = + let tokens = String.split_on_char ' ' s in + let rec chop = function + | [] | [""] -> [] + | level :: message_index :: message_tick :: rest -> + { + level = int_of_string level; + message_index = int_of_string message_index; + message_tick = int_of_string message_tick; + } + :: chop rest + | _ -> raise Not_found + in + try Some (chop tokens |> List.sort compare_failure) with _ -> None + +let is_failure failures ~level ~message_index = + List.filter + (fun f -> Compare.Int.(f.level = level && f.message_index = message_index)) + failures + |> List.map (fun f -> f.message_tick) diff --git a/src/proto_alpha/bin_sc_rollup_node/loser_mode.mli b/src/proto_alpha/bin_sc_rollup_node/loser_mode.mli new file mode 100644 index 0000000000000000000000000000000000000000..d86a269ee844a2529a13b30e531a068fed52659d --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/loser_mode.mli @@ -0,0 +1,46 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +(** A list of failures. *) +type t + +val encoding : t Data_encoding.t + +(** [no_failures] are planned. *) +val no_failures : t + +(** [make s] parses a list of integers separated by spaces that is a + periodic sequence of triple [level message_index message_tick] + representing a failure that the rollup node is supposed to make. + This function returns [None] if the input string is not syntactically + correct. *) +val make : string -> t option + +(** [is_failure failures ~level ~message_index] returns [message_ticks] + where a failure is supposed to happen at the point + of the rollup node processing of a given inbox [level], a given + [message_index] and for all [message_ticks]. Ticks are sorted by + increasing order. *) +val is_failure : t -> level:int -> message_index:int -> int list diff --git a/src/proto_alpha/bin_sc_rollup_node/main_sc_rollup_node_alpha.ml b/src/proto_alpha/bin_sc_rollup_node/main_sc_rollup_node_alpha.ml index 20057bc9e0e3ceceaeefbb2b85f409ef259fcc7a..3dec94adb753febc6b874cfc31ebc2ced998c8bc 100644 --- a/src/proto_alpha/bin_sc_rollup_node/main_sc_rollup_node_alpha.ml +++ b/src/proto_alpha/bin_sc_rollup_node/main_sc_rollup_node_alpha.ml @@ -177,6 +177,17 @@ let burn_cap_arg = | Some t -> return t | None -> failwith "Bad burn cap")) +let loser_mode = + Clic.default_arg + ~long:"loser-mode" + ~placeholder:"mode" + ~default:"" + ~doc:"Set the rollup node failure points (for test only!)." + (Clic.parameter (fun _ s -> + match Loser_mode.make s with + | Some t -> return t + | None -> failwith "Invalid syntax for failure points")) + let group = { Clic.name = "sc_rollup.node"; @@ -188,7 +199,7 @@ let config_init_command = command ~group ~desc:"Configure the smart-contract rollup node." - (args9 + (args10 data_dir_arg rpc_addr_arg rpc_port_arg @@ -197,7 +208,8 @@ let config_init_command = minimal_nanotez_per_gas_unit_arg force_low_fee_arg fee_cap_arg - burn_cap_arg) + burn_cap_arg + loser_mode) (prefixes ["config"; "init"; "on"] @@ sc_rollup_address_param @@ prefixes ["with"; "operator"] @@ -210,7 +222,8 @@ let config_init_command = minimal_nanotez_per_gas_unit, force_low_fee, fee_cap, - burn_cap ) + burn_cap, + loser_mode ) sc_rollup_address sc_rollup_node_operator cctxt -> @@ -231,6 +244,7 @@ let config_init_command = fee_cap; burn_cap; }; + loser_mode; } in save config >>=? fun () -> diff --git a/src/proto_alpha/bin_sc_rollup_node/node_context.ml b/src/proto_alpha/bin_sc_rollup_node/node_context.ml index d769f4f3082ca99bed989fc761843bb8452a34b9..d11e41788933390b877acab7e64ce98a42f28773 100644 --- a/src/proto_alpha/bin_sc_rollup_node/node_context.ml +++ b/src/proto_alpha/bin_sc_rollup_node/node_context.ml @@ -34,6 +34,7 @@ type t = { block_finality_time : int; kind : Sc_rollup.Kind.t; fee_parameter : Injection.fee_parameter; + loser_mode : Loser_mode.t; } let get_operator_keys node_ctxt = @@ -42,7 +43,7 @@ let get_operator_keys node_ctxt = (node_ctxt.operator, pk, sk) let init (cctxt : Protocol_client_context.full) rollup_address operator - fee_parameter = + fee_parameter ~loser_mode = let open Lwt_result_syntax in let* initial_level = Plugin.RPC.Sc_rollup.initial_level @@ -61,4 +62,5 @@ let init (cctxt : Protocol_client_context.full) rollup_address operator kind; block_finality_time = 2; fee_parameter; + loser_mode; } diff --git a/src/proto_alpha/bin_sc_rollup_node/node_context.mli b/src/proto_alpha/bin_sc_rollup_node/node_context.mli index e51c11150b6c8c9fefe08e3f4f54c4f8c19a6b81..9333d5d82488a409fb696e0f5e48dc4ea7cf4319 100644 --- a/src/proto_alpha/bin_sc_rollup_node/node_context.mli +++ b/src/proto_alpha/bin_sc_rollup_node/node_context.mli @@ -42,6 +42,9 @@ type t = { kind : Sc_rollup.Kind.t; (** Kind of the smart contract rollup. *) fee_parameter : Injection.fee_parameter; (** Fee parameter to use when injecting operations in layer 1. *) + loser_mode : Loser_mode.t; + (** If [true], the rollup node issues wrong commitments (for + tests). *) } (** [get_operator_keys cctxt] returns a triple [(pkh, pk, sk)] corresponding @@ -62,4 +65,5 @@ val init : Sc_rollup.t -> Signature.Public_key_hash.t -> Injection.fee_parameter -> + loser_mode:Loser_mode.t -> t tzresult Lwt.t diff --git a/src/proto_alpha/bin_sc_rollup_node/refutation_game.ml b/src/proto_alpha/bin_sc_rollup_node/refutation_game.ml new file mode 100644 index 0000000000000000000000000000000000000000..68cee4a064de9f8ffaba6066eeea2fd88dfb33f2 --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/refutation_game.ml @@ -0,0 +1,357 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +(** This module implements the refutation game logic of the rollup + node. + + When a new L1 block arises, the rollup node asks the L1 node for + the current game it is part of, if any. + + If a game is running and it is the rollup operator turn, the rollup + node injects the next move of the winning strategy. + + If a game is running and it is not the rollup operator turn, the + rollup node asks the L1 node whether the timeout is reached to play + the timeout argument if possible. + + Otherwise, if no game is running, the rollup node asks the L1 node + whether there is a conflict with one of its disputable commitments. If + there is such a conflict with a commitment C', then the rollup node + starts a game to refute C' by starting a game with one of its staker. + +*) +open Protocol + +open Alpha_context + +module type S = sig + module PVM : Pvm.S + + val process : + Layer1.head -> Node_context.t -> PVM.context -> unit tzresult Lwt.t +end + +module Make (PVM : Pvm.S) : S with module PVM = PVM = struct + module PVM = PVM + module Interpreter = Interpreter.Make (PVM) + open Sc_rollup.Game + + let node_role node_ctxt Sc_rollup.Game.Index.{alice; bob} = + let self = node_ctxt.Node_context.operator in + if Sc_rollup.Staker.equal alice self then Alice + else if Sc_rollup.Staker.equal bob self then Bob + else (* By validity of [ongoing_game] RPC. *) + assert false + + type role = Our_turn | Their_turn + + let turn node_ctxt game players = + let Sc_rollup.Game.Index.{alice; bob} = players in + match (node_role node_ctxt players, game.turn) with + | Alice, Alice -> (Our_turn, bob) + | Bob, Bob -> (Our_turn, alice) + | Alice, Bob -> (Their_turn, bob) + | Bob, Alice -> (Their_turn, bob) + + (** [inject_next_move node_ctxt move] submits an L1 operation to + issue the next move in the refutation game. [node_ctxt] provides + the connection to the Tezos node. *) + let inject_next_move node_ctxt (refutation, opponent) = + let open Node_context in + let open Lwt_result_syntax in + let* source, src_pk, src_sk = Node_context.get_operator_keys node_ctxt in + let {rollup_address; cctxt; _} = node_ctxt in + let* _, _, Manager_operation_result {operation_result; _} = + Client_proto_context.sc_rollup_refute + cctxt + ~chain:cctxt#chain + ~block:cctxt#block + ~refutation + ~opponent + ~source + ~rollup:rollup_address + ~src_pk + ~src_sk + ~fee_parameter:Configuration.default_fee_parameter + () + in + let open Apply_results in + let*! () = + match operation_result with + | Applied (Sc_rollup_refute_result _) -> + Refutation_game_event.refutation_published opponent refutation + | Failed (Sc_rollup_refute_manager_kind, _errors) -> + Refutation_game_event.refutation_failed opponent refutation + | Backtracked (Sc_rollup_refute_result _, _errors) -> + Refutation_game_event.refutation_backtracked opponent refutation + | Skipped Sc_rollup_refute_manager_kind -> + Refutation_game_event.refutation_skipped opponent refutation + in + return_unit + + let as_single_tick_dissection dissection = + match (List.hd dissection, List.last_opt dissection) with + | Some (Some start_hash, start_tick), Some (_stop_state, stop_tick) -> + if Sc_rollup.Tick.distance stop_tick start_tick = Z.one then + Some (start_hash, start_tick) + else None + | _ -> + (* By wellformedness of games returned by the [ongoing_game] RPC. *) + assert false + + let generate_proof node_ctxt store game start_state = + let open Lwt_result_syntax in + let module P = struct + include PVM + + let context = store + + let state = start_state + end in + let*! hash = Layer1.hash_of_level store (Raw_level.to_int32 game.level) in + let*! inbox = Inbox.inbox_of_hash node_ctxt store hash in + let*! r = Sc_rollup.Proof.produce (module P) inbox game.level in + match r with + | Ok r -> return r + | Error err -> + failwith + "The rollup node cannot produce a proof: %a" + Environment.Error_monad.pp + err + + let new_dissection node_ctxt store last_level ok our_view = + let open Lwt_result_syntax in + let _start_hash, start_tick = ok in + let our_state, stop_tick = our_view in + (* TODO: #3200 + We should not rely on an hard-coded constant here but instead + introduce a protocol constant for the maximum number of sections + in a dissection. + *) + let max_number_of_sections = Z.of_int 32 in + let trace_length = Z.succ (Sc_rollup.Tick.distance stop_tick start_tick) in + let number_of_sections = Z.min max_number_of_sections trace_length in + let section_length = + Z.(max (of_int 1) (div trace_length number_of_sections)) + in + (* [k] is the number of sections in [rev_dissection]. *) + let rec make rev_dissection k tick = + if Z.equal k (Z.pred number_of_sections) then + return @@ List.rev ((our_state, stop_tick) :: rev_dissection) + else + let* r = Interpreter.state_of_tick node_ctxt store tick last_level in + let hash = Option.map snd r in + let next_tick = Sc_rollup.Tick.jump tick section_length in + make ((hash, tick) :: rev_dissection) (Z.succ k) next_tick + in + make [] Z.zero start_tick + + (** [generate_from_dissection node_ctxt store game] + traverses the current [game.dissection] and returns a move which + performs a new dissection of the execution trace or provide a + refutation proof to serve as the next move of the [game]. *) + let generate_next_dissection node_ctxt store game = + let open Lwt_result_syntax in + let rec traverse ok = function + | [] -> + (* 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. *) + assert false + | (their_hash, tick) :: dissection -> ( + let open Lwt_result_syntax in + let* our = + Interpreter.state_of_tick node_ctxt store tick game.level + in + match (their_hash, our) with + | None, None -> assert false + | Some _, None | None, Some _ -> + return (ok, (Option.map snd our, tick)) + | Some their_hash, Some (_, our_hash) -> + if Sc_rollup.State_hash.equal our_hash their_hash then + traverse (their_hash, tick) dissection + else return (ok, (Some our_hash, tick))) + in + match game.dissection with + | (Some hash, tick) :: dissection -> + let* ok, ko = traverse (hash, tick) dissection in + let choice = snd ok in + let* dissection = new_dissection node_ctxt store game.level ok ko in + return (choice, dissection) + | [] | (None, _) :: _ -> + (* + By wellformedness of dissection. + A dissection always starts with a tick of the form [(Some hash, tick)]. + A dissection always contains strictly more than one element. + *) + assert false + + let next_move node_ctxt store game = + let open Lwt_result_syntax in + let final_move start_tick = + let* start_state = + Interpreter.state_of_tick node_ctxt store start_tick game.level + in + match start_state with + | None -> assert false + | Some (start_state, _start_hash) -> + let* proof = generate_proof node_ctxt store game start_state in + let choice = start_tick in + return {choice; step = Proof proof} + in + match as_single_tick_dissection game.dissection with + | Some (_start_hash, start_tick) -> final_move start_tick + | None -> ( + let* choice, dissection = + generate_next_dissection node_ctxt store game + in + match as_single_tick_dissection dissection with + | Some (_, start_tick) -> final_move start_tick + | None -> return {choice; step = Dissection dissection}) + + let play_next_move node_ctxt store game opponent = + let open Lwt_result_syntax in + let* refutation = next_move node_ctxt store game in + inject_next_move node_ctxt (Some refutation, opponent) + + let try_timeout node_ctxt players = + let Sc_rollup.Game.Index.{alice; bob} = players in + let open Node_context in + let open Lwt_result_syntax in + let* source, src_pk, src_sk = Node_context.get_operator_keys node_ctxt in + let {rollup_address; cctxt; _} = node_ctxt in + let* _, _, Manager_operation_result {operation_result; _} = + Client_proto_context.sc_rollup_timeout + cctxt + ~chain:cctxt#chain + ~block:cctxt#block + ~source + ~alice + ~bob + ~rollup:rollup_address + ~src_pk + ~src_sk + ~fee_parameter:Configuration.default_fee_parameter + () + in + let open Apply_results in + let*! () = + match operation_result with + | Applied (Sc_rollup_timeout_result _) -> + Refutation_game_event.timeout_published players + | Failed (Sc_rollup_timeout_manager_kind, _errors) -> + Refutation_game_event.timeout_failed players + | Backtracked (Sc_rollup_timeout_result _, _errors) -> + Refutation_game_event.timeout_backtracked players + | Skipped Sc_rollup_timeout_manager_kind -> + Refutation_game_event.timeout_skipped players + in + return_unit + + let play node_ctxt store (game, players) = + let open Lwt_result_syntax in + match turn node_ctxt game players with + | Our_turn, opponent -> play_next_move node_ctxt store game opponent + | Their_turn, _ -> + let* is_timeout_reached = + let Node_context.{rollup_address; cctxt; _} = node_ctxt in + Plugin.RPC.Sc_rollup.is_staker_timeout_reached + cctxt + (cctxt#chain, cctxt#block) + rollup_address + players.alice + players.bob + () + in + if is_timeout_reached then + let*! res = try_timeout node_ctxt players in + match res with + | Ok _ -> return_unit + | Error _ -> + let*! () = Refutation_game_event.timeout_failed players in + return_unit + else return_unit + + let ongoing_game node_ctxt = + let Node_context.{rollup_address; cctxt; operator; _} = node_ctxt in + Plugin.RPC.Sc_rollup.ongoing_refutation_game + cctxt + (cctxt#chain, cctxt#block) + rollup_address + operator + () + + let play_opening_move node_ctxt conflict = + let open Lwt_syntax in + let open Sc_rollup.Refutation_storage in + let* () = Refutation_game_event.conflict_detected conflict in + inject_next_move node_ctxt (None, conflict.other) + + let start_game_if_conflict (Layer1.Head {level; _}) node_ctxt = + let open Lwt_result_syntax in + let Node_context.{rollup_address; cctxt; operator; _} = node_ctxt in + let* conflicts = + Plugin.RPC.Sc_rollup.conflicts + cctxt + (cctxt#chain, cctxt#block) + rollup_address + operator + () + in + let play_new_game conflicts = + match conflicts with + | [] -> return () + | conflict :: _conflicts -> + (* + + When two identical implementations of the rollup node are + running, they will start the game simultaneously and one + of the two injected operations will fail. To prevent + this, we introduce a deterministic strategy where the + rollup node starts the game depending on a comparison + between the adresses of the two operators in conflict and + the parity of the level. + + *) + let open Sc_rollup.Refutation_storage in + let cmp = + if Signature.Public_key_hash.compare conflict.other operator < 0 + then 0 + else 1 + in + if Int32.to_int level mod 2 = cmp then + play_opening_move node_ctxt conflict + else return_unit + in + play_new_game conflicts + + let process head node_ctxt store = + let open Lwt_result_syntax in + let* game = ongoing_game node_ctxt in + match game with + | Some game -> play node_ctxt store game + | None -> start_game_if_conflict head node_ctxt +end diff --git a/src/proto_alpha/bin_sc_rollup_node/refutation_game.mli b/src/proto_alpha/bin_sc_rollup_node/refutation_game.mli new file mode 100644 index 0000000000000000000000000000000000000000..62dcfe35f09eaee34ae244cd867bf00f978b76af --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/refutation_game.mli @@ -0,0 +1,33 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +module type S = sig + module PVM : Pvm.S + + val process : + Layer1.head -> Node_context.t -> PVM.context -> unit tzresult Lwt.t +end + +module Make (PVM : Pvm.S) : S with module PVM = PVM diff --git a/src/proto_alpha/bin_sc_rollup_node/refutation_game_event.ml b/src/proto_alpha/bin_sc_rollup_node/refutation_game_event.ml new file mode 100644 index 0000000000000000000000000000000000000000..ff8f7e798762a3e6c594c6565cfac9e9cf6a3469 --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/refutation_game_event.ml @@ -0,0 +1,188 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* 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. *) +(* *) +(*****************************************************************************) + +open Protocol.Alpha_context + +(* TODO: https://gitlab.com/tezos/tezos/-/issues/2880 + Add corresponding .mli file. *) + +module Simple = struct + include Internal_event.Simple + + let section = ["sc_rollup_node"; "refutation_game"] + + let timeout = + declare_1 + ~section + ~name:"sc_rollup_node_timeout" + ~msg: + "The rollup node has been slashed because of a timeout issued by \ + {address}" + ~level:Notice + ("address", Signature.Public_key_hash.encoding) + + let invalid_move = + declare_0 + ~section + ~name:"sc_rollup_node_invalid_move" + ~msg: + "The rollup node is about to make an invalid move in the refutation \ + game! It is stopped to avoid being slashed. The problem should be \ + reported immediately or the rollup node should be upgraded to have a \ + chance to be back before the timeout is reached." + ~level:Notice + () + + let conflict_detected = + declare_5 + ~name:"sc_rollup_node_conflict_detected" + ~msg: + "A conflict has been found with our commitment {our_commitment_hash} \ + at level {level} with staker {other} that hash issued commitment \ + {their_commitment_hash} both based on {parent_commitment_hash}." + ~level:Notice + ("our_commitment_hash", Sc_rollup.Commitment.Hash.encoding) + ("level", Raw_level.encoding) + ("other", Sc_rollup.Staker.encoding) + ("their_commitment_hash", Sc_rollup.Commitment.Hash.encoding) + ("parent_commitment_hash", Sc_rollup.Commitment.Hash.encoding) + + let refutation_published = + declare_2 + ~section + ~name:"sc_rollup_node_refutation_published" + ~msg: + "Refutation was published - opponent: {opponent}, refutation: \ + {refutation}" + ~level:Notice + ("opponent", Sc_rollup.Staker.encoding) + ("refutation", Data_encoding.option Sc_rollup.Game.refutation_encoding) + + let refutation_failed = + declare_2 + ~section + ~name:"sc_rollup_node_refutation_failed" + ~msg: + "Publishing refutation has failed - opponent: {opponent}, refutation: \ + {refutation}" + ~level:Notice + ("opponent", Sc_rollup.Staker.encoding) + ("refutation", Data_encoding.option Sc_rollup.Game.refutation_encoding) + + let refutation_backtracked = + declare_2 + ~section + ~name:"sc_rollup_node_refutation_backtracked" + ~msg: + "Publishing refutation was backtracked - opponent: {opponent}, \ + refutation: {refutation}" + ~level:Notice + ("opponent", Sc_rollup.Staker.encoding) + ("refutation", Data_encoding.option Sc_rollup.Game.refutation_encoding) + + let refutation_skipped = + declare_2 + ~section + ~name:"sc_rollup_node_refutation_skipped" + ~msg: + "Publishing refutation was skipped - opponent: {opponent}, refutation: \ + {refutation}" + ~level:Notice + ("opponent", Sc_rollup.Staker.encoding) + ("refutation", Data_encoding.option Sc_rollup.Game.refutation_encoding) + + let timeout_published = + declare_1 + ~section + ~name:"sc_rollup_node_timeout_published" + ~msg:"Timeout was published - players: {players}" + ~level:Notice + ("players", Sc_rollup.Game.Index.encoding) + + let timeout_failed = + declare_1 + ~section + ~name:"sc_rollup_node_timeout_failed" + ~msg:"Publishing timeout has failed - players: {players}" + ~level:Notice + ("players", Sc_rollup.Game.Index.encoding) + + let timeout_backtracked = + declare_1 + ~section + ~name:"sc_rollup_node_timeout_backtracked" + ~msg:"Publishing timeout was backtracked - players: {players}" + ~level:Notice + ("players", Sc_rollup.Game.Index.encoding) + + let timeout_skipped = + declare_1 + ~section + ~name:"sc_rollup_node_timeout_skipped" + ~msg:"Publishing timeout was skipped - players: {players}" + ~level:Notice + ("players", Sc_rollup.Game.Index.encoding) +end + +let timeout address = Simple.(emit timeout address) + +let invalid_move () = Simple.(emit invalid_move ()) + +let refutation_published opponent refutation = + Simple.(emit refutation_published (opponent, refutation)) + +let refutation_failed opponent refutation = + Simple.(emit refutation_failed (opponent, refutation)) + +let refutation_backtracked opponent refutation = + Simple.(emit refutation_backtracked (opponent, refutation)) + +let refutation_skipped opponent refutation = + Simple.(emit refutation_skipped (opponent, refutation)) + +let timeout_published players = Simple.(emit timeout_published players) + +let timeout_failed players = Simple.(emit timeout_failed players) + +let timeout_backtracked players = Simple.(emit timeout_backtracked players) + +let timeout_skipped players = Simple.(emit timeout_skipped players) + +let conflict_detected (conflict : Sc_rollup.Refutation_storage.conflict) = + let our_commitment_hash = Sc_rollup.Commitment.hash conflict.our_commitment in + let their_commitment_hash = + Sc_rollup.Commitment.hash conflict.their_commitment + in + let parent_commitment_hash = conflict.parent_commitment in + let other = conflict.other in + let level = conflict.our_commitment.inbox_level in + Simple.( + emit + conflict_detected + ( our_commitment_hash, + level, + other, + their_commitment_hash, + parent_commitment_hash )) diff --git a/src/proto_alpha/bin_sc_rollup_node/store.ml b/src/proto_alpha/bin_sc_rollup_node/store.ml index e49a8b3d31e57615da1d07e679813e6e1662bb0d..d01def0dc9dee2122e97983a330ad0816fb5817e 100644 --- a/src/proto_alpha/bin_sc_rollup_node/store.ml +++ b/src/proto_alpha/bin_sc_rollup_node/store.ml @@ -43,12 +43,23 @@ let load configuration = let* repo = IStore.Repo.v (Irmin_pack.config configuration.data_dir) in IStore.main repo +let flush store = IStore.flush (IStore.repo store) + let close store = IStore.Repo.close (IStore.repo store) let info message = let date = Unix.gettimeofday () |> int_of_float |> Int64.of_int in Irmin.Info.Default.v ~author:"Tezos smart-contract rollup node" ~message date +let commit ~time ?(message = "") context = + let open Lwt_syntax in + let info = + IStore.Info.v ~author:"Tezos" (Time.Protocol.to_seconds time) ~message + in + let* tree = IStore.tree context in + let* commit = IStore.Commit.v (IStore.repo context) ~info ~parents:[] tree in + return @@ IStore.Commit.key commit + module type Mutable_value = sig type value @@ -63,7 +74,7 @@ module type Mutable_value = sig val find : t -> value option Lwt.t end -module Make_append_only_map (P : sig +module type KeyValue = sig val path : path val keep_last_n_entries_in_memory : int @@ -75,8 +86,9 @@ module Make_append_only_map (P : sig type value val value_encoding : value Data_encoding.t -end) = -struct +end + +module Make_map (P : KeyValue) = struct (* Ignored for now. *) let _ = P.keep_last_n_entries_in_memory @@ -104,20 +116,45 @@ struct let open Lwt_syntax in let* exists = mem store key in if exists then get store key else return (on_default ()) +end + +module Make_updatable_map (P : KeyValue) = struct + include Make_map (P) let add store key value = - let open Lwt_syntax in - let* already_exists = mem store key in let full_path = String.concat "/" (P.path @ [P.string_of_key key]) in - if already_exists then - Stdlib.failwith (Printf.sprintf "Key %s already exists" full_path) ; - let encoded_value = - Data_encoding.Binary.to_bytes_exn P.value_encoding value - in + let encode v = Data_encoding.Binary.to_bytes_exn P.value_encoding v in + let encoded_value = encode value in let info () = info full_path in IStore.set_exn ~info store (make_key key) encoded_value end +module Make_append_only_map (P : KeyValue) = struct + include Make_map (P) + + let add store key value = + let open Lwt_syntax in + let* existing_value = find store key in + let full_path = String.concat "/" (P.path @ [P.string_of_key key]) in + let encode v = Data_encoding.Binary.to_bytes_exn P.value_encoding v in + let encoded_value = encode value in + match existing_value with + | None -> + let info () = info full_path in + IStore.set_exn ~info store (make_key key) encoded_value + | Some existing_value -> + (* To be robust to interruption in the middle of processes, + we accept to redo some work when we restart the node. + Hence, it is fine to insert twice the same value for a + given value. *) + if not (Bytes.equal (encode existing_value) encoded_value) then + Stdlib.failwith + (Printf.sprintf + "Key %s already exists with a different value" + full_path) + else return_unit +end + module Make_mutable_value (P : sig val path : path @@ -219,7 +256,11 @@ module MessageTrees = struct message_tree end -type state_info = {num_messages : Z.t; num_ticks : Z.t} +type state_info = { + num_messages : Z.t; + num_ticks : Z.t; + initial_tick : Sc_rollup.Tick.t; +} (** Extraneous state information for the PVM *) module StateInfo = Make_append_only_map (struct @@ -236,13 +277,79 @@ module StateInfo = Make_append_only_map (struct let value_encoding = let open Data_encoding in conv - (fun {num_messages; num_ticks} -> (num_messages, num_ticks)) - (fun (num_messages, num_ticks) -> {num_messages; num_ticks}) - (obj2 + (fun {num_messages; num_ticks; initial_tick} -> + (num_messages, num_ticks, initial_tick)) + (fun (num_messages, num_ticks, initial_tick) -> + {num_messages; num_ticks; initial_tick}) + (obj3 (req "num_messages" Data_encoding.z) - (req "num_ticks" Data_encoding.z)) + (req "num_ticks" Data_encoding.z) + (req "initial_tick" Sc_rollup.Tick.encoding)) end) +module StateHistoryRepr = struct + let path = ["state_history"] + + type event = { + tick : Sc_rollup.Tick.t; + block_hash : Block_hash.t; + predecessor_hash : Block_hash.t; + level : Raw_level.t; + } + + module TickMap = Map.Make (Sc_rollup.Tick) + + type value = event TickMap.t + + let event_encoding = + let open Data_encoding in + conv + (fun {tick; block_hash; predecessor_hash; level} -> + (tick, block_hash, predecessor_hash, level)) + (fun (tick, block_hash, predecessor_hash, level) -> + {tick; block_hash; predecessor_hash; level}) + (obj4 + (req "tick" Sc_rollup.Tick.encoding) + (req "block_hash" Block_hash.encoding) + (req "predecessor_hash" Block_hash.encoding) + (req "level" Raw_level.encoding)) + + let value_encoding = + let open Data_encoding in + conv + TickMap.bindings + (fun bindings -> TickMap.of_seq (List.to_seq bindings)) + (Data_encoding.list (tup2 Sc_rollup.Tick.encoding event_encoding)) +end + +module StateHistory = struct + include Make_mutable_value (StateHistoryRepr) + + let insert store event = + let open Lwt_result_syntax in + let open StateHistoryRepr in + let*! history = find store in + let history = + match history with + | None -> StateHistoryRepr.TickMap.empty + | Some history -> history + in + set store (TickMap.add event.tick event history) + + let event_of_largest_tick_before store tick = + let open Lwt_result_syntax in + let open StateHistoryRepr in + let*! history = find store in + match history with + | None -> return_none + | Some history -> ( + let events_before, opt_value, _ = TickMap.split tick history in + match opt_value with + | Some event -> return (Some event) + | None -> + return @@ Option.map snd @@ TickMap.max_binding_opt events_before) +end + (** Unaggregated messages per block *) module Messages = Make_append_only_map (struct let path = ["messages"] @@ -351,3 +458,27 @@ module Commitments_published_at_level = Make_append_only_map (struct let value_encoding = Raw_level.encoding end) + +module OngoingGameRepr = struct + let path = ["game"] + + type value = { + state : Sc_rollup.Game.t; + starting_level : Raw_level.t; + last_move_level : Raw_level.t; + } + + let value_encoding = + let open Data_encoding in + conv + (fun {state; starting_level; last_move_level} -> + (state, starting_level, last_move_level)) + (fun (state, starting_level, last_move_level) -> + {state; starting_level; last_move_level}) + @@ obj3 + (req "state" Sc_rollup.Game.encoding) + (req "starting_level" Raw_level.encoding) + (req "last_move_level" Raw_level.encoding) +end + +module OngoingGame = Make_mutable_value (OngoingGameRepr) diff --git a/src/proto_alpha/lib_client/client_proto_context.ml b/src/proto_alpha/lib_client/client_proto_context.ml index fd8a161c13cdfe90827cba4d8d44b20c2984dc37..e196148e16258f6f746add5c78d97ad4f8cdf0ff 100644 --- a/src/proto_alpha/lib_client/client_proto_context.ml +++ b/src/proto_alpha/lib_client/client_proto_context.ml @@ -1346,3 +1346,70 @@ let sc_rollup_recover_bond (cctxt : #full) ~chain ~block ?confirmations ?dry_run match Apply_results.pack_contents_list op result with | Apply_results.Single_and_result ((Manager_operation _ as op), result) -> return (oph, op, result) + +let sc_rollup_refute (cctxt : #full) ~chain ~block ?confirmations ?dry_run + ?verbose_signing ?simulation ?fee ?gas_limit ?storage_limit ?counter ~source + ~rollup ~refutation ~opponent ~src_pk ~src_sk ~fee_parameter () = + let op = + Annotated_manager_operation.Single_manager + (Injection.prepare_manager_operation + ~fee:(Limit.of_option fee) + ~gas_limit:(Limit.of_option gas_limit) + ~storage_limit:(Limit.of_option storage_limit) + (Sc_rollup_refute {rollup; refutation; opponent})) + in + Injection.inject_manager_operation + cctxt + ~chain + ~block + ?confirmations + ?dry_run + ?verbose_signing + ?simulation + ?counter + ~source + ~fee:(Limit.of_option fee) + ~storage_limit:(Limit.of_option storage_limit) + ~gas_limit:(Limit.of_option gas_limit) + ~src_pk + ~src_sk + ~fee_parameter + op + >>=? fun (oph, _, op, result) -> + match Apply_results.pack_contents_list op result with + | Apply_results.Single_and_result ((Manager_operation _ as op), result) -> + return (oph, op, result) + +let sc_rollup_timeout (cctxt : #full) ~chain ~block ?confirmations ?dry_run + ?verbose_signing ?simulation ?fee ?gas_limit ?storage_limit ?counter ~source + ~rollup ~alice ~bob ~src_pk ~src_sk ~fee_parameter () = + let stakers = Sc_rollup.Game.Index.make alice bob in + let op = + Annotated_manager_operation.Single_manager + (Injection.prepare_manager_operation + ~fee:(Limit.of_option fee) + ~gas_limit:(Limit.of_option gas_limit) + ~storage_limit:(Limit.of_option storage_limit) + (Sc_rollup_timeout {rollup; stakers})) + in + Injection.inject_manager_operation + cctxt + ~chain + ~block + ?confirmations + ?dry_run + ?verbose_signing + ?simulation + ?counter + ~source + ~fee:(Limit.of_option fee) + ~storage_limit:(Limit.of_option storage_limit) + ~gas_limit:(Limit.of_option gas_limit) + ~src_pk + ~src_sk + ~fee_parameter + op + >>=? fun (oph, _, op, result) -> + match Apply_results.pack_contents_list op result with + | Apply_results.Single_and_result ((Manager_operation _ as op), result) -> + return (oph, op, result) diff --git a/src/proto_alpha/lib_client/client_proto_context.mli b/src/proto_alpha/lib_client/client_proto_context.mli index 2404bb012e2b76ab75bda43b4b76550274a55295..03d4b766ec1aa40012d6bcc0b925b9370e5cc767 100644 --- a/src/proto_alpha/lib_client/client_proto_context.mli +++ b/src/proto_alpha/lib_client/client_proto_context.mli @@ -820,3 +820,55 @@ val sc_rollup_recover_bond : * Kind.sc_rollup_recover_bond Kind.manager Apply_results.contents_result) tzresult Lwt.t + +val sc_rollup_refute : + #Protocol_client_context.full -> + chain:Chain_services.chain -> + block:Block_services.block -> + ?confirmations:int -> + ?dry_run:bool -> + ?verbose_signing:bool -> + ?simulation:bool -> + ?fee:Tez.t -> + ?gas_limit:Gas.Arith.integral -> + ?storage_limit:counter -> + ?counter:counter -> + source:public_key_hash -> + rollup:Alpha_context.Sc_rollup.t -> + refutation:Alpha_context.Sc_rollup.Game.refutation option -> + opponent:Alpha_context.Sc_rollup.Staker.t -> + src_pk:public_key -> + src_sk:Client_keys.sk_uri -> + fee_parameter:Injection.fee_parameter -> + unit -> + (Operation_hash.t + * Kind.sc_rollup_refute Kind.manager contents + * Kind.sc_rollup_refute Kind.manager Apply_results.contents_result) + tzresult + Lwt.t + +val sc_rollup_timeout : + #Protocol_client_context.full -> + chain:Chain_services.chain -> + block:Block_services.block -> + ?confirmations:int -> + ?dry_run:bool -> + ?verbose_signing:bool -> + ?simulation:bool -> + ?fee:Tez.t -> + ?gas_limit:Gas.Arith.integral -> + ?storage_limit:counter -> + ?counter:counter -> + source:public_key_hash -> + rollup:Alpha_context.Sc_rollup.t -> + alice:Alpha_context.Sc_rollup.Staker.t -> + bob:Alpha_context.Sc_rollup.Staker.t -> + src_pk:public_key -> + src_sk:Client_keys.sk_uri -> + fee_parameter:Injection.fee_parameter -> + unit -> + (Operation_hash.t + * Kind.sc_rollup_timeout Kind.manager contents + * Kind.sc_rollup_timeout Kind.manager Apply_results.contents_result) + tzresult + Lwt.t diff --git a/src/proto_alpha/lib_client/operation_result.ml b/src/proto_alpha/lib_client/operation_result.ml index 90d681a1d383bf7fd93568afadfb07c440516dcf..23cc8591e6b0c63a1e5338853a2295cd794fe9bc 100644 --- a/src/proto_alpha/lib_client/operation_result.ml +++ b/src/proto_alpha/lib_client/operation_result.ml @@ -282,18 +282,18 @@ let pp_manager_operation_content (type kind) source ppf commitment Sc_rollup.Address.pp rollup - | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} -> + | Sc_rollup_refute {rollup; opponent; refutation} -> Format.fprintf ppf - "Refute staker %a in the smart contract rollup at address %a using \ - refutation %a %s" + "Refute staker %a in the smart contract rollup at address %a using %a" Sc_rollup.Staker.pp opponent Sc_rollup.Address.pp rollup - Sc_rollup.Game.pp_refutation + (fun fmt -> function + | None -> Format.pp_print_string fmt "opening move of the game" + | Some refutation -> Sc_rollup.Game.pp_refutation fmt refutation) refutation - (if is_opening_move then "(opening move of game)" else "") | Sc_rollup_timeout {rollup; stakers = {alice; bob}} -> Format.fprintf ppf @@ -712,7 +712,7 @@ let pp_manager_operation_contents_result ppf op_result = | Sc_rollup_cement_result _ -> "smart contract rollup commitment cementing" | Sc_rollup_publish_result _ -> "smart contract rollup commitment publishing" - | Sc_rollup_refute_result _ -> "smart contract rollup refutation start" + | Sc_rollup_refute_result _ -> "smart contract rollup refutation move" | Sc_rollup_timeout_result _ -> "smart contract rollup refutation timeout" | Sc_rollup_execute_outbox_message_result _ -> "smart contract output message execution" diff --git a/src/proto_alpha/lib_parameters/default_parameters.ml b/src/proto_alpha/lib_parameters/default_parameters.ml index 36ce5101dabd3ef1655cf1e7811862c4c16bd3ff..cc04b1d1da1ff3d064ae9b390b5b0c5f04b28e03 100644 --- a/src/proto_alpha/lib_parameters/default_parameters.ml +++ b/src/proto_alpha/lib_parameters/default_parameters.ml @@ -193,7 +193,7 @@ let constants_mainnet = max_available_messages = 1_000_000; (* TODO: https://gitlab.com/tezos/tezos/-/issues/2756 The following constants need to be refined. *) - stake_amount = Tez.of_mutez_exn 32_000_000L; + stake_amount = Tez.of_mutez_exn 10_000_000_000L; commitment_period_in_blocks = 30; max_lookahead_in_blocks = 30_000l; max_active_outbox_levels = sc_rollup_max_active_outbox_levels; diff --git a/src/proto_alpha/lib_plugin/RPC.ml b/src/proto_alpha/lib_plugin/RPC.ml index 03c8f89a1e060228cb8c52c898d4498805b033cf..6d4dfd320cc2f4448ac4e3c191fb17186da85d3f 100644 --- a/src/proto_alpha/lib_plugin/RPC.ml +++ b/src/proto_alpha/lib_plugin/RPC.ml @@ -1831,6 +1831,64 @@ module Sc_rollup = struct ~query:RPC_query.empty ~output:(Data_encoding.list Sc_rollup.Address.encoding) path + + let ongoing_refutation_game = + let query = + let open RPC_query in + query Sc_rollup.Staker.of_b58check_exn + |+ field "staker" RPC_arg.string "" (fun x -> + Format.asprintf "%a" Sc_rollup.Staker.pp x) + |> seal + in + let output = + Sc_rollup.( + Data_encoding.(option (tup2 Game.encoding Game.Index.encoding))) + in + RPC_service.get_service + ~description:"Ongoing refufation game for a given staker" + ~query + ~output + RPC_path.(path /: Sc_rollup.Address.rpc_arg / "game") + + let is_staker_timeout_reached = + let query = + let open RPC_query in + let open Sc_rollup.Game.Index in + query (fun alice bob -> + let alice = Sc_rollup.Staker.of_b58check_exn alice + and bob = Sc_rollup.Staker.of_b58check_exn bob in + make alice bob) + |+ field "alice" RPC_arg.string "" (fun x -> + Format.asprintf "%a" Sc_rollup.Staker.pp x.alice) + |+ field "bob" RPC_arg.string "" (fun x -> + Format.asprintf "%a" Sc_rollup.Staker.pp x.bob) + |> seal + in + let output = Data_encoding.bool in + RPC_service.get_service + ~description: + "Determine whether the timeout of the current player in the game \ + played by the given stakers is reached" + ~query + ~output + RPC_path.(path /: Sc_rollup.Address.rpc_arg / "timeout") + + let conflicts = + let query = + let open RPC_query in + query Sc_rollup.Staker.of_b58check_exn + |+ field "staker" RPC_arg.string "" (fun x -> + Format.asprintf "%a" Sc_rollup.Staker.pp x) + |> seal + in + let output = + Sc_rollup.(Data_encoding.list Refutation_storage.conflict_encoding) + in + RPC_service.get_service + ~description:"List of stakers in conflict with the given staker" + ~query + ~output + RPC_path.(path /: Sc_rollup.Address.rpc_arg / "conflicts") end let kind ctxt block sc_rollup_address = @@ -1890,6 +1948,41 @@ module Sc_rollup = struct Registration.register0 ~chunked:true S.root (fun context () () -> Sc_rollup.list context) + let register_ongoing_refutation_game () = + Registration.register1 + ~chunked:false + S.ongoing_refutation_game + (fun context rollup staker () -> + let open Lwt_tzresult_syntax in + let* game, _ = + Sc_rollup.Refutation_storage.get_ongoing_game_for_staker + context + rollup + staker + in + return game) + + let register_staker_timeout_reached () = + Registration.register1 + ~chunked:false + S.is_staker_timeout_reached + (fun context rollup game_index () -> + let open Lwt_tzresult_syntax in + let*! res = + Sc_rollup.Refutation_storage.timeout context rollup game_index + in + match res with Ok _ -> return true | _ -> return false) + + let register_conflicts () = + Registration.register1 + ~chunked:false + S.conflicts + (fun context rollup staker () -> + Sc_rollup.Refutation_storage.conflicting_stakers_uncarbonated + context + rollup + staker) + let register () = register_kind () ; register_inbox () ; @@ -1897,7 +1990,10 @@ module Sc_rollup = struct register_boot_sector () ; register_last_cemented_commitment_hash_with_level () ; register_commitment () ; - register_root () + register_root () ; + register_ongoing_refutation_game () ; + register_staker_timeout_reached () ; + register_conflicts () let list ctxt block = RPC_context.make_call0 S.root ctxt block () () @@ -1915,6 +2011,26 @@ module Sc_rollup = struct let boot_sector ctxt block sc_rollup_address = RPC_context.make_call1 S.boot_sector ctxt block sc_rollup_address () () + + let ongoing_refutation_game ctxt block sc_rollup_address staker = + RPC_context.make_call1 + S.ongoing_refutation_game + ctxt + block + sc_rollup_address + staker + + let is_staker_timeout_reached ctxt block sc_rollup_address alice bob = + let game_index = Sc_rollup.Game.Index.make alice bob in + RPC_context.make_call1 + S.is_staker_timeout_reached + ctxt + block + sc_rollup_address + game_index + + let conflicts ctxt block sc_rollup_address staker = + RPC_context.make_call1 S.conflicts ctxt block sc_rollup_address staker end module Tx_rollup = struct diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index e246db86a4fd878c70ea338282fe0b20878d3c5d..caaf69e83d77ef11d74704841cd3d59e9b2c9a39 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -2485,6 +2485,8 @@ module Sc_rollup : sig val next : t -> t + val jump : t -> Z.t -> t + val distance : t -> t -> Z.t val of_int : int -> t option @@ -2617,6 +2619,10 @@ module Sc_rollup : sig val produce_output_proof : context -> state -> output -> (output_proof, error) result Lwt.t + + module Internal_for_tests : sig + val insert_failure : state -> state Lwt.t + end end type t = (module S) @@ -2649,6 +2655,8 @@ module Sc_rollup : sig type tree = Tree.tree + val hash_tree : tree -> State_hash.t + type proof val proof_encoding : proof Data_encoding.t @@ -2922,7 +2930,7 @@ module Sc_rollup : sig type t = {pvm_step : wrapped_proof; inbox : Inbox.Proof.t option} module type PVM_with_context_and_state = sig - include Sc_rollups.PVM.S + include PVM.S val context : context @@ -2931,14 +2939,16 @@ module Sc_rollup : sig val produce : (module PVM_with_context_and_state) -> - Sc_rollup_inbox_repr.t -> - Raw_level_repr.t -> + Inbox.t -> + Raw_level.t -> (t, error) result Lwt.t end module Game : sig type player = Alice | Bob + val player_equal : player -> player -> bool + type t = { turn : player; inbox_snapshot : Inbox.t; @@ -2947,17 +2957,23 @@ module Sc_rollup : sig dissection : (State_hash.t option * Tick.t) list; } + val pp : Format.formatter -> t -> unit + module Index : sig type t = private {alice : Staker.t; bob : Staker.t} val make : Staker.t -> Staker.t -> t + + val encoding : t Data_encoding.t end + val encoding : t Data_encoding.t + val opponent : player -> player - type step = - | Dissection of (State_hash.t option * Tick.t) list - | Proof of Proof.t + type step = Dissection of dissection | Proof of Proof.t + + and dissection = (State_hash.t option * Tick.t) list type refutation = {choice : Tick.t; step : step} @@ -2965,6 +2981,8 @@ module Sc_rollup : sig type reason = Conflict_resolved | Invalid_move of string | Timeout + val refutation_encoding : refutation Data_encoding.t + val pp_reason : Format.formatter -> reason -> unit val reason_encoding : reason Data_encoding.t @@ -3026,13 +3044,37 @@ module Sc_rollup : sig type conflict_point = point * point + type conflict = { + other : Staker.t; + their_commitment : Commitment.t; + our_commitment : Commitment.t; + parent_commitment : Commitment.Hash.t; + } + + val conflict_encoding : conflict Data_encoding.t + + val get_ongoing_game_for_staker : + context -> + t -> + Staker.t -> + ((Game.t * Game.Index.t) option * context) tzresult Lwt.t + + val conflicting_stakers_uncarbonated : + context -> t -> Staker.t -> conflict list tzresult Lwt.t + + val start_game : + context -> + t -> + player:Signature.public_key_hash -> + opponent:Signature.public_key_hash -> + context tzresult Lwt.t + val game_move : context -> t -> player:Staker.t -> opponent:Staker.t -> Game.refutation -> - is_opening_move:bool -> (Game.outcome option * context) tzresult Lwt.t val timeout : @@ -3549,8 +3591,7 @@ and _ manager_operation = | Sc_rollup_refute : { rollup : Sc_rollup.t; opponent : Sc_rollup.Staker.t; - refutation : Sc_rollup.Game.refutation; - is_opening_move : bool; + refutation : Sc_rollup.Game.refutation option; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { diff --git a/src/proto_alpha/lib_protocol/apply.ml b/src/proto_alpha/lib_protocol/apply.ml index bd65117ab13d5d829d225beaf367428ad353e71c..5da2f2daad722b94a580d571828c315e14b42852 100644 --- a/src/proto_alpha/lib_protocol/apply.ml +++ b/src/proto_alpha/lib_protocol/apply.ml @@ -1812,14 +1812,14 @@ let apply_external_manager_operation_content : {staked_hash; consumed_gas; published_at_level; balance_updates} in return (ctxt, result, []) - | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} -> - Sc_rollup.Refutation_storage.game_move - ctxt - rollup - ~player:source - ~opponent - refutation - ~is_opening_move + | Sc_rollup_refute {rollup; opponent; refutation} -> + let open Sc_rollup.Refutation_storage in + let player = source in + (match refutation with + | None -> + start_game ctxt rollup ~player ~opponent >>=? fun ctxt -> + return (None, ctxt) + | Some refutation -> game_move ctxt rollup ~player ~opponent refutation) >>=? fun (outcome, ctxt) -> (match outcome with | None -> return (Sc_rollup.Game.Ongoing, ctxt, []) diff --git a/src/proto_alpha/lib_protocol/operation_repr.ml b/src/proto_alpha/lib_protocol/operation_repr.ml index 4e737928a4def8d3d44538d550c37672c5d86171..6c571862b7f2fbc53b2399ed00f2899ac482bbe8 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.ml +++ b/src/proto_alpha/lib_protocol/operation_repr.ml @@ -410,8 +410,7 @@ and _ manager_operation = | Sc_rollup_refute : { rollup : Sc_rollup_repr.t; opponent : Sc_rollup_repr.Staker.t; - refutation : Sc_rollup_game_repr.refutation; - is_opening_move : bool; + refutation : Sc_rollup_game_repr.refutation option; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { @@ -1055,22 +1054,22 @@ module Encoding = struct tag = sc_rollup_operation_refute_tag; name = "sc_rollup_refute"; encoding = - obj4 + obj3 (req "rollup" Sc_rollup_repr.encoding) (req "opponent" Sc_rollup_repr.Staker.encoding) - (req "refutation" Sc_rollup_game_repr.refutation_encoding) - (req "is_opening_move" bool); + (req + "refutation" + (option Sc_rollup_game_repr.refutation_encoding)); select = (function | Manager (Sc_rollup_refute _ as op) -> Some op | _ -> None); proj = (function - | Sc_rollup_refute {rollup; opponent; refutation; is_opening_move} - -> - (rollup, opponent, refutation, is_opening_move)); + | Sc_rollup_refute {rollup; opponent; refutation} -> + (rollup, opponent, refutation)); inj = - (fun (rollup, opponent, refutation, is_opening_move) -> - Sc_rollup_refute {rollup; opponent; refutation; is_opening_move}); + (fun (rollup, opponent, refutation) -> + Sc_rollup_refute {rollup; opponent; refutation}); } let[@coq_axiom_with_reason "gadt"] sc_rollup_timeout_case = diff --git a/src/proto_alpha/lib_protocol/operation_repr.mli b/src/proto_alpha/lib_protocol/operation_repr.mli index 0277f6fad073064a31bda4a7c777f8102c317088..f9501516a2e62980f60287e7fa76b429ac42d661 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.mli +++ b/src/proto_alpha/lib_protocol/operation_repr.mli @@ -476,8 +476,7 @@ and _ manager_operation = | Sc_rollup_refute : { rollup : Sc_rollup_repr.t; opponent : Sc_rollup_repr.Staker.t; - refutation : Sc_rollup_game_repr.refutation; - is_opening_move : bool; + refutation : Sc_rollup_game_repr.refutation option; } -> Kind.sc_rollup_refute manager_operation | Sc_rollup_timeout : { diff --git a/src/proto_alpha/lib_protocol/raw_context.ml b/src/proto_alpha/lib_protocol/raw_context.ml index f14d37096401f7938221f23a4d9fcd6eca786fbd..212d9006799ab51201d6118ad65f3c7fa4d1e97e 100644 --- a/src/proto_alpha/lib_protocol/raw_context.ml +++ b/src/proto_alpha/lib_protocol/raw_context.ml @@ -996,7 +996,7 @@ let prepare_first_block ~level ~timestamp ctxt = max_available_messages = 1_000_000; (* TODO: https://gitlab.com/tezos/tezos/-/issues/2756 The following constants need to be refined. *) - stake_amount = Tez_repr.of_mutez_exn 32_000_000L; + stake_amount = Tez_repr.of_mutez_exn 10_000_000_000L; commitment_period_in_blocks = 30; max_lookahead_in_blocks = 30_000l; (* Number of active levels kept for executing outbox messages. diff --git a/src/proto_alpha/lib_protocol/sc_rollup_PVM_sem.ml b/src/proto_alpha/lib_protocol/sc_rollup_PVM_sem.ml index 5d688ddb87e609b562cd0c69d546bf8cf0f8d00b..90f448093b7f0846d6427044e086bcea28f1dc13 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_PVM_sem.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_PVM_sem.ml @@ -323,4 +323,8 @@ module type S = sig outbox. *) val produce_output_proof : context -> state -> output -> (output_proof, error) result Lwt.t + + module Internal_for_tests : sig + val insert_failure : state -> state Lwt.t + end end diff --git a/src/proto_alpha/lib_protocol/sc_rollup_arith.ml b/src/proto_alpha/lib_protocol/sc_rollup_arith.ml index 2fceb16ac63bf231729a1098e23194dd6bd236f5..fc82fc3fea568193aa60a7a465dcf0989431432a 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_arith.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_arith.ml @@ -31,6 +31,8 @@ module type P = sig type tree = Tree.tree + val hash_tree : tree -> State_hash.t + type proof val proof_encoding : proof Data_encoding.t @@ -671,6 +673,7 @@ module Make (Context : P) : let* vars_pp = Vars.pp in let* output_pp = Output.pp in let* stack = Stack.to_list in + let* current_tick_pp = CurrentTick.pp in return @@ fun fmt () -> Format.fprintf fmt @@ -682,6 +685,7 @@ module Make (Context : P) : %a@;\ %a@;\ %a@;\ + tick : %a@;\ vars : %a@;\ output :%a@;\ stack : %a@;\ @@ -700,6 +704,8 @@ module Make (Context : P) : () evaluation_result_pp () + current_tick_pp + () vars_pp () output_pp @@ -739,9 +745,7 @@ module Make (Context : P) : let* status = Status.get in match status with | Halted -> return State_hash.zero - | _ -> - Context_hash.to_bytes @@ Tree.hash state |> fun h -> - return @@ State_hash.hash_bytes [h] + | _ -> return @@ Context.hash_tree state in let open Lwt_syntax in let* state = Monad.run m state in @@ -858,15 +862,14 @@ module Make (Context : P) : let* () = ParsingResult.set None in let* () = ParserState.set SkipLayout in let* () = LexerState.set (0, 0) in - let* () = Status.set Parsing in let* () = Code.clear in return () let start_evaluating : unit t = let open Monad.Syntax in + let* () = Status.set Evaluating in let* () = EvaluationResult.set None in let* () = Stack.clear in - let* () = Status.set Evaluating in return () let stop_parsing outcome = @@ -1112,6 +1115,14 @@ module Make (Context : P) : return {output_proof; output_proof_state; output_proof_output} | Some (_, false) -> fail Arith_invalid_claim_about_outbox | None -> fail Arith_output_proof_production_failed + + module Internal_for_tests = struct + let insert_failure state = + let add n = Tree.add state ["failures"; string_of_int n] Bytes.empty in + let open Lwt_syntax in + let* n = Tree.length state ["failures"] in + add n + end end module ProtocolImplementation = Make (struct @@ -1129,6 +1140,9 @@ module ProtocolImplementation = Make (struct type tree = Context.tree + let hash_tree tree = + State_hash.context_hash_to_state_hash (Context.Tree.hash tree) + type proof = Context.Proof.tree Context.Proof.t let verify_proof p f = @@ -1139,8 +1153,7 @@ module ProtocolImplementation = Make (struct Lwt.return None let kinded_hash_to_state_hash = function - | `Value hash | `Node hash -> - State_hash.hash_bytes [Context_hash.to_bytes hash] + | `Value hash | `Node hash -> State_hash.context_hash_to_state_hash hash let proof_before proof = kinded_hash_to_state_hash proof.Context.Proof.before diff --git a/src/proto_alpha/lib_protocol/sc_rollup_arith.mli b/src/proto_alpha/lib_protocol/sc_rollup_arith.mli index dc3efc840c8dfe5cc9c4800c5887fe73809850bd..3fae2cb0915c0f2d815472a48559e4e1b4c845f2 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_arith.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_arith.mli @@ -133,6 +133,8 @@ module type P = sig type tree = Tree.tree + val hash_tree : tree -> Sc_rollup_repr.State_hash.t + type proof val proof_encoding : proof Data_encoding.t 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 7077617212618c55660f17e75fbcacc3053740d8..65e6cdd7b3a8dbb951ea9aee5705fec61409b9c0 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.ml @@ -36,6 +36,9 @@ type t = { dissection : (State_hash.t option * Sc_rollup_tick_repr.t) list; } +let player_equal p1 p2 = + match (p1, p2) with Alice, Alice -> true | Bob, Bob -> true | _, _ -> false + let player_encoding = let open Data_encoding in union @@ -44,13 +47,13 @@ let player_encoding = case ~title:"Alice" (Tag 0) - unit + (constant "alice") (function Alice -> Some () | _ -> None) (fun () -> Alice); case ~title:"Bob" (Tag 1) - unit + (constant "bob") (function Bob -> Some () | _ -> None) (fun () -> Bob); ] @@ -177,9 +180,9 @@ let initial inbox ~pvm_name ~(parent : Sc_rollup_commitment_repr.t) ]; } -type step = - | Dissection of (State_hash.t option * Sc_rollup_tick_repr.t) list - | Proof of Sc_rollup_proof_repr.t +type step = Dissection of dissection | Proof of Sc_rollup_proof_repr.t + +and dissection = (State_hash.t option * Sc_rollup_tick_repr.t) list let step_encoding = let open Data_encoding in @@ -257,7 +260,7 @@ let reason_encoding = case ~title:"Conflict_resolved" (Tag 0) - unit + (constant "conflict_resolved") (function Conflict_resolved -> Some () | _ -> None) (fun () -> Conflict_resolved); case @@ -269,7 +272,7 @@ let reason_encoding = case ~title:"Timeout" (Tag 2) - unit + (constant "timeout") (function Timeout -> Some () | _ -> None) (fun () -> Timeout); ] @@ -296,7 +299,7 @@ let status_encoding = case ~title:"Ongoing" (Tag 0) - unit + (constant "ongoing") (function Ongoing -> Some () | _ -> None) (fun () -> Ongoing); case diff --git a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli index 2df0a7415e130f55c484788da37b486eee7d46b7..44230e35034f170b37355f3f18aeb3483fd162a2 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_game_repr.mli @@ -170,6 +170,8 @@ type t = { (** Return the other player *) val opponent : player -> player +val player_equal : player -> player -> bool + val encoding : t Data_encoding.t val pp_dissection : @@ -236,9 +238,9 @@ val initial : (** A [step] in the game is either a new dissection (if there are intermediate ticks remaining to put in it) or a proof. *) -type step = - | Dissection of (State_hash.t option * Sc_rollup_tick_repr.t) list - | Proof of Sc_rollup_proof_repr.t +type step = Dissection of dissection | Proof of Sc_rollup_proof_repr.t + +and dissection = (State_hash.t option * Sc_rollup_tick_repr.t) list (** A [refutation] is a move in the game. [choice] is the final tick in the current dissection at which the two players agree. *) diff --git a/src/proto_alpha/lib_protocol/sc_rollup_proof_repr.ml b/src/proto_alpha/lib_protocol/sc_rollup_proof_repr.ml index 84333262f7aecba559d54fa31d9d8bde80781e0b..4e5a3f8dd01eb967284c214627930a9415c66cfa 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_proof_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_proof_repr.ml @@ -115,8 +115,6 @@ module type PVM_with_context_and_state = sig val state : state end -type error += Proof_cannot_be_wrapped - let produce pvm_and_state inbox commit_level = let open Lwt_result_syntax in let (module P : PVM_with_context_and_state) = pvm_and_state in @@ -144,4 +142,4 @@ let produce pvm_and_state inbox commit_level = end in match Sc_rollups.wrap_proof (module P_with_proof) with | Some pvm_step -> return {pvm_step; inbox} - | None -> fail Proof_cannot_be_wrapped + | None -> proof_error "Could not wrap proof" 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..1751ff8c82bf11b70a09d8bc4b4041d3c2c56988 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.ml @@ -45,6 +45,20 @@ let timeout_level ctxt = let level = Raw_context.current_level ctxt in Raw_level_repr.add level.level timeout_period_in_blocks +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 = @@ -230,19 +244,23 @@ let init_game ctxt rollup ~refuter ~defender = let* ctxt, _ = Store.Opponent.init (ctxt, rollup) defender refuter in return (game, ctxt) -let game_move ctxt rollup ~player ~opponent refutation ~is_opening_move = +let start_game ctxt rollup ~player ~opponent = let open Lwt_tzresult_syntax in - let ({alice; bob} as stakers : Sc_rollup_game_repr.Index.t) = - Sc_rollup_game_repr.Index.make player opponent - in - let* game, ctxt = - if is_opening_move then - init_game ctxt rollup ~refuter:player ~defender:opponent - else get_game ctxt rollup stakers + let idx = Sc_rollup_game_repr.Index.make player opponent in + let* game, ctxt = init_game ctxt rollup ~refuter:player ~defender:opponent in + let* ctxt, _ = Store.Game.update (ctxt, rollup) idx game in + let* ctxt, _ = + Store.Game_timeout.update (ctxt, rollup) idx (timeout_level ctxt) in + return ctxt + +let game_move ctxt rollup ~player ~opponent refutation = + let open Lwt_tzresult_syntax in + let idx = Sc_rollup_game_repr.Index.make player opponent in + let* game, ctxt = get_game ctxt rollup idx in let* () = fail_unless - (let turn = match game.turn with Alice -> alice | Bob -> bob in + (let turn = match game.turn with Alice -> idx.alice | Bob -> idx.bob in Sc_rollup_repr.Staker.equal turn player) Sc_rollup_wrong_turn in @@ -252,9 +270,9 @@ let game_move ctxt rollup ~player ~opponent refutation ~is_opening_move = match move_result with | Either.Left outcome -> return (Some outcome, ctxt) | Either.Right new_game -> - let* ctxt, _ = Store.Game.update (ctxt, rollup) stakers new_game in + let* ctxt, _ = Store.Game.update (ctxt, rollup) idx new_game in let* ctxt, _ = - Store.Game_timeout.update (ctxt, rollup) stakers (timeout_level ctxt) + Store.Game_timeout.update (ctxt, rollup) idx (timeout_level ctxt) in return (None, ctxt) @@ -293,3 +311,48 @@ let apply_outcome ctxt rollup stakers (outcome : Sc_rollup_game_repr.outcome) = 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 make_conflict ctxt rollup other (our_point, their_point) = + let our_hash = our_point.hash and their_hash = their_point.hash in + let open Lwt_tzresult_syntax 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} + +let conflicting_stakers_uncarbonated ctxt rollup staker = + let open Lwt_tzresult_syntax in + let* 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 diff --git a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli index faac56a0eaaf88a7c1ecde61eca07fa5b30adbf9..9a34b595fbe64bc4497b0a46ca78b664654a6224 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_refutation_storage.mli @@ -33,10 +33,52 @@ type point = { type conflict_point = point * point -(** [game_move ctxt rollup player opponent refutation is_opening_move] +(** [get_ongoing_game_for_staker ctxt rollup staker] returns [Some game] if [staker] + is currently playing a refutation game in the [rollup]. *) +val get_ongoing_game_for_staker : + Raw_context.t -> + Sc_rollup_repr.t -> + Sc_rollup_repr.Staker.t -> + ((Sc_rollup_game_repr.t * Sc_rollup_game_repr.Index.t) option * Raw_context.t) + tzresult + Lwt.t + +(** A conflict between a staker and an [other] staker. The conflict is + about the commitment that follows the [parent_commitment]: + [their_commitment] and [our_commitment] are distinct, hence in + conflict. *) +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; +} + +val conflict_encoding : conflict Data_encoding.t + +(** [conflicting_stakers_uncarbonated rollup staker] returns the list + of conflicts with [staker] in [rollup]. + + Notice that this operation can be expensive as it is proportional + to the number of stakers multiplied by the number of commitments in + the staked branches. Fortunately, this operation is only useful as + an RPC for the rollup node to look for a new conflict to solve. *) +val conflicting_stakers_uncarbonated : + Raw_context.t -> + Sc_rollup_repr.t -> + Sc_rollup_repr.Staker.t -> + conflict list tzresult Lwt.t + +val start_game : + Raw_context.t -> + Sc_rollup_repr.t -> + player:Signature.public_key_hash -> + opponent:Signature.public_key_hash -> + Raw_context.t tzresult Lwt.t + +(** [game_move ctxt rollup player opponent refutation] handles the storage-side logic for when one of the players makes a - move in the game. It initializes the game if [is_opening_move] is - [true]. Otherwise, it checks the game already exists. Then it checks + move in the game. It checks the game already exists. Then it checks that [player] is the player whose turn it is; if so, it applies [refutation] using the [play] function. @@ -61,7 +103,7 @@ type conflict_point = point * point {li [Sc_rollup_wrong_turn] if a player is trying to move out of turn} } - + The [is_opening_move] argument is included here to make sure that an operation intended to start a refutation game is never mistaken for an operation to play the second move of the game---this may @@ -76,7 +118,6 @@ val game_move : player:Sc_rollup_repr.Staker.t -> opponent:Sc_rollup_repr.Staker.t -> Sc_rollup_game_repr.refutation -> - is_opening_move:bool -> (Sc_rollup_game_repr.outcome option * Raw_context.t) tzresult Lwt.t (* TODO: #2902 update reference to timeout period in doc-string *) diff --git a/src/proto_alpha/lib_protocol/sc_rollup_repr.ml b/src/proto_alpha/lib_protocol/sc_rollup_repr.ml index db14462b787e1d22ea374a4aec61fe053798508f..54d93a6b21b668b315fe1b0ab8fd0f5bd9f53ad4 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_repr.ml @@ -109,6 +109,8 @@ module State_hash = struct let () = Base58.check_encoded_prefix b58check_encoding prefix encoded_size include Path_encoding.Make_hex (H) + + let context_hash_to_state_hash h = hash_bytes [Context_hash.to_bytes h] end type t = Address.t diff --git a/src/proto_alpha/lib_protocol/sc_rollup_repr.mli b/src/proto_alpha/lib_protocol/sc_rollup_repr.mli index e24094de265155f1f20a2d90df1198e0d0c4d2a1..d53e1b94e12b20ac5a4ba10c82fb3f3c2976915e 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_repr.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_repr.mli @@ -61,7 +61,11 @@ module Internal_for_tests : sig val originated_sc_rollup : Origination_nonce.t -> Address.t end -module State_hash : S.HASH +module State_hash : sig + include S.HASH + + val context_hash_to_state_hash : Context_hash.t -> t +end (** Number of messages consumed by a single commitment. This represents a claim about the shape of the Inbox, which can be disputed as part of a commitment diff --git a/src/proto_alpha/lib_protocol/sc_rollup_storage.ml b/src/proto_alpha/lib_protocol/sc_rollup_storage.ml index 06a0fea1999db959ade3155cc350eaf600f761e8..50a3507cba7a0abe5a080bb7fc4ef05bff91c532 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_storage.ml @@ -40,6 +40,31 @@ let originate ctxt ~kind ~boot_sector ~parameters_ty = >>=? fun (ctxt, param_ty_size_diff, _added) -> let inbox = Sc_rollup_inbox_repr.empty address level.level in Store.Inbox.init ctxt address inbox >>=? fun (ctxt, inbox_size_diff) -> + (* FIXME: #3054 + This is a temporary code until proper origination commitment is implemented. + *) + let number_of_messages = + match Sc_rollup_repr.Number_of_messages.of_int32 0l with + | None -> assert false + | Some x -> x + in + let number_of_ticks = + match Sc_rollup_repr.Number_of_ticks.of_int32 0l with + | None -> assert false + | Some x -> x + in + let initial_commitment = + Sc_rollup_commitment_repr. + { + compressed_state = Sc_rollup_repr.State_hash.zero; + inbox_level = level.level; + predecessor = Commitment_hash.zero; + number_of_messages; + number_of_ticks; + } + in + Store.Commitments.init (ctxt, address) Commitment_hash.zero initial_commitment + >>=? fun (ctxt, _size_diff) -> Store.Last_cemented_commitment.init ctxt address Commitment_hash.zero >>=? fun (ctxt, lcc_size_diff) -> Store.Staker_count.init ctxt address 0l >>=? fun (ctxt, stakers_size_diff) -> diff --git a/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.ml b/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.ml index 22156b964fedbc8dc43fb4ac1185cf1cf2148ca8..8ce3b1c62607245e6fe1291d73f645636a84ab98 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.ml @@ -30,6 +30,8 @@ let initial = zero let next = succ +let jump tick z = max initial (add tick z) + let pp = pp_print let encoding = Data_encoding.n diff --git a/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.mli b/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.mli index feb5c90eb63f6c8088cd85e8a48679ef3c301126..56b6aa33a4f545e523e79e1f3499c4a534f6c9cc 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.mli +++ b/src/proto_alpha/lib_protocol/sc_rollup_tick_repr.mli @@ -36,6 +36,10 @@ val initial : t (** [next tick] returns the counter successor of [tick]. No overflow can happen. *) val next : t -> t +(** [jump tick k] moves [tick] by [k] seps. No overflow can happen. + The move stops at [initial] when going back in time. *) +val jump : t -> Z.t -> t + (** [distance t1 t2] is the absolute value of the difference between [t1] and [t2]. *) val distance : t -> t -> Z.t diff --git a/src/proto_alpha/lib_protocol/sc_rollup_wasm.ml b/src/proto_alpha/lib_protocol/sc_rollup_wasm.ml index 8243b7403fc735cf4c55e8c4e02d0b9cf7131b13..350fbdba2013f30d6d0298960eed73b4d5627c2f 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_wasm.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_wasm.ml @@ -463,6 +463,14 @@ module V2_0_0 = struct let open Lwt_result_syntax in let*! output_proof_state = state_hash state in return {output_proof_state; output_proof_output} + + module Internal_for_tests = struct + let insert_failure state = + let add n = Tree.add state ["failures"; string_of_int n] Bytes.empty in + let open Lwt_syntax in + let* n = Tree.length state ["failures"] in + add n + end end module ProtocolImplementation = Make (struct diff --git a/src/proto_alpha/lib_protocol/storage.ml b/src/proto_alpha/lib_protocol/storage.ml index f5f916ec2278e8c48213a5fd88676dfe9996293e..b691a72096580f12e87d8e491eaf15bdbec0370c 100644 --- a/src/proto_alpha/lib_protocol/storage.ml +++ b/src/proto_alpha/lib_protocol/storage.ml @@ -480,11 +480,7 @@ module Big_map = struct let encoding = Script_repr.expr_encoding end) - module Contents : - Non_iterable_indexed_carbonated_data_storage_with_values - with type key = Script_expr_hash.t - and type value = Script_repr.expr - and type t := key = struct + module Contents = struct module I = Storage_functors.Make_indexed_carbonated_data_storage (Make_subcontext (Registered) (Indexed_context.Raw_context) @@ -518,7 +514,10 @@ module Big_map = struct let add = I.add - let list_values = I.list_values + let list_values ?offset ?length (ctxt, id) = + let open Lwt_tzresult_syntax in + let* ctxt, values = I.list_values ?offset ?length (ctxt, id) in + return (ctxt, List.map snd values) let consume_deserialize_gas ctxt value = Raw_context.consume_gas ctxt (Script_repr.deserialized_cost value) @@ -1600,19 +1599,27 @@ module Sc_rollup = struct let encoding = Sc_rollup_commitment_repr.Hash.encoding end) + module Stakers_subcontext = + Make_subcontext (Registered) (Indexed_context.Raw_context) + (struct + let name = ["stakers"] + end) + module Stakers = Make_indexed_carbonated_data_storage - (Make_subcontext (Registered) (Indexed_context.Raw_context) - (struct - let name = ["stakers"] - end)) - (Public_key_hash_index) + (Stakers_subcontext) + (Public_key_hash_index) (struct type t = Sc_rollup_commitment_repr.Hash.t let encoding = Sc_rollup_commitment_repr.Hash.encoding end) + let stakers (ctxt : Raw_context.t) (rollup : Sc_rollup_repr.t) = + let open Lwt_tzresult_syntax in + let* _, values = Stakers.list_values (ctxt, rollup) in + return values + module Staker_count = Indexed_context.Make_carbonated_map (struct diff --git a/src/proto_alpha/lib_protocol/storage.mli b/src/proto_alpha/lib_protocol/storage.mli index e6aa79faa30a4e6cd6930a5147047c545cf5c9f9..74d6245042082c11250f0a7a31810c7d1bcd294f 100644 --- a/src/proto_alpha/lib_protocol/storage.mli +++ b/src/proto_alpha/lib_protocol/storage.mli @@ -722,6 +722,13 @@ module Sc_rollup : sig and type value = Sc_rollup_commitment_repr.Hash.t and type t = Raw_context.t * Sc_rollup_repr.t + val stakers : + Raw_context.t -> + Sc_rollup_repr.t -> + (Signature.Public_key_hash.t * Sc_rollup_commitment_repr.Hash.t) list + tzresult + Lwt.t + (** Cache: This should always be the number of entries in [Stakers]. Combined with {!Commitment_stake_count} (see below), this ensures we can diff --git a/src/proto_alpha/lib_protocol/storage_functors.ml b/src/proto_alpha/lib_protocol/storage_functors.ml index d32d3f00c45626102bd9c53593d79b5911708f3f..4170b7bb2f5548dd27c8897e7b60ba9fee3fee38 100644 --- a/src/proto_alpha/lib_protocol/storage_functors.ml +++ b/src/proto_alpha/lib_protocol/storage_functors.ml @@ -500,7 +500,7 @@ module Make_indexed_carbonated_data_storage_INTERNAL | None -> assert false | Some key -> get_unprojected s key >|=? fun (s, value) -> - (s, value :: rev_values, 0, pred length)) + (s, (key, value) :: rev_values, 0, pred length)) | _ -> Lwt.return acc) >|=? fun (s, rev_values, _offset, _length) -> (C.project s, List.rev rev_values) diff --git a/src/proto_alpha/lib_protocol/storage_sigs.ml b/src/proto_alpha/lib_protocol/storage_sigs.ml index ad16d90af8555bdb258883746b3d1bb33b7d8436..7871fc33b00b66a587a79df8b78611283b59ed20 100644 --- a/src/proto_alpha/lib_protocol/storage_sigs.ml +++ b/src/proto_alpha/lib_protocol/storage_sigs.ml @@ -222,7 +222,7 @@ module type Non_iterable_indexed_carbonated_data_storage_with_values = sig ?offset:int -> ?length:int -> t -> - (Raw_context.t * value list) tzresult Lwt.t + (Raw_context.t * (key * value) list) tzresult Lwt.t end module type Non_iterable_indexed_carbonated_data_storage_INTERNAL = sig diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.ml b/src/proto_alpha/lib_protocol/test/helpers/op.ml index 53da6f52100bf357dc2340db844c18e7c3156b27..5cdf16cb3708c0c0c4e6aa1e22e70b2ff1d99164 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/op.ml @@ -861,7 +861,7 @@ let sc_rollup_add_messages ?force_reveal ?counter ?fee ?gas_limit ?storage_limit sign account.sk ctxt to_sign_op let sc_rollup_refute ?force_reveal ?counter ?fee ?gas_limit ?storage_limit ctxt - (src : Contract.t) rollup opponent refutation is_opening_move = + (src : Contract.t) rollup opponent refutation = manager_operation ?force_reveal ?counter @@ -870,7 +870,7 @@ let sc_rollup_refute ?force_reveal ?counter ?fee ?gas_limit ?storage_limit ctxt ?storage_limit ~source:src ctxt - (Sc_rollup_refute {rollup; opponent; refutation; is_opening_move}) + (Sc_rollup_refute {rollup; opponent; refutation}) >>=? fun to_sign_op -> Context.Contract.manager ctxt src >|=? fun account -> sign account.sk ctxt to_sign_op diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.mli b/src/proto_alpha/lib_protocol/test/helpers/op.mli index df18aab209c118a35895d7e9594031a18076a5f1..26ebf8a9f642a8db621b05f03abebbc99a5ded8d 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/op.mli @@ -603,8 +603,7 @@ val sc_rollup_refute : Contract.t -> Sc_rollup.t -> public_key_hash -> - Sc_rollup.Game.refutation -> - bool -> + Sc_rollup.Game.refutation option -> Operation.packed tzresult Lwt.t val sc_rollup_timeout : 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 82b21eb0353636bed2e26844520d76475d904531..4fb13dc2431372c13c43614e1ad444f875c073bc 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 @@ -574,25 +574,16 @@ module Strategies (PVM : TestPVM with type hash = State_hash.t) = struct in let* outcome = let rec loop game refuter_move = - let player = if refuter_move then "refuter" else "defender" in let* move = if refuter_move then refuter_client.next_move game else defender_client.next_move game in match move with - | None -> - Printf.eprintf "@[No move from %s@]" player ; - return (if refuter_move then Defender_wins else Refuter_wins) + | None -> return (if refuter_move then Defender_wins else Refuter_wins) | Some move -> ( - Format.eprintf - "@[Move from %s is %a@]@." - player - Game.pp_refutation - move ; let* game_result = Game.play game move in match game_result with | Either.Left outcome -> - Format.eprintf "@[%a@]@." Game.pp_outcome outcome ; return (loser_to_outcome_for_tests outcome.loser alice_is_refuter) | Either.Right game -> loop game (not refuter_move)) diff --git a/tezt/lib_tezos/sc_rollup_node.ml b/tezt/lib_tezos/sc_rollup_node.ml index 8cdac867d2079d3415f17520881978c622770c77..6fc4628295b4f2162db6f2dc36c5390491254846 100644 --- a/tezt/lib_tezos/sc_rollup_node.ml +++ b/tezt/lib_tezos/sc_rollup_node.ml @@ -86,27 +86,28 @@ let layer1_port sc_node = Node.rpc_port sc_node.persistent_state.node let spawn_command sc_node = Process.spawn ~name:sc_node.name ~color:sc_node.color sc_node.path -let spawn_config_init sc_node rollup_address = +let spawn_config_init sc_node ?loser_mode rollup_address = spawn_command sc_node - [ - "config"; - "init"; - "on"; - rollup_address; - "with"; - "operator"; - operator_pkh sc_node; - "--data-dir"; - data_dir sc_node; - "--rpc-addr"; - rpc_host sc_node; - "--rpc-port"; - string_of_int @@ rpc_port sc_node; - ] - -let config_init sc_node rollup_address = - let process = spawn_config_init sc_node rollup_address in + ([ + "config"; + "init"; + "on"; + rollup_address; + "with"; + "operator"; + operator_pkh sc_node; + "--data-dir"; + data_dir sc_node; + "--rpc-addr"; + rpc_host sc_node; + "--rpc-port"; + string_of_int @@ rpc_port sc_node; + ] + @ match loser_mode with None -> [] | Some mode -> ["--loser-mode"; mode]) + +let config_init sc_node ?loser_mode rollup_address = + let process = spawn_config_init sc_node ?loser_mode rollup_address in let* output = Process.check_and_read_stdout process in match output diff --git a/tezt/lib_tezos/sc_rollup_node.mli b/tezt/lib_tezos/sc_rollup_node.mli index e837793e420799049eb0d1120bd911445af2f3ad..1a27ab14e55962ef86bcd2f94b23f58398ef1c36 100644 --- a/tezt/lib_tezos/sc_rollup_node.mli +++ b/tezt/lib_tezos/sc_rollup_node.mli @@ -109,9 +109,9 @@ val wait : t -> Unix.process_status Lwt.t a SIGKILL is sent instead of a SIGTERM. *) val terminate : ?kill:bool -> t -> unit Lwt.t -(** Run [tezos-sc-rollup-node-alpha config init rollup_address]. +(** Run [tezos-sc-rollup-node-alpha config init ?loser_mode rollup_address]. Returns the name of the resulting configuration file. *) -val config_init : t -> string -> string Lwt.t +val config_init : t -> ?loser_mode:string -> string -> string Lwt.t module Config_file : sig (** Sc node configuration files. *) diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 4880721beff8e1e43152acf2f3d129f3984ae679..eb1513fcf62ae1c9ea0d05b1c00d4f3a3f708816 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -484,10 +484,7 @@ let send_messages ?batch_size n sc_rollup_address client = messages let to_text_messages_arg msgs = - let text_messages = - List.map (fun msg -> Hex.of_string msg |> Hex.show) msgs - in - let json = Ezjsonm.list Ezjsonm.string text_messages in + let json = Ezjsonm.list Ezjsonm.string msgs in "text:" ^ Ezjsonm.to_string ~minify:true json let send_text_messages client sc_rollup_address msgs = @@ -1159,7 +1156,7 @@ let commitment_stored _protocol sc_rollup_node sc_rollup_address _node client = (Check.option Check.int) ~error_msg: "Number of messages processed by commitment is different from the \ - number of messages expected (%L = %R)") ; + number of messages expected (%L expected <> %R processed)") ; let* published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in @@ -1913,6 +1910,85 @@ let test_rollup_client_list_keys = pp maybe_keys) +(* Refutation game scenarios + ------------------------- +*) +let test_refutation_scenario ?commitment_period ?challenge_window variant + (loser_mode, inputs, final_level) = + test_scenario + ?commitment_period + ?challenge_window + { + tags = ["refutation"; "node"]; + variant; + description = "observing the winning strategy of refutation game"; + } + @@ fun _protocol sc_rollup_node sc_rollup_address node client -> + let honest = Constant.bootstrap1.public_key_hash + and dishonest = Constant.bootstrap2.public_key_hash in + + (* Initialize a dishonest rollup node using the provided loser_mode. *) + let sc_rollup_node2 = + Sc_rollup_node.create node client ~operator_pkh:dishonest + in + let* _configuration_filename = + Sc_rollup_node.config_init ~loser_mode sc_rollup_node2 sc_rollup_address + in + + (* + Send messages to the rollup and wait enough time for the conflicting + commitments to be published, and refuted. + *) + let* () = Sc_rollup_node.run sc_rollup_node in + let* () = Sc_rollup_node.run sc_rollup_node2 in + + let block inputs = + let* () = + Lwt_list.iter_s (send_text_messages client sc_rollup_address) inputs + in + Client.bake_for_and_wait client + in + let* () = Lwt_list.iter_s block inputs in + let* () = bake_levels (final_level - List.length inputs) client in + + (* + We verify that the dishonest node has lost its bond while the + honest node has its bond untouched. + *) + let* honest_balances = contract_balances ~pkh:honest client in + let* dishonest_balances = contract_balances ~pkh:dishonest client in + + let stake_value = 10_000_000_000 in + + Check.( + (honest_balances.frozen = stake_value) + int + ~error_msg: + "Frozen bond of honest player is incorrect, expected value = %R, got %L") ; + + Check.( + (dishonest_balances.frozen = 0) + int + ~error_msg: + "Frozen bond of dishonest player is incorrect, expected value = %R, \ + got %L") ; + + return () + +let rec swap i l = + if i <= 0 then l + else match l with [_] | [] -> l | x :: y :: l -> y :: swap (i - 1) (x :: l) + +let valid_inputs_for_nlevels n = + List.init n @@ fun i -> + [swap i ["3 3 +"; "1"; "1 1 x"; "3 7 8 + * y"; "2 2 out"]] + +let test_refutation protocols = + let challenge_window = 50 in + [("failure_in_internal_step", ("5 1 3", valid_inputs_for_nlevels 10, 30))] + |> List.iter (fun (variant, inputs) -> + test_refutation_scenario ~challenge_window variant inputs protocols) + let register ~protocols = test_origination protocols ; test_rollup_node_configuration protocols ; @@ -1960,4 +2036,5 @@ let register ~protocols = test_rollup_node_uses_boot_sector protocols ; test_rollup_client_show_address protocols ; test_rollup_client_generate_keys protocols ; - test_rollup_client_list_keys protocols + test_rollup_client_list_keys protocols ; + test_refutation protocols