diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/RPC_server.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/RPC_server.ml index ca55fb24a41b8500354054b1dca712103bfa90f6..2c12e8358aba2802295b066cb5bfc6ecfc602925 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/RPC_server.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/RPC_server.ml @@ -39,7 +39,7 @@ let get_finalized store = let*! head = State.get_finalized_head_opt store in match head with | None -> failwith "No finalized head" - | Some {hash; _} -> return hash + | Some {header = {block_hash; _}; _} -> return block_hash let get_last_cemented (node_ctxt : _ Node_context.t) = let open Lwt_result_syntax in @@ -376,10 +376,10 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct let commitment_hash = Sc_rollup_block.most_recent_commitment head.header in - let*! commitment = - Store.Commitments.get node_ctxt.store commitment_hash + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash in - return (commitment, commitment_hash, None) + return (commitment, commitment_hash) in return res @@ -396,10 +396,16 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct (* The corresponding level in Store.Commitments.published_at_level is available only when the commitment has been published and included in a block. *) - let*! published_at_level = + let*! published_at_level_info = Store.Commitments_published_at_level.find node_ctxt.store hash in - return (commitment, hash, published_at_level) + let first_published, published = + match published_at_level_info with + | None -> (None, None) + | Some {first_published_at_level; published_at_level} -> + (Some first_published_at_level, published_at_level) + in + return (commitment, hash, first_published, published) in return result @@ -496,8 +502,8 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct let finalized = match finalized_head with | None -> false - | Some {level = finalized_level; _} -> - Compare.Int32.(inbox_level <= finalized_level) + | Some {header = {level = finalized_level; _}; _} -> + Compare.Int32.(inbox_level <= Raw_level.to_int32 finalized_level) in let cemented = Compare.Int32.(inbox_level <= Raw_level.to_int32 node_ctxt.lcc.level) @@ -560,11 +566,15 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct commitment_hash in match published_at with - | None -> + | None | Some {published_at_level = None; _} -> (* Commitment not published yet *) return (Sc_rollup_services.Included (info, inbox_info)) - | Some published_at -> + | Some + { + first_published_at_level; + published_at_level = Some published_at_level; + } -> (* Commitment published *) let*! commitment = Store.Commitments.get @@ -573,7 +583,12 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct in let commitment_info = Sc_rollup_services. - {commitment; commitment_hash; published_at} + { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + } in return (Sc_rollup_services.Committed diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment.ml index b1a5c83952f212f13e1a43a9b1d03cf7d448d225..82ab504670bb7955eae9121bd9ff0e7d080ce9a8 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment.ml @@ -49,6 +49,11 @@ let add_level level increment = if increment < 0 then invalid_arg "Commitment.add_level negative increment" ; Raw_level.Internal_for_tests.add level increment +let sub_level level decrement = + (* We only use this function with positive increments so it is safe *) + if decrement < 0 then invalid_arg "Commitment.sub_level negative decrement" ; + Raw_level.Internal_for_tests.sub level decrement + let sc_rollup_commitment_period node_ctxt = node_ctxt.Node_context.protocol_constants.parametric.sc_rollup .commitment_period_in_blocks @@ -57,29 +62,6 @@ let sc_rollup_challenge_window node_ctxt = node_ctxt.Node_context.protocol_constants.parametric.sc_rollup .challenge_window_in_blocks -let next_lcc_level node_ctxt = - add_level - node_ctxt.Node_context.lcc.level - (sc_rollup_commitment_period node_ctxt) - -(** Returns the next level for which a commitment can be published, i.e. the - level that is [commitment_period] blocks after the last published one. It - returns [None] if this level is not finalized because we only publish - commitments for inbox of finalized L1 blocks. *) -let next_publishable_level node_ctxt = - let open Lwt_option_syntax in - let lpc_level = - match node_ctxt.Node_context.lpc with - | None -> node_ctxt.genesis_info.level - | Some lpc -> lpc.inbox_level - in - let next_level = - add_level lpc_level (sc_rollup_commitment_period node_ctxt) - in - let* finalized_level = State.get_finalized_head_opt node_ctxt.store in - if Raw_level.(of_int32_exn finalized_level.level < next_level) then fail - else return next_level - let next_commitment_level node_ctxt last_commitment_level = add_level last_commitment_level (sc_rollup_commitment_period node_ctxt) @@ -189,126 +171,197 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct in return_some commitment_hash - let block_of_known_level (node_ctxt : _ Node_context.t) level = - let open Lwt_option_syntax in + let missing_commitments (node_ctxt : _ Node_context.t) = + let open Lwt_syntax in + let lpc_level = + match node_ctxt.lpc with + | None -> node_ctxt.genesis_info.level + | Some lpc -> lpc.inbox_level + in let* head = State.last_processed_head_opt node_ctxt.store in - if Raw_level.(head.header.level < level) then - (* Level is not known yet *) - fail - else - let*! block_hash = - State.hash_of_level node_ctxt (Raw_level.to_int32 level) + let next_head_level = + Option.map + (fun (b : Sc_rollup_block.t) -> Raw_level.succ b.header.level) + head + in + let sc_rollup_challenge_window_int32 = + sc_rollup_challenge_window node_ctxt |> Int32.of_int + in + let rec gather acc (commitment_hash : Sc_rollup.Commitment.Hash.t) = + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash in - let*? block_hash = Result.to_option block_hash in - Store.L2_blocks.find node_ctxt.store block_hash - - let get_commitment_and_publish ~check_lcc_hash node_ctxt next_level_to_publish - = - let open Lwt_result_syntax in - let*! commitment = - let open Lwt_option_syntax in - let* block = block_of_known_level node_ctxt next_level_to_publish in - let*? commitment_hash = block.header.commitment_hash in - Store.Commitments.find node_ctxt.store commitment_hash + match commitment with + | None -> return acc + | Some commitment + when Raw_level.(commitment.inbox_level <= node_ctxt.lcc.level) -> + (* Commitment is before or at the LCC, we have reached the end. *) + return acc + | Some commitment when Raw_level.(commitment.inbox_level <= lpc_level) -> + (* Commitment is before the last published one, we have also reached + the end because we only publish commitments that are for the inbox + of a finalized L1 block. *) + return acc + | Some commitment -> + let* published_info = + Store.Commitments_published_at_level.find + node_ctxt.store + commitment_hash + in + let past_curfew = + match (published_info, next_head_level) with + | None, _ | _, None -> false + | Some {first_published_at_level; _}, Some next_head_level -> + Raw_level.diff next_head_level first_published_at_level + > sc_rollup_challenge_window_int32 + in + let acc = if past_curfew then acc else commitment :: acc in + (* We keep the commitment and go back to the previous one. *) + gather acc commitment.predecessor in - match commitment with - | None -> - (* Commitment not available *) - return_unit - | Some commitment -> ( - let* () = - if check_lcc_hash then - let open Lwt_result_syntax in - if - Sc_rollup.Commitment.Hash.equal - node_ctxt.lcc.commitment - commitment.predecessor - then return () - else - let*! () = - Commitment_event.commitment_parent_is_not_lcc - commitment.inbox_level - commitment.predecessor - node_ctxt.lcc.commitment - in - tzfail - (Sc_rollup_node_errors.Commitment_predecessor_should_be_LCC - commitment) - else return_unit + let* finalized_block = State.get_finalized_head_opt node_ctxt.store in + match finalized_block with + | None -> return_nil + | Some finalized -> + (* Start from finalized block's most recent commitment and gather all + commitments that are missing. *) + let commitment = + Sc_rollup_block.most_recent_commitment finalized.header in - let operator = Node_context.get_operator node_ctxt Publish in - match operator with - | None -> - (* Configured to not publish commitments *) - return_unit - | Some source -> - let publish_operation = - Sc_rollup_publish {rollup = node_ctxt.rollup_address; commitment} - in - let* _hash = - Injector.add_pending_operation ~source publish_operation - in - (* TODO: https://gitlab.com/tezos/tezos/-/issues/3462 - Decouple commitments from head processing - - Move the following, in a part where we know the operation is - included. *) - node_ctxt.lpc <- Some commitment ; - return_unit) + gather [] commitment - let publish_commitment node_ctxt = + let publish_commitment (node_ctxt : _ Node_context.t) ~source + (commitment : Sc_rollup.Commitment.t) = let open Lwt_result_syntax in - (* Check level of next publishable commitment and avoid publishing if it is - on or before the last cemented commitment. - *) - let next_lcc_level = next_lcc_level node_ctxt in - let*! next_publishable_level = next_publishable_level node_ctxt in - match next_publishable_level with - | None -> return_unit - | Some next_publishable_level -> - let check_lcc_hash, level_to_publish = - if Raw_level.(next_publishable_level < next_lcc_level) then - (* - - This situation can happen if the rollup node has been - shutdown and the rollup has been progressing in the - meantime. In that case, the rollup node must wait to reach - [lcc_level + commitment_frequency] to publish the - commitment. ([lcc_level] is a multiple of - commitment_frequency.) - - We need to check that the published commitment comes - immediately after the last cemented commitment, otherwise - that's an invalid commitment. + let publish_operation = + Sc_rollup_publish {rollup = node_ctxt.rollup_address; commitment} + in + let* _hash = Injector.add_pending_operation ~source publish_operation in + return_unit - *) - (true, next_lcc_level) - else (false, next_publishable_level) - in - get_commitment_and_publish node_ctxt level_to_publish ~check_lcc_hash + let publish_commitments (node_ctxt : _ Node_context.t) = + let open Lwt_result_syntax in + let operator = Node_context.get_operator node_ctxt Publish in + match operator with + | None -> + (* Configured to not publish commitments *) + return_unit + | Some source -> + let*! commitments = missing_commitments node_ctxt in + List.iter_es (publish_commitment node_ctxt ~source) commitments + (* Commitments can only be cemented after [sc_rollup_challenge_window] has + passed since they were first published. *) let earliest_cementing_level node_ctxt commitment_hash = let open Lwt_option_syntax in - let+ published_at_level = + let+ {first_published_at_level; _} = Store.Commitments_published_at_level.find node_ctxt.Node_context.store commitment_hash in - add_level published_at_level (sc_rollup_challenge_window node_ctxt) + add_level first_published_at_level (sc_rollup_challenge_window node_ctxt) - let can_be_cemented node_ctxt earliest_cementing_level head_level - commitment_hash = - let {Node_context.cctxt; rollup_address; _} = node_ctxt in + (** [latest_cementable_commitment node_ctxt head] is the most recent commitment + hash that could be cemented in [head]'s successor if: + + - all its predecessors were cemented + - it would have been first published at the same level as its inbox + + It does not need to be exact but it must be an upper bound on which we can + start the search for cementable commitments. *) + let latest_cementable_commitment (node_ctxt : _ Node_context.t) + (head : Sc_rollup_block.t) = + let open Lwt_option_syntax in + let commitment_hash = Sc_rollup_block.most_recent_commitment head.header in + let* commitment = Store.Commitments.find node_ctxt.store commitment_hash in + let*? cementable_level_bound = + sub_level commitment.inbox_level (sc_rollup_challenge_window node_ctxt) + in + if Raw_level.(cementable_level_bound <= node_ctxt.lcc.level) then fail + else + let* cementable_bound_block_hash = + State.hash_of_level_opt + node_ctxt + (Raw_level.to_int32 cementable_level_bound) + in + let* cementable_bound_block = + Store.L2_blocks.find node_ctxt.store cementable_bound_block_hash + in + let cementable_commitment = + Sc_rollup_block.most_recent_commitment cementable_bound_block.header + in + return cementable_commitment + + let cementable_commitments (node_ctxt : _ Node_context.t) = let open Lwt_result_syntax in - if earliest_cementing_level <= head_level then - Plugin.RPC.Sc_rollup.can_be_cemented - cctxt - (cctxt#chain, cctxt#block) - rollup_address - commitment_hash - else return_false + let ( let*& ) x f = + (* A small monadic combinator to return an empty list of cementable + commitments on None results. *) + let*! x = x in + match x with None -> return_nil | Some x -> f x + in + let*& head = State.last_processed_head_opt node_ctxt.Node_context.store in + let head_level = head.header.level in + let rec gather acc (commitment_hash : Sc_rollup.Commitment.Hash.t) = + let open Lwt_syntax in + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash + in + match commitment with + | None -> return acc + | Some commitment + when Raw_level.(commitment.inbox_level <= node_ctxt.lcc.level) -> + (* If we have moved backward passed or at the current LCC then we have + reached the end. *) + return acc + | Some commitment -> + let* earliest_cementing_level = + earliest_cementing_level node_ctxt commitment_hash + in + let acc = + match earliest_cementing_level with + | None -> acc + | Some earliest_cementing_level -> + if Raw_level.(earliest_cementing_level > head_level) then + (* Commitments whose cementing level are after the head's + successor won't be cementable in the next block. *) + acc + else commitment_hash :: acc + in + gather acc commitment.predecessor + in + (* We start our search from the last possible cementable commitment. This is + to avoid iterating over a large number of commitments + ([challenge_window_in_blocks / commitment_period_in_blocks], in the order + of 10^3 on mainnet). *) + let*& latest_cementable_commitment = + latest_cementable_commitment node_ctxt head + in + let*! cementable = gather [] latest_cementable_commitment in + match cementable with + | [] -> return_nil + | first_cementable :: _ -> + (* Make sure that the first commitment can be cemented according to the + Layer 1 node as a failsafe. *) + let* green_light = + Plugin.RPC.Sc_rollup.can_be_cemented + node_ctxt.cctxt + (node_ctxt.cctxt#chain, `Head 0) + node_ctxt.rollup_address + first_cementable + in + if green_light then return cementable else return_nil + + let cement_commitment (node_ctxt : _ Node_context.t) ~source commitment_hash = + let open Lwt_result_syntax in + let cement_operation = + Sc_rollup_cement + {rollup = node_ctxt.rollup_address; commitment = commitment_hash} + in + let* _hash = Injector.add_pending_operation ~source cement_operation in + return_unit - let cement_commitment (node_ctxt : _ Node_context.t) commitment_hash = + let cement_commitments node_ctxt = let open Lwt_result_syntax in let operator = Node_context.get_operator node_ctxt Cement in match operator with @@ -316,42 +369,10 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct (* Configured to not cement commitments *) return_unit | Some source -> - let cement_operation = - Sc_rollup_cement - {rollup = node_ctxt.rollup_address; commitment = commitment_hash} - in - let* _hash = Injector.add_pending_operation ~source cement_operation in - return_unit - - let cement_commitment_if_possible node_ctxt Layer1.{level = head_level; _} = - let open Lwt_result_syntax in - let next_level_to_cement = next_lcc_level node_ctxt in - let*! block = block_of_known_level node_ctxt next_level_to_cement in - match block with - | None | Some {header = {commitment_hash = None; _}; _} -> - (* Commitment not available *) - return_unit - | Some {header = {commitment_hash = Some commitment_hash; _}; _} -> ( - (* If `commitment_hash` is defined, the commitment to be cemented has - been stored but not necessarily published by the rollup node. *) - let*! earliest_cementing_level = - earliest_cementing_level node_ctxt commitment_hash - in - match earliest_cementing_level with - (* If `earliest_cementing_level` is well defined, then the rollup node - has previously published `commitment`, which means that the rollup - is indirectly staked on it. *) - | Some earliest_cementing_level -> - let* green_flag = - can_be_cemented - node_ctxt - earliest_cementing_level - (Raw_level.of_int32_exn head_level) - commitment_hash - in - if green_flag then cement_commitment node_ctxt commitment_hash - else return_unit - | None -> return_unit) + let* cementable_commitments = cementable_commitments node_ctxt in + List.iter_es + (cement_commitment node_ctxt ~source) + cementable_commitments let start () = Commitment_event.starting () end diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment_sig.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment_sig.ml index b8527a2833910a9558dd5fa3ff3f76c5a811bea1..443930ced36d271a55d8447799f37d45952a537c 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment_sig.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/commitment_sig.ml @@ -54,35 +54,16 @@ module type S = sig Context.rw -> Protocol.Alpha_context.Sc_rollup.Commitment.Hash.t option tzresult Lwt.t - (** [publish_commitment node_ctxt] publishes the earliest commitment - stored in [store] that has not been published yet, unless its inbox level - is below or equal to the inbox level of the last cemented commitment in - the layer1 chain. In this case, the rollup node checks whether it has - computed a commitment whose inbox level is - [sc_rollup_commitment_period] levels after the inbox level of the last - cemented commitment: - {ul - {li if the commitment is found and its predecessor hash coincides with - the hash of the LCC, the rollup node will try to publish that commitment - instead; } - {li if the commitment is found but its predecessor hash differs from the - hash of the LCC, the rollup node will stop its execution;} - {li if no commitment is found, no action is taken by the rollup node; - in particular, no commitment is published.} - } - *) - val publish_commitment : Node_context.rw -> unit tzresult Lwt.t + (** [publish_commitments node_ctxt] publishes the commitments that were not + yet published up to the finalized head and which are after the last + cemented commitment. *) + val publish_commitments : _ Node_context.t -> unit tzresult Lwt.t - (** [cement_commitment_if_possible node_ctxt head] checks whether the next - commitment to be cemented (i.e. whose inbox level is - [sc_rollup_commitment_period] levels after - [Store.Last_cemented_commitment_level store]) can be cemented. In - particular, the request to cement the commitment happens only if the - commitment is stored in [Store.Commitments store], and if - [sc_rollup_challenge_period] levels have passed since when the commitment - was originally published. *) - val cement_commitment_if_possible : - _ Node_context.t -> Layer1.head -> unit tzresult Lwt.t + (** [cement_commitments node_ctxt] cements the commitments that can be + cemented, i.e. the commitments that are after the current last cemented + commitment and which have [sc_rollup_challenge_period] levels on top of + them since they were originally published. *) + val cement_commitments : _ Node_context.t -> unit tzresult Lwt.t (** [start ()] only emits the event that the commitment manager for the rollup node has started. *) diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/daemon.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/daemon.ml index 236149238d40c6cacd006e21eac31c2ebab2fe66..6d4996a4cfd67f1071cdf57f731e19c2178d17f8 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/daemon.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/daemon.ml @@ -42,12 +42,12 @@ module Make (PVM : Pvm.S) = struct Sc_rollup_publish_result {published_at_level; _} ) when Node_context.is_operator node_ctxt source -> (* Published commitment --------------------------------------------- *) - let is_newest_lpc = + let save_lpc = match node_ctxt.lpc with | None -> true - | Some lpc -> Raw_level.(commitment.inbox_level > lpc.inbox_level) + | Some lpc -> Raw_level.(commitment.inbox_level >= lpc.inbox_level) in - if is_newest_lpc then node_ctxt.lpc <- Some commitment ; + if save_lpc then node_ctxt.lpc <- Some commitment ; let commitment_hash = Sc_rollup.Commitment.hash_uncarbonated commitment in @@ -55,12 +55,64 @@ module Make (PVM : Pvm.S) = struct Store.Commitments_published_at_level.add node_ctxt.store commitment_hash - published_at_level + { + first_published_at_level = published_at_level; + published_at_level = + Some (Raw_level.of_int32_exn head.Layer1.level); + } in return_unit + | ( Sc_rollup_publish {commitment; _}, + Sc_rollup_publish_result {published_at_level; _} ) -> + (* Commitment published by someone else *) + let commitment_hash = + Sc_rollup.Commitment.hash_uncarbonated commitment + in + let*! known_commitment = + Store.Commitments.mem node_ctxt.store commitment_hash + in + if not known_commitment then return_unit + else + let*! republication = + Store.Commitments_published_at_level.mem + node_ctxt.store + commitment_hash + in + if republication then return_unit + else + let*! () = + Store.Commitments_published_at_level.add + node_ctxt.store + commitment_hash + { + first_published_at_level = published_at_level; + published_at_level = None; + } + in + return_unit | Sc_rollup_cement {commitment; _}, Sc_rollup_cement_result {inbox_level; _} -> (* Cemented commitment ---------------------------------------------- *) + let* inbox_block_hash = + State.hash_of_level node_ctxt (Raw_level.to_int32 inbox_level) + in + let*! inbox_block = + Store.L2_blocks.get node_ctxt.store inbox_block_hash + in + let*? () = + (* We stop the node if we disagree with a cemented commitment *) + error_unless + (Option.equal + Sc_rollup.Commitment.Hash.( = ) + inbox_block.header.commitment_hash + (Some commitment)) + (Sc_rollup_node_errors.Disagree_with_cemented + { + inbox_level; + ours = inbox_block.header.commitment_hash; + on_l1 = commitment; + }) + in let*! () = if Raw_level.(inbox_level > node_ctxt.lcc.level) then ( node_ctxt.lcc <- {commitment; level = inbox_level} ; @@ -88,12 +140,11 @@ module Make (PVM : Pvm.S) = struct tzfail (Sc_rollup_node_errors.Lost_game (loser, reason, slashed_amount)) | Dal_publish_slot_header slot_header, Dal_publish_slot_header_result _ -> assert (Node_context.dal_enabled node_ctxt) ; - let {Dal.Slot.Header.header = {id = {index; _}; _}; _} = slot_header in let*! () = Store.Dal_slots_headers.add node_ctxt.store - ~primary_key:head - ~secondary_key:index + ~primary_key:head.Layer1.hash + ~secondary_key:slot_header.header.id.index slot_header.header in return_unit @@ -147,14 +198,15 @@ module Make (PVM : Pvm.S) = struct (* No action for non successful operations *) return_unit - let process_l1_block_operations ~finalized node_ctxt Layer1.{hash; _} = + let process_l1_block_operations ~finalized node_ctxt + (Layer1.{hash; _} as head) = let open Lwt_result_syntax in let* block = Layer1.fetch_tezos_block node_ctxt.Node_context.l1_ctxt hash in let apply (type kind) accu ~source (operation : kind manager_operation) result = let open Lwt_result_syntax in let* () = accu in - process_l1_operation ~finalized node_ctxt hash ~source operation result + process_l1_operation ~finalized node_ctxt head ~source operation result in let apply_internal (type kind) accu ~source:_ (_operation : kind Apply_internal_results.internal_operation) @@ -179,7 +231,7 @@ module Make (PVM : Pvm.S) = struct let*! last_finalized = State.get_finalized_head_opt node_ctxt.store in let already_finalized = match last_finalized with - | Some Layer1.{level = finalized_level; _} -> level <= finalized_level + | Some finalized -> level <= Raw_level.to_int32 finalized.header.level | None -> false in unless (already_finalized || before_origination node_ctxt block) @@ -190,7 +242,7 @@ module Make (PVM : Pvm.S) = struct in let*! () = Daemon_event.head_processing hash level ~finalized:true in let* () = process_l1_block_operations ~finalized:true node_ctxt block in - let*! () = State.mark_finalized_head node_ctxt.store block in + let*! () = State.mark_finalized_head node_ctxt.store hash in return_unit let process_head (node_ctxt : _ Node_context.t) Layer1.({hash; level} as head) @@ -259,12 +311,6 @@ module Make (PVM : Pvm.S) = struct in let* () = processed_finalized_block node_ctxt finalized_block in let*! () = State.save_l2_block node_ctxt.store l2_block in - (* Publishing a commitment when one is available does not depend on the - state of the current head. *) - let* () = Components.Commitment.publish_commitment node_ctxt in - let* () = - Components.Commitment.cement_commitment_if_possible node_ctxt head - in let*! () = Daemon_event.new_head_processed hash (Raw_level.to_int32 level) in @@ -331,6 +377,8 @@ module Make (PVM : Pvm.S) = struct in let*! () = Daemon_event.processing_heads_iteration reorg.new_chain in let* () = List.iter_es (process_head node_ctxt) reorg.new_chain in + let* () = Components.Commitment.publish_commitments node_ctxt in + let* () = Components.Commitment.cement_commitments node_ctxt in let* () = notify_injector node_ctxt new_head reorg in let*! () = Daemon_event.new_heads_processed reorg.new_chain in let* () = Components.Refutation_game.process head node_ctxt in diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/sc_rollup_node_errors.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/sc_rollup_node_errors.ml index e4fec530e84dbc7269a647a56ea5c3da94e5c385..32e4b4c31d19b11237a8becf5e0159ef491f80d5 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/sc_rollup_node_errors.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/sc_rollup_node_errors.ml @@ -31,7 +31,11 @@ type error += | Cannot_produce_proof of Sc_rollup.Inbox.t * Raw_level.t | Missing_mode_operators of {mode : string; missing_operators : string list} | Bad_minimal_fees of string - | Commitment_predecessor_should_be_LCC of Sc_rollup.Commitment.t + | Disagree_with_cemented of { + inbox_level : Raw_level.t; + ours : Sc_rollup.Commitment.Hash.t option; + on_l1 : Sc_rollup.Commitment.Hash.t; + } | Unreliable_tezos_node_returning_inconsistent_game | Inconsistent_inbox of { layer1_inbox : Sc_rollup.Inbox.t; @@ -59,22 +63,34 @@ let () = register_error_kind `Permanent - ~id:"internal.commitment_should_be_next_to_lcc" - ~title: - "Internal error: The next commitment should have the LCC as predecessor" + ~id:"internal.node_disagrees_with_cemented" + ~title:"Internal error: The node disagrees with a cemented commitment on L1" ~description: - "Internal error: The next commitment should have the LCC as predecessor" - ~pp:(fun ppf commitment -> + "Internal error: The node disagrees with a cemented commitment on L1" + ~pp:(fun ppf (inbox_level, ours, on_l1) -> Format.fprintf ppf - "invalid commitment '%a'" - Sc_rollup.Commitment.pp - commitment) - Data_encoding.(obj1 (req "commitment" Sc_rollup.Commitment.encoding)) + "Internal error: The node has commitment %a for inbox level %a but \ + this level is cemented on L1 with commitment %a" + (Format.pp_print_option + ~none:(fun ppf () -> Format.pp_print_string ppf "[None]") + Sc_rollup.Commitment.Hash.pp) + ours + Raw_level.pp + inbox_level + Sc_rollup.Commitment.Hash.pp + on_l1) + Data_encoding.( + obj3 + (req "inbox_level" Raw_level.encoding) + (req "ours" (option Sc_rollup.Commitment.Hash.encoding)) + (req "on_l1" Sc_rollup.Commitment.Hash.encoding)) (function - | Commitment_predecessor_should_be_LCC commitment -> Some commitment + | Disagree_with_cemented {inbox_level; ours; on_l1} -> + Some (inbox_level, ours, on_l1) | _ -> None) - (fun commitment -> Commitment_predecessor_should_be_LCC commitment) ; + (fun (inbox_level, ours, on_l1) -> + Disagree_with_cemented {inbox_level; ours; on_l1}) ; register_error_kind `Permanent diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/state.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/state.ml index 9c71b60c501714b678da0663d7aa6ed4a2b13322..dcd13e4db0ac97db361b0594860dc33bde3a024e 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/state.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/state.ml @@ -47,14 +47,14 @@ module Store = struct module Last_finalized_head = Store.Make_mutable_value (struct - let path = ["tezos"; "finalized_head"] + let path = ["finalized_head"] end) (struct - type value = Layer1.head + type value = Sc_rollup_block.t - let name = "head" + let name = "l2_block" - let encoding = Layer1.head_encoding + let encoding = Sc_rollup_block.encoding end) (** Table from L1 levels to blocks hashes. *) @@ -120,7 +120,12 @@ let is_processed store head = Raw_store.L2_blocks.mem store head let last_processed_head_opt store = Store.L2_head.find store -let mark_finalized_head store head = Store.Last_finalized_head.set store head +let mark_finalized_head store head_hash = + let open Lwt_syntax in + let* block = Raw_store.L2_blocks.find store head_hash in + match block with + | None -> return_unit + | Some block -> Store.Last_finalized_head.set store block let get_finalized_head_opt store = Store.Last_finalized_head.find store diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/state.mli b/src/proto_016_PtMumbai/bin_sc_rollup_node/state.mli index a5c7335a19bd270b14d1934065ca6013bac6e96b..c67158f14744b84f3860cab56e34f56fe557e482 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/state.mli +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/state.mli @@ -40,10 +40,10 @@ val last_processed_head_opt : _ Store.t -> Sc_rollup_block.t option Lwt.t (** [mark_finalized_head store head] remembers that the [head] is finalized. By construction, every block whose level is smaller than [head]'s is also finalized. *) -val mark_finalized_head : Store.rw -> Layer1.head -> unit Lwt.t +val mark_finalized_head : Store.rw -> Block_hash.t -> unit Lwt.t (** [last_finalized_head_opt store] returns the last finalized head if it exists. *) -val get_finalized_head_opt : _ Store.t -> Layer1.head option Lwt.t +val get_finalized_head_opt : _ Store.t -> Sc_rollup_block.t option Lwt.t (** [hash_of_level node_ctxt level] returns the current block hash for a given [level]. *) diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/store.ml b/src/proto_016_PtMumbai/bin_sc_rollup_node/store.ml index 2c7b9924c0fb88c097fb495fef5aa60680dabc40..84de5a5e8635e812968da360482f80b58f222714 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/store.ml +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/store.ml @@ -163,23 +163,41 @@ module Last_stored_commitment_level = let encoding = Raw_level.encoding end) -module Commitments_published_at_level = - Make_updatable_map - (struct - let path = ["commitments"; "published_at_level"] - end) - (struct - type key = Sc_rollup.Commitment.Hash.t +module Commitments_published_at_level = struct + type element = { + first_published_at_level : Raw_level.t; + published_at_level : Raw_level.t option; + } - let to_path_representation = Sc_rollup.Commitment.Hash.to_b58check - end) - (struct - type value = Raw_level.t + let element_encoding = + let open Data_encoding in + conv + (fun {first_published_at_level; published_at_level} -> + (first_published_at_level, published_at_level)) + (fun (first_published_at_level, published_at_level) -> + {first_published_at_level; published_at_level}) + @@ obj2 + (req "first_published_at_level" Raw_level.encoding) + (opt "published_at_level" Raw_level.encoding) - let name = "raw_level" + include + Make_updatable_map + (struct + let path = ["commitments"; "published_at_level"] + end) + (struct + type key = Sc_rollup.Commitment.Hash.t - let encoding = Raw_level.encoding - end) + let to_path_representation = Sc_rollup.Commitment.Hash.to_b58check + end) + (struct + type value = element + + let name = "published_levels" + + let encoding = element_encoding + end) +end (* Published slot headers per block hash, stored as a list of bindings from `Dal_slot_index.t` diff --git a/src/proto_016_PtMumbai/bin_sc_rollup_node/store.mli b/src/proto_016_PtMumbai/bin_sc_rollup_node/store.mli index 05766389c95a9c899d88151ab37cab50fe08e1b3..51af89bcf21cdaf14f9162003d1a02dd247ba378 100644 --- a/src/proto_016_PtMumbai/bin_sc_rollup_node/store.mli +++ b/src/proto_016_PtMumbai/bin_sc_rollup_node/store.mli @@ -102,11 +102,22 @@ module Last_stored_commitment_level : (** Storage mapping commitment hashes to the level when they were published by the rollup node. It only contains hashes of commitments published by this rollup node. *) -module Commitments_published_at_level : - Store_sigs.Map - with type key := Sc_rollup.Commitment.Hash.t - and type value := Raw_level.t - and type 'a store := 'a store +module Commitments_published_at_level : sig + type element = { + first_published_at_level : Raw_level.t; + (** The level at which this commitment was first published. *) + published_at_level : Raw_level.t option; + (** The level at which we published this commitment. If + [first_published_at_level <> published_at_level] it means that the + commitment is republished. *) + } + + include + Store_sigs.Map + with type key := Sc_rollup.Commitment.Hash.t + and type value := element + and type 'a store := 'a store +end (** Published slot headers per block hash, stored as a list of bindings from [Dal_slot_index.t] diff --git a/src/proto_016_PtMumbai/lib_sc_rollup/sc_rollup_services.ml b/src/proto_016_PtMumbai/lib_sc_rollup/sc_rollup_services.ml index b96c04e1f558d1d887d394924a19809b00161c66..609d325ff3dca18423b915e6ffdca54c9929a762 100644 --- a/src/proto_016_PtMumbai/lib_sc_rollup/sc_rollup_services.ml +++ b/src/proto_016_PtMumbai/lib_sc_rollup/sc_rollup_services.ml @@ -66,7 +66,8 @@ type inbox_info = {finalized : bool; cemented : bool} type commitment_info = { commitment : Sc_rollup.Commitment.t; commitment_hash : Sc_rollup.Commitment.Hash.t; - published_at : Raw_level.t; + first_published_at_level : Raw_level.t; + published_at_level : Raw_level.t; } type message_status = @@ -80,10 +81,16 @@ type message_status = module Encodings = struct open Data_encoding - let commitment_with_hash_and_level = - obj3 + let commitment_with_hash = + obj2 (req "commitment" Sc_rollup.Commitment.encoding) (req "hash" Sc_rollup.Commitment.Hash.encoding) + + let commitment_with_hash_and_level_infos = + obj4 + (req "commitment" Sc_rollup.Commitment.encoding) + (req "hash" Sc_rollup.Commitment.Hash.encoding) + (opt "first_published_at_level" Raw_level.encoding) (opt "published_at_level" Raw_level.encoding) let hex_string = conv Bytes.of_string Bytes.to_string bytes @@ -144,15 +151,31 @@ module Encodings = struct let commitment_info = conv - (fun {commitment; commitment_hash; published_at} -> - (commitment, (commitment_hash, published_at))) - (fun (commitment, (commitment_hash, published_at)) -> - {commitment; commitment_hash; published_at}) - @@ merge_objs - Sc_rollup.Commitment.encoding - (obj2 - (req "hash" Sc_rollup.Commitment.Hash.encoding) - (req "published_at" Raw_level.encoding)) + (fun { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + } -> + ( commitment, + commitment_hash, + first_published_at_level, + published_at_level )) + (fun ( commitment, + commitment_hash, + first_published_at_level, + published_at_level ) -> + { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + }) + @@ obj4 + (req "commitment" Sc_rollup.Commitment.encoding) + (req "hash" Sc_rollup.Commitment.Hash.encoding) + (req "first_published_at_level" Raw_level.encoding) + (req "published_at_level" Raw_level.encoding) let message_status = union @@ -327,7 +350,7 @@ module Global = struct Tezos_rpc.Service.get_service ~description:"Last commitment computed by the node" ~query:Tezos_rpc.Query.empty - ~output:(Data_encoding.option Encodings.commitment_with_hash_and_level) + ~output:(Data_encoding.option Encodings.commitment_with_hash) (path / "last_stored_commitment") module Helpers = struct @@ -628,7 +651,8 @@ module Local = struct Tezos_rpc.Service.get_service ~description:"Last commitment published by the node" ~query:Tezos_rpc.Query.empty - ~output:(Data_encoding.option Encodings.commitment_with_hash_and_level) + ~output: + (Data_encoding.option Encodings.commitment_with_hash_and_level_infos) (path / "last_published_commitment") let injection = diff --git a/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml b/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml index ca55fb24a41b8500354054b1dca712103bfa90f6..2c12e8358aba2802295b066cb5bfc6ecfc602925 100644 --- a/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml +++ b/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml @@ -39,7 +39,7 @@ let get_finalized store = let*! head = State.get_finalized_head_opt store in match head with | None -> failwith "No finalized head" - | Some {hash; _} -> return hash + | Some {header = {block_hash; _}; _} -> return block_hash let get_last_cemented (node_ctxt : _ Node_context.t) = let open Lwt_result_syntax in @@ -376,10 +376,10 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct let commitment_hash = Sc_rollup_block.most_recent_commitment head.header in - let*! commitment = - Store.Commitments.get node_ctxt.store commitment_hash + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash in - return (commitment, commitment_hash, None) + return (commitment, commitment_hash) in return res @@ -396,10 +396,16 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct (* The corresponding level in Store.Commitments.published_at_level is available only when the commitment has been published and included in a block. *) - let*! published_at_level = + let*! published_at_level_info = Store.Commitments_published_at_level.find node_ctxt.store hash in - return (commitment, hash, published_at_level) + let first_published, published = + match published_at_level_info with + | None -> (None, None) + | Some {first_published_at_level; published_at_level} -> + (Some first_published_at_level, published_at_level) + in + return (commitment, hash, first_published, published) in return result @@ -496,8 +502,8 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct let finalized = match finalized_head with | None -> false - | Some {level = finalized_level; _} -> - Compare.Int32.(inbox_level <= finalized_level) + | Some {header = {level = finalized_level; _}; _} -> + Compare.Int32.(inbox_level <= Raw_level.to_int32 finalized_level) in let cemented = Compare.Int32.(inbox_level <= Raw_level.to_int32 node_ctxt.lcc.level) @@ -560,11 +566,15 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct commitment_hash in match published_at with - | None -> + | None | Some {published_at_level = None; _} -> (* Commitment not published yet *) return (Sc_rollup_services.Included (info, inbox_info)) - | Some published_at -> + | Some + { + first_published_at_level; + published_at_level = Some published_at_level; + } -> (* Commitment published *) let*! commitment = Store.Commitments.get @@ -573,7 +583,12 @@ module Make (Simulation : Simulation.S) (Batcher : Batcher.S) = struct in let commitment_info = Sc_rollup_services. - {commitment; commitment_hash; published_at} + { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + } in return (Sc_rollup_services.Committed diff --git a/src/proto_alpha/bin_sc_rollup_node/commitment.ml b/src/proto_alpha/bin_sc_rollup_node/commitment.ml index b1a5c83952f212f13e1a43a9b1d03cf7d448d225..82ab504670bb7955eae9121bd9ff0e7d080ce9a8 100644 --- a/src/proto_alpha/bin_sc_rollup_node/commitment.ml +++ b/src/proto_alpha/bin_sc_rollup_node/commitment.ml @@ -49,6 +49,11 @@ let add_level level increment = if increment < 0 then invalid_arg "Commitment.add_level negative increment" ; Raw_level.Internal_for_tests.add level increment +let sub_level level decrement = + (* We only use this function with positive increments so it is safe *) + if decrement < 0 then invalid_arg "Commitment.sub_level negative decrement" ; + Raw_level.Internal_for_tests.sub level decrement + let sc_rollup_commitment_period node_ctxt = node_ctxt.Node_context.protocol_constants.parametric.sc_rollup .commitment_period_in_blocks @@ -57,29 +62,6 @@ let sc_rollup_challenge_window node_ctxt = node_ctxt.Node_context.protocol_constants.parametric.sc_rollup .challenge_window_in_blocks -let next_lcc_level node_ctxt = - add_level - node_ctxt.Node_context.lcc.level - (sc_rollup_commitment_period node_ctxt) - -(** Returns the next level for which a commitment can be published, i.e. the - level that is [commitment_period] blocks after the last published one. It - returns [None] if this level is not finalized because we only publish - commitments for inbox of finalized L1 blocks. *) -let next_publishable_level node_ctxt = - let open Lwt_option_syntax in - let lpc_level = - match node_ctxt.Node_context.lpc with - | None -> node_ctxt.genesis_info.level - | Some lpc -> lpc.inbox_level - in - let next_level = - add_level lpc_level (sc_rollup_commitment_period node_ctxt) - in - let* finalized_level = State.get_finalized_head_opt node_ctxt.store in - if Raw_level.(of_int32_exn finalized_level.level < next_level) then fail - else return next_level - let next_commitment_level node_ctxt last_commitment_level = add_level last_commitment_level (sc_rollup_commitment_period node_ctxt) @@ -189,126 +171,197 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct in return_some commitment_hash - let block_of_known_level (node_ctxt : _ Node_context.t) level = - let open Lwt_option_syntax in + let missing_commitments (node_ctxt : _ Node_context.t) = + let open Lwt_syntax in + let lpc_level = + match node_ctxt.lpc with + | None -> node_ctxt.genesis_info.level + | Some lpc -> lpc.inbox_level + in let* head = State.last_processed_head_opt node_ctxt.store in - if Raw_level.(head.header.level < level) then - (* Level is not known yet *) - fail - else - let*! block_hash = - State.hash_of_level node_ctxt (Raw_level.to_int32 level) + let next_head_level = + Option.map + (fun (b : Sc_rollup_block.t) -> Raw_level.succ b.header.level) + head + in + let sc_rollup_challenge_window_int32 = + sc_rollup_challenge_window node_ctxt |> Int32.of_int + in + let rec gather acc (commitment_hash : Sc_rollup.Commitment.Hash.t) = + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash in - let*? block_hash = Result.to_option block_hash in - Store.L2_blocks.find node_ctxt.store block_hash - - let get_commitment_and_publish ~check_lcc_hash node_ctxt next_level_to_publish - = - let open Lwt_result_syntax in - let*! commitment = - let open Lwt_option_syntax in - let* block = block_of_known_level node_ctxt next_level_to_publish in - let*? commitment_hash = block.header.commitment_hash in - Store.Commitments.find node_ctxt.store commitment_hash + match commitment with + | None -> return acc + | Some commitment + when Raw_level.(commitment.inbox_level <= node_ctxt.lcc.level) -> + (* Commitment is before or at the LCC, we have reached the end. *) + return acc + | Some commitment when Raw_level.(commitment.inbox_level <= lpc_level) -> + (* Commitment is before the last published one, we have also reached + the end because we only publish commitments that are for the inbox + of a finalized L1 block. *) + return acc + | Some commitment -> + let* published_info = + Store.Commitments_published_at_level.find + node_ctxt.store + commitment_hash + in + let past_curfew = + match (published_info, next_head_level) with + | None, _ | _, None -> false + | Some {first_published_at_level; _}, Some next_head_level -> + Raw_level.diff next_head_level first_published_at_level + > sc_rollup_challenge_window_int32 + in + let acc = if past_curfew then acc else commitment :: acc in + (* We keep the commitment and go back to the previous one. *) + gather acc commitment.predecessor in - match commitment with - | None -> - (* Commitment not available *) - return_unit - | Some commitment -> ( - let* () = - if check_lcc_hash then - let open Lwt_result_syntax in - if - Sc_rollup.Commitment.Hash.equal - node_ctxt.lcc.commitment - commitment.predecessor - then return () - else - let*! () = - Commitment_event.commitment_parent_is_not_lcc - commitment.inbox_level - commitment.predecessor - node_ctxt.lcc.commitment - in - tzfail - (Sc_rollup_node_errors.Commitment_predecessor_should_be_LCC - commitment) - else return_unit + let* finalized_block = State.get_finalized_head_opt node_ctxt.store in + match finalized_block with + | None -> return_nil + | Some finalized -> + (* Start from finalized block's most recent commitment and gather all + commitments that are missing. *) + let commitment = + Sc_rollup_block.most_recent_commitment finalized.header in - let operator = Node_context.get_operator node_ctxt Publish in - match operator with - | None -> - (* Configured to not publish commitments *) - return_unit - | Some source -> - let publish_operation = - Sc_rollup_publish {rollup = node_ctxt.rollup_address; commitment} - in - let* _hash = - Injector.add_pending_operation ~source publish_operation - in - (* TODO: https://gitlab.com/tezos/tezos/-/issues/3462 - Decouple commitments from head processing - - Move the following, in a part where we know the operation is - included. *) - node_ctxt.lpc <- Some commitment ; - return_unit) + gather [] commitment - let publish_commitment node_ctxt = + let publish_commitment (node_ctxt : _ Node_context.t) ~source + (commitment : Sc_rollup.Commitment.t) = let open Lwt_result_syntax in - (* Check level of next publishable commitment and avoid publishing if it is - on or before the last cemented commitment. - *) - let next_lcc_level = next_lcc_level node_ctxt in - let*! next_publishable_level = next_publishable_level node_ctxt in - match next_publishable_level with - | None -> return_unit - | Some next_publishable_level -> - let check_lcc_hash, level_to_publish = - if Raw_level.(next_publishable_level < next_lcc_level) then - (* - - This situation can happen if the rollup node has been - shutdown and the rollup has been progressing in the - meantime. In that case, the rollup node must wait to reach - [lcc_level + commitment_frequency] to publish the - commitment. ([lcc_level] is a multiple of - commitment_frequency.) - - We need to check that the published commitment comes - immediately after the last cemented commitment, otherwise - that's an invalid commitment. + let publish_operation = + Sc_rollup_publish {rollup = node_ctxt.rollup_address; commitment} + in + let* _hash = Injector.add_pending_operation ~source publish_operation in + return_unit - *) - (true, next_lcc_level) - else (false, next_publishable_level) - in - get_commitment_and_publish node_ctxt level_to_publish ~check_lcc_hash + let publish_commitments (node_ctxt : _ Node_context.t) = + let open Lwt_result_syntax in + let operator = Node_context.get_operator node_ctxt Publish in + match operator with + | None -> + (* Configured to not publish commitments *) + return_unit + | Some source -> + let*! commitments = missing_commitments node_ctxt in + List.iter_es (publish_commitment node_ctxt ~source) commitments + (* Commitments can only be cemented after [sc_rollup_challenge_window] has + passed since they were first published. *) let earliest_cementing_level node_ctxt commitment_hash = let open Lwt_option_syntax in - let+ published_at_level = + let+ {first_published_at_level; _} = Store.Commitments_published_at_level.find node_ctxt.Node_context.store commitment_hash in - add_level published_at_level (sc_rollup_challenge_window node_ctxt) + add_level first_published_at_level (sc_rollup_challenge_window node_ctxt) - let can_be_cemented node_ctxt earliest_cementing_level head_level - commitment_hash = - let {Node_context.cctxt; rollup_address; _} = node_ctxt in + (** [latest_cementable_commitment node_ctxt head] is the most recent commitment + hash that could be cemented in [head]'s successor if: + + - all its predecessors were cemented + - it would have been first published at the same level as its inbox + + It does not need to be exact but it must be an upper bound on which we can + start the search for cementable commitments. *) + let latest_cementable_commitment (node_ctxt : _ Node_context.t) + (head : Sc_rollup_block.t) = + let open Lwt_option_syntax in + let commitment_hash = Sc_rollup_block.most_recent_commitment head.header in + let* commitment = Store.Commitments.find node_ctxt.store commitment_hash in + let*? cementable_level_bound = + sub_level commitment.inbox_level (sc_rollup_challenge_window node_ctxt) + in + if Raw_level.(cementable_level_bound <= node_ctxt.lcc.level) then fail + else + let* cementable_bound_block_hash = + State.hash_of_level_opt + node_ctxt + (Raw_level.to_int32 cementable_level_bound) + in + let* cementable_bound_block = + Store.L2_blocks.find node_ctxt.store cementable_bound_block_hash + in + let cementable_commitment = + Sc_rollup_block.most_recent_commitment cementable_bound_block.header + in + return cementable_commitment + + let cementable_commitments (node_ctxt : _ Node_context.t) = let open Lwt_result_syntax in - if earliest_cementing_level <= head_level then - Plugin.RPC.Sc_rollup.can_be_cemented - cctxt - (cctxt#chain, cctxt#block) - rollup_address - commitment_hash - else return_false + let ( let*& ) x f = + (* A small monadic combinator to return an empty list of cementable + commitments on None results. *) + let*! x = x in + match x with None -> return_nil | Some x -> f x + in + let*& head = State.last_processed_head_opt node_ctxt.Node_context.store in + let head_level = head.header.level in + let rec gather acc (commitment_hash : Sc_rollup.Commitment.Hash.t) = + let open Lwt_syntax in + let* commitment = + Store.Commitments.find node_ctxt.store commitment_hash + in + match commitment with + | None -> return acc + | Some commitment + when Raw_level.(commitment.inbox_level <= node_ctxt.lcc.level) -> + (* If we have moved backward passed or at the current LCC then we have + reached the end. *) + return acc + | Some commitment -> + let* earliest_cementing_level = + earliest_cementing_level node_ctxt commitment_hash + in + let acc = + match earliest_cementing_level with + | None -> acc + | Some earliest_cementing_level -> + if Raw_level.(earliest_cementing_level > head_level) then + (* Commitments whose cementing level are after the head's + successor won't be cementable in the next block. *) + acc + else commitment_hash :: acc + in + gather acc commitment.predecessor + in + (* We start our search from the last possible cementable commitment. This is + to avoid iterating over a large number of commitments + ([challenge_window_in_blocks / commitment_period_in_blocks], in the order + of 10^3 on mainnet). *) + let*& latest_cementable_commitment = + latest_cementable_commitment node_ctxt head + in + let*! cementable = gather [] latest_cementable_commitment in + match cementable with + | [] -> return_nil + | first_cementable :: _ -> + (* Make sure that the first commitment can be cemented according to the + Layer 1 node as a failsafe. *) + let* green_light = + Plugin.RPC.Sc_rollup.can_be_cemented + node_ctxt.cctxt + (node_ctxt.cctxt#chain, `Head 0) + node_ctxt.rollup_address + first_cementable + in + if green_light then return cementable else return_nil + + let cement_commitment (node_ctxt : _ Node_context.t) ~source commitment_hash = + let open Lwt_result_syntax in + let cement_operation = + Sc_rollup_cement + {rollup = node_ctxt.rollup_address; commitment = commitment_hash} + in + let* _hash = Injector.add_pending_operation ~source cement_operation in + return_unit - let cement_commitment (node_ctxt : _ Node_context.t) commitment_hash = + let cement_commitments node_ctxt = let open Lwt_result_syntax in let operator = Node_context.get_operator node_ctxt Cement in match operator with @@ -316,42 +369,10 @@ module Make (PVM : Pvm.S) : Commitment_sig.S with module PVM = PVM = struct (* Configured to not cement commitments *) return_unit | Some source -> - let cement_operation = - Sc_rollup_cement - {rollup = node_ctxt.rollup_address; commitment = commitment_hash} - in - let* _hash = Injector.add_pending_operation ~source cement_operation in - return_unit - - let cement_commitment_if_possible node_ctxt Layer1.{level = head_level; _} = - let open Lwt_result_syntax in - let next_level_to_cement = next_lcc_level node_ctxt in - let*! block = block_of_known_level node_ctxt next_level_to_cement in - match block with - | None | Some {header = {commitment_hash = None; _}; _} -> - (* Commitment not available *) - return_unit - | Some {header = {commitment_hash = Some commitment_hash; _}; _} -> ( - (* If `commitment_hash` is defined, the commitment to be cemented has - been stored but not necessarily published by the rollup node. *) - let*! earliest_cementing_level = - earliest_cementing_level node_ctxt commitment_hash - in - match earliest_cementing_level with - (* If `earliest_cementing_level` is well defined, then the rollup node - has previously published `commitment`, which means that the rollup - is indirectly staked on it. *) - | Some earliest_cementing_level -> - let* green_flag = - can_be_cemented - node_ctxt - earliest_cementing_level - (Raw_level.of_int32_exn head_level) - commitment_hash - in - if green_flag then cement_commitment node_ctxt commitment_hash - else return_unit - | None -> return_unit) + let* cementable_commitments = cementable_commitments node_ctxt in + List.iter_es + (cement_commitment node_ctxt ~source) + cementable_commitments let start () = Commitment_event.starting () end diff --git a/src/proto_alpha/bin_sc_rollup_node/commitment_sig.ml b/src/proto_alpha/bin_sc_rollup_node/commitment_sig.ml index b8527a2833910a9558dd5fa3ff3f76c5a811bea1..443930ced36d271a55d8447799f37d45952a537c 100644 --- a/src/proto_alpha/bin_sc_rollup_node/commitment_sig.ml +++ b/src/proto_alpha/bin_sc_rollup_node/commitment_sig.ml @@ -54,35 +54,16 @@ module type S = sig Context.rw -> Protocol.Alpha_context.Sc_rollup.Commitment.Hash.t option tzresult Lwt.t - (** [publish_commitment node_ctxt] publishes the earliest commitment - stored in [store] that has not been published yet, unless its inbox level - is below or equal to the inbox level of the last cemented commitment in - the layer1 chain. In this case, the rollup node checks whether it has - computed a commitment whose inbox level is - [sc_rollup_commitment_period] levels after the inbox level of the last - cemented commitment: - {ul - {li if the commitment is found and its predecessor hash coincides with - the hash of the LCC, the rollup node will try to publish that commitment - instead; } - {li if the commitment is found but its predecessor hash differs from the - hash of the LCC, the rollup node will stop its execution;} - {li if no commitment is found, no action is taken by the rollup node; - in particular, no commitment is published.} - } - *) - val publish_commitment : Node_context.rw -> unit tzresult Lwt.t + (** [publish_commitments node_ctxt] publishes the commitments that were not + yet published up to the finalized head and which are after the last + cemented commitment. *) + val publish_commitments : _ Node_context.t -> unit tzresult Lwt.t - (** [cement_commitment_if_possible node_ctxt head] checks whether the next - commitment to be cemented (i.e. whose inbox level is - [sc_rollup_commitment_period] levels after - [Store.Last_cemented_commitment_level store]) can be cemented. In - particular, the request to cement the commitment happens only if the - commitment is stored in [Store.Commitments store], and if - [sc_rollup_challenge_period] levels have passed since when the commitment - was originally published. *) - val cement_commitment_if_possible : - _ Node_context.t -> Layer1.head -> unit tzresult Lwt.t + (** [cement_commitments node_ctxt] cements the commitments that can be + cemented, i.e. the commitments that are after the current last cemented + commitment and which have [sc_rollup_challenge_period] levels on top of + them since they were originally published. *) + val cement_commitments : _ Node_context.t -> unit tzresult Lwt.t (** [start ()] only emits the event that the commitment manager for the rollup node has started. *) diff --git a/src/proto_alpha/bin_sc_rollup_node/daemon.ml b/src/proto_alpha/bin_sc_rollup_node/daemon.ml index 9cb48ef7bb8fd2fefabcbf20921fe71bd9210144..e0b99967f67f4a52e069775544df76a733978e68 100644 --- a/src/proto_alpha/bin_sc_rollup_node/daemon.ml +++ b/src/proto_alpha/bin_sc_rollup_node/daemon.ml @@ -42,12 +42,12 @@ module Make (PVM : Pvm.S) = struct Sc_rollup_publish_result {published_at_level; _} ) when Node_context.is_operator node_ctxt source -> (* Published commitment --------------------------------------------- *) - let is_newest_lpc = + let save_lpc = match node_ctxt.lpc with | None -> true - | Some lpc -> Raw_level.(commitment.inbox_level > lpc.inbox_level) + | Some lpc -> Raw_level.(commitment.inbox_level >= lpc.inbox_level) in - if is_newest_lpc then node_ctxt.lpc <- Some commitment ; + if save_lpc then node_ctxt.lpc <- Some commitment ; let commitment_hash = Sc_rollup.Commitment.hash_uncarbonated commitment in @@ -55,12 +55,64 @@ module Make (PVM : Pvm.S) = struct Store.Commitments_published_at_level.add node_ctxt.store commitment_hash - published_at_level + { + first_published_at_level = published_at_level; + published_at_level = + Some (Raw_level.of_int32_exn head.Layer1.level); + } in return_unit + | ( Sc_rollup_publish {commitment; _}, + Sc_rollup_publish_result {published_at_level; _} ) -> + (* Commitment published by someone else *) + let commitment_hash = + Sc_rollup.Commitment.hash_uncarbonated commitment + in + let*! known_commitment = + Store.Commitments.mem node_ctxt.store commitment_hash + in + if not known_commitment then return_unit + else + let*! republication = + Store.Commitments_published_at_level.mem + node_ctxt.store + commitment_hash + in + if republication then return_unit + else + let*! () = + Store.Commitments_published_at_level.add + node_ctxt.store + commitment_hash + { + first_published_at_level = published_at_level; + published_at_level = None; + } + in + return_unit | Sc_rollup_cement {commitment; _}, Sc_rollup_cement_result {inbox_level; _} -> (* Cemented commitment ---------------------------------------------- *) + let* inbox_block_hash = + State.hash_of_level node_ctxt (Raw_level.to_int32 inbox_level) + in + let*! inbox_block = + Store.L2_blocks.get node_ctxt.store inbox_block_hash + in + let*? () = + (* We stop the node if we disagree with a cemented commitment *) + error_unless + (Option.equal + Sc_rollup.Commitment.Hash.( = ) + inbox_block.header.commitment_hash + (Some commitment)) + (Sc_rollup_node_errors.Disagree_with_cemented + { + inbox_level; + ours = inbox_block.header.commitment_hash; + on_l1 = commitment; + }) + in let*! () = if Raw_level.(inbox_level > node_ctxt.lcc.level) then ( node_ctxt.lcc <- {commitment; level = inbox_level} ; @@ -92,7 +144,7 @@ module Make (PVM : Pvm.S) = struct let*! () = Store.Dal_slots_headers.add node_ctxt.store - ~primary_key:head + ~primary_key:head.Layer1.hash ~secondary_key:slot_header.id.index slot_header in @@ -147,14 +199,15 @@ module Make (PVM : Pvm.S) = struct (* No action for non successful operations *) return_unit - let process_l1_block_operations ~finalized node_ctxt Layer1.{hash; _} = + let process_l1_block_operations ~finalized node_ctxt + (Layer1.{hash; _} as head) = let open Lwt_result_syntax in let* block = Layer1.fetch_tezos_block node_ctxt.Node_context.l1_ctxt hash in let apply (type kind) accu ~source (operation : kind manager_operation) result = let open Lwt_result_syntax in let* () = accu in - process_l1_operation ~finalized node_ctxt hash ~source operation result + process_l1_operation ~finalized node_ctxt head ~source operation result in let apply_internal (type kind) accu ~source:_ (_operation : kind Apply_internal_results.internal_operation) @@ -179,7 +232,7 @@ module Make (PVM : Pvm.S) = struct let*! last_finalized = State.get_finalized_head_opt node_ctxt.store in let already_finalized = match last_finalized with - | Some Layer1.{level = finalized_level; _} -> level <= finalized_level + | Some finalized -> level <= Raw_level.to_int32 finalized.header.level | None -> false in unless (already_finalized || before_origination node_ctxt block) @@ -190,7 +243,7 @@ module Make (PVM : Pvm.S) = struct in let*! () = Daemon_event.head_processing hash level ~finalized:true in let* () = process_l1_block_operations ~finalized:true node_ctxt block in - let*! () = State.mark_finalized_head node_ctxt.store block in + let*! () = State.mark_finalized_head node_ctxt.store hash in return_unit let process_head (node_ctxt : _ Node_context.t) Layer1.({hash; level} as head) @@ -259,12 +312,6 @@ module Make (PVM : Pvm.S) = struct in let* () = processed_finalized_block node_ctxt finalized_block in let*! () = State.save_l2_block node_ctxt.store l2_block in - (* Publishing a commitment when one is available does not depend on the - state of the current head. *) - let* () = Components.Commitment.publish_commitment node_ctxt in - let* () = - Components.Commitment.cement_commitment_if_possible node_ctxt head - in let*! () = Daemon_event.new_head_processed hash (Raw_level.to_int32 level) in @@ -331,6 +378,8 @@ module Make (PVM : Pvm.S) = struct in let*! () = Daemon_event.processing_heads_iteration reorg.new_chain in let* () = List.iter_es (process_head node_ctxt) reorg.new_chain in + let* () = Components.Commitment.publish_commitments node_ctxt in + let* () = Components.Commitment.cement_commitments node_ctxt in let* () = notify_injector node_ctxt new_head reorg in let*! () = Daemon_event.new_heads_processed reorg.new_chain in let* () = Components.Refutation_game.process head node_ctxt in diff --git a/src/proto_alpha/bin_sc_rollup_node/sc_rollup_node_errors.ml b/src/proto_alpha/bin_sc_rollup_node/sc_rollup_node_errors.ml index 6a02cd2565d3de90ec7260d57950ca888594c841..bb38f41cd862abbc5c71c63f61c616275eb91f8d 100644 --- a/src/proto_alpha/bin_sc_rollup_node/sc_rollup_node_errors.ml +++ b/src/proto_alpha/bin_sc_rollup_node/sc_rollup_node_errors.ml @@ -31,7 +31,11 @@ type error += | Cannot_produce_proof of Sc_rollup.Inbox.t * Raw_level.t | Missing_mode_operators of {mode : string; missing_operators : string list} | Bad_minimal_fees of string - | Commitment_predecessor_should_be_LCC of Sc_rollup.Commitment.t + | Disagree_with_cemented of { + inbox_level : Raw_level.t; + ours : Sc_rollup.Commitment.Hash.t option; + on_l1 : Sc_rollup.Commitment.Hash.t; + } | Unreliable_tezos_node_returning_inconsistent_game | Inconsistent_inbox of { layer1_inbox : Sc_rollup.Inbox.t; @@ -59,22 +63,34 @@ let () = register_error_kind `Permanent - ~id:"internal.commitment_should_be_next_to_lcc" - ~title: - "Internal error: The next commitment should have the LCC as predecessor" + ~id:"internal.node_disagrees_with_cemented" + ~title:"Internal error: The node disagrees with a cemented commitment on L1" ~description: - "Internal error: The next commitment should have the LCC as predecessor" - ~pp:(fun ppf commitment -> + "Internal error: The node disagrees with a cemented commitment on L1" + ~pp:(fun ppf (inbox_level, ours, on_l1) -> Format.fprintf ppf - "invalid commitment '%a'" - Sc_rollup.Commitment.pp - commitment) - Data_encoding.(obj1 (req "commitment" Sc_rollup.Commitment.encoding)) + "Internal error: The node has commitment %a for inbox level %a but \ + this level is cemented on L1 with commitment %a" + (Format.pp_print_option + ~none:(fun ppf () -> Format.pp_print_string ppf "[None]") + Sc_rollup.Commitment.Hash.pp) + ours + Raw_level.pp + inbox_level + Sc_rollup.Commitment.Hash.pp + on_l1) + Data_encoding.( + obj3 + (req "inbox_level" Raw_level.encoding) + (req "ours" (option Sc_rollup.Commitment.Hash.encoding)) + (req "on_l1" Sc_rollup.Commitment.Hash.encoding)) (function - | Commitment_predecessor_should_be_LCC commitment -> Some commitment + | Disagree_with_cemented {inbox_level; ours; on_l1} -> + Some (inbox_level, ours, on_l1) | _ -> None) - (fun commitment -> Commitment_predecessor_should_be_LCC commitment) ; + (fun (inbox_level, ours, on_l1) -> + Disagree_with_cemented {inbox_level; ours; on_l1}) ; register_error_kind `Permanent diff --git a/src/proto_alpha/bin_sc_rollup_node/state.ml b/src/proto_alpha/bin_sc_rollup_node/state.ml index 9c71b60c501714b678da0663d7aa6ed4a2b13322..dcd13e4db0ac97db361b0594860dc33bde3a024e 100644 --- a/src/proto_alpha/bin_sc_rollup_node/state.ml +++ b/src/proto_alpha/bin_sc_rollup_node/state.ml @@ -47,14 +47,14 @@ module Store = struct module Last_finalized_head = Store.Make_mutable_value (struct - let path = ["tezos"; "finalized_head"] + let path = ["finalized_head"] end) (struct - type value = Layer1.head + type value = Sc_rollup_block.t - let name = "head" + let name = "l2_block" - let encoding = Layer1.head_encoding + let encoding = Sc_rollup_block.encoding end) (** Table from L1 levels to blocks hashes. *) @@ -120,7 +120,12 @@ let is_processed store head = Raw_store.L2_blocks.mem store head let last_processed_head_opt store = Store.L2_head.find store -let mark_finalized_head store head = Store.Last_finalized_head.set store head +let mark_finalized_head store head_hash = + let open Lwt_syntax in + let* block = Raw_store.L2_blocks.find store head_hash in + match block with + | None -> return_unit + | Some block -> Store.Last_finalized_head.set store block let get_finalized_head_opt store = Store.Last_finalized_head.find store diff --git a/src/proto_alpha/bin_sc_rollup_node/state.mli b/src/proto_alpha/bin_sc_rollup_node/state.mli index a5c7335a19bd270b14d1934065ca6013bac6e96b..c67158f14744b84f3860cab56e34f56fe557e482 100644 --- a/src/proto_alpha/bin_sc_rollup_node/state.mli +++ b/src/proto_alpha/bin_sc_rollup_node/state.mli @@ -40,10 +40,10 @@ val last_processed_head_opt : _ Store.t -> Sc_rollup_block.t option Lwt.t (** [mark_finalized_head store head] remembers that the [head] is finalized. By construction, every block whose level is smaller than [head]'s is also finalized. *) -val mark_finalized_head : Store.rw -> Layer1.head -> unit Lwt.t +val mark_finalized_head : Store.rw -> Block_hash.t -> unit Lwt.t (** [last_finalized_head_opt store] returns the last finalized head if it exists. *) -val get_finalized_head_opt : _ Store.t -> Layer1.head option Lwt.t +val get_finalized_head_opt : _ Store.t -> Sc_rollup_block.t option Lwt.t (** [hash_of_level node_ctxt level] returns the current block hash for a given [level]. *) diff --git a/src/proto_alpha/bin_sc_rollup_node/store.ml b/src/proto_alpha/bin_sc_rollup_node/store.ml index 2c7b9924c0fb88c097fb495fef5aa60680dabc40..84de5a5e8635e812968da360482f80b58f222714 100644 --- a/src/proto_alpha/bin_sc_rollup_node/store.ml +++ b/src/proto_alpha/bin_sc_rollup_node/store.ml @@ -163,23 +163,41 @@ module Last_stored_commitment_level = let encoding = Raw_level.encoding end) -module Commitments_published_at_level = - Make_updatable_map - (struct - let path = ["commitments"; "published_at_level"] - end) - (struct - type key = Sc_rollup.Commitment.Hash.t +module Commitments_published_at_level = struct + type element = { + first_published_at_level : Raw_level.t; + published_at_level : Raw_level.t option; + } - let to_path_representation = Sc_rollup.Commitment.Hash.to_b58check - end) - (struct - type value = Raw_level.t + let element_encoding = + let open Data_encoding in + conv + (fun {first_published_at_level; published_at_level} -> + (first_published_at_level, published_at_level)) + (fun (first_published_at_level, published_at_level) -> + {first_published_at_level; published_at_level}) + @@ obj2 + (req "first_published_at_level" Raw_level.encoding) + (opt "published_at_level" Raw_level.encoding) - let name = "raw_level" + include + Make_updatable_map + (struct + let path = ["commitments"; "published_at_level"] + end) + (struct + type key = Sc_rollup.Commitment.Hash.t - let encoding = Raw_level.encoding - end) + let to_path_representation = Sc_rollup.Commitment.Hash.to_b58check + end) + (struct + type value = element + + let name = "published_levels" + + let encoding = element_encoding + end) +end (* Published slot headers per block hash, stored as a list of bindings from `Dal_slot_index.t` diff --git a/src/proto_alpha/bin_sc_rollup_node/store.mli b/src/proto_alpha/bin_sc_rollup_node/store.mli index 05766389c95a9c899d88151ab37cab50fe08e1b3..51af89bcf21cdaf14f9162003d1a02dd247ba378 100644 --- a/src/proto_alpha/bin_sc_rollup_node/store.mli +++ b/src/proto_alpha/bin_sc_rollup_node/store.mli @@ -102,11 +102,22 @@ module Last_stored_commitment_level : (** Storage mapping commitment hashes to the level when they were published by the rollup node. It only contains hashes of commitments published by this rollup node. *) -module Commitments_published_at_level : - Store_sigs.Map - with type key := Sc_rollup.Commitment.Hash.t - and type value := Raw_level.t - and type 'a store := 'a store +module Commitments_published_at_level : sig + type element = { + first_published_at_level : Raw_level.t; + (** The level at which this commitment was first published. *) + published_at_level : Raw_level.t option; + (** The level at which we published this commitment. If + [first_published_at_level <> published_at_level] it means that the + commitment is republished. *) + } + + include + Store_sigs.Map + with type key := Sc_rollup.Commitment.Hash.t + and type value := element + and type 'a store := 'a store +end (** Published slot headers per block hash, stored as a list of bindings from [Dal_slot_index.t] diff --git a/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml b/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml index fccaf23df7c60ce43cfe94063fd3e82ef19d2ca2..c311d86ed731d8dc86c442986c07157bf3ef12df 100644 --- a/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml +++ b/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml @@ -66,7 +66,8 @@ type inbox_info = {finalized : bool; cemented : bool} type commitment_info = { commitment : Sc_rollup.Commitment.t; commitment_hash : Sc_rollup.Commitment.Hash.t; - published_at : Raw_level.t; + first_published_at_level : Raw_level.t; + published_at_level : Raw_level.t; } type message_status = @@ -80,10 +81,16 @@ type message_status = module Encodings = struct open Data_encoding - let commitment_with_hash_and_level = - obj3 + let commitment_with_hash = + obj2 (req "commitment" Sc_rollup.Commitment.encoding) (req "hash" Sc_rollup.Commitment.Hash.encoding) + + let commitment_with_hash_and_level_infos = + obj4 + (req "commitment" Sc_rollup.Commitment.encoding) + (req "hash" Sc_rollup.Commitment.Hash.encoding) + (opt "first_published_at_level" Raw_level.encoding) (opt "published_at_level" Raw_level.encoding) let hex_string = conv Bytes.of_string Bytes.to_string bytes @@ -144,15 +151,31 @@ module Encodings = struct let commitment_info = conv - (fun {commitment; commitment_hash; published_at} -> - (commitment, (commitment_hash, published_at))) - (fun (commitment, (commitment_hash, published_at)) -> - {commitment; commitment_hash; published_at}) - @@ merge_objs - Sc_rollup.Commitment.encoding - (obj2 - (req "hash" Sc_rollup.Commitment.Hash.encoding) - (req "published_at" Raw_level.encoding)) + (fun { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + } -> + ( commitment, + commitment_hash, + first_published_at_level, + published_at_level )) + (fun ( commitment, + commitment_hash, + first_published_at_level, + published_at_level ) -> + { + commitment; + commitment_hash; + first_published_at_level; + published_at_level; + }) + @@ obj4 + (req "commitment" Sc_rollup.Commitment.encoding) + (req "hash" Sc_rollup.Commitment.Hash.encoding) + (req "first_published_at_level" Raw_level.encoding) + (req "published_at_level" Raw_level.encoding) let message_status = union @@ -327,7 +350,7 @@ module Global = struct Tezos_rpc.Service.get_service ~description:"Last commitment computed by the node" ~query:Tezos_rpc.Query.empty - ~output:(Data_encoding.option Encodings.commitment_with_hash_and_level) + ~output:(Data_encoding.option Encodings.commitment_with_hash) (path / "last_stored_commitment") module Helpers = struct @@ -628,7 +651,8 @@ module Local = struct Tezos_rpc.Service.get_service ~description:"Last commitment published by the node" ~query:Tezos_rpc.Query.empty - ~output:(Data_encoding.option Encodings.commitment_with_hash_and_level) + ~output: + (Data_encoding.option Encodings.commitment_with_hash_and_level_infos) (path / "last_published_commitment") let injection = diff --git a/tezt/lib_tezos/sc_rollup_client.ml b/tezt/lib_tezos/sc_rollup_client.ml index 33733c71a0fc5514d58bab368f0dd49ab01a5d82..22b7674ccb61790e47a67f473c03f5eea3d25502 100644 --- a/tezt/lib_tezos/sc_rollup_client.ml +++ b/tezt/lib_tezos/sc_rollup_client.ml @@ -59,17 +59,27 @@ let commitment_from_json json = let number_of_ticks = JSON.as_int @@ JSON.get "number_of_ticks" json in Some {compressed_state; inbox_level; predecessor; number_of_ticks} -let commitment_with_hash_and_level_from_json json = - let hash, commitment_json, published_at_level = +let commitment_with_hash_from_json json = + let hash, commitment_json = + (JSON.get "hash" json, JSON.get "commitment" json) + in + Option.map + (fun commitment -> (JSON.as_string hash, commitment)) + (commitment_from_json commitment_json) + +let commitment_with_hash_and_levels_from_json json = + let hash, commitment_json, first_published_at_level, included_at_level = ( JSON.get "hash" json, JSON.get "commitment" json, - JSON.get "published_at_level" json ) + JSON.get "first_published_at_level" json, + JSON.get "included_at_level" json ) in Option.map (fun commitment -> ( JSON.as_string hash, commitment, - published_at_level |> JSON.as_opt |> Option.map JSON.as_int )) + first_published_at_level |> JSON.as_opt |> Option.map JSON.as_int, + included_at_level |> JSON.as_opt |> Option.map JSON.as_int )) (commitment_from_json commitment_json) let next_name = ref 1 @@ -250,11 +260,11 @@ let outbox ?hooks ?(block = "cemented") ~outbox_level sc_client = let last_stored_commitment ?hooks sc_client = rpc_get ?hooks sc_client ["global"; "last_stored_commitment"] - |> Runnable.map commitment_with_hash_and_level_from_json + |> Runnable.map commitment_with_hash_from_json let last_published_commitment ?hooks sc_client = rpc_get ?hooks sc_client ["local"; "last_published_commitment"] - |> Runnable.map commitment_with_hash_and_level_from_json + |> Runnable.map commitment_with_hash_and_levels_from_json let dal_slot_headers ?hooks ?(block = "head") sc_client = rpc_get ?hooks sc_client ["global"; "block"; block; "dal"; "slot_headers"] diff --git a/tezt/lib_tezos/sc_rollup_client.mli b/tezt/lib_tezos/sc_rollup_client.mli index 4e88f5ee6415dc3b2448aa5d23f7b6e92f561bca..7c509cca4dc4e6740cb81d1064fde5039f63ed50 100644 --- a/tezt/lib_tezos/sc_rollup_client.mli +++ b/tezt/lib_tezos/sc_rollup_client.mli @@ -164,25 +164,24 @@ val encode_batch : (** [commitment_from_json] parses a commitment from its JSON representation. *) val commitment_from_json : JSON.t -> commitment option -(** [commitment_with_hash_and_level_from_json] parses a commitment, its hash - and the level when the commitment was first published (if any), from the - JSON representation. *) -val commitment_with_hash_and_level_from_json : - JSON.t -> (string * commitment * int option) option +(** [commitment_with_hash_and_level_from_json] parses a commitment, its hash and + the levels when the commitment was first published (if any) and included, + from the JSON representation. *) +val commitment_with_hash_and_levels_from_json : + JSON.t -> (string * commitment * int option * int option) option (** [last_stored_commitment client] gets the last commitment with its hash stored by the rollup node. *) val last_stored_commitment : - ?hooks:Process.hooks -> - t -> - (string * commitment * int option) option Runnable.process + ?hooks:Process.hooks -> t -> (string * commitment) option Runnable.process -(** [last_published_commitment client] gets the last commitment published by the rollup node, -with its hash and level when the commitment was first published. *) +(** [last_published_commitment client] gets the last commitment published by the + rollup node, with its hash and level when the commitment was first published + and the level it was included. *) val last_published_commitment : ?hooks:Process.hooks -> t -> - (string * commitment * int option) option Runnable.process + (string * commitment * int option * int option) option Runnable.process (** [dal_slot_headers ?block client] returns the dal slot headers of the [block] (default ["head"]). *) diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 6b6c18d4236de4c31ced8fe0090d0f814aea9537..a13ec69d6c3e418c19b73d3ab8a72e175020c4e5 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -306,10 +306,19 @@ let test_full_scenario ?regression ~kind ?boot_sector ?commitment_period in scenario protocol rollup_node rollup_client sc_rollup tezos_node tezos_client -let inbox_level (_hash, (commitment : Sc_rollup_client.commitment), _level) = +let stored_inbox_level (_hash, (commitment : Sc_rollup_client.commitment)) = commitment.inbox_level -let number_of_ticks (_hash, (commitment : Sc_rollup_client.commitment), _level) +let stored_number_of_ticks (_hash, (commitment : Sc_rollup_client.commitment)) = + commitment.number_of_ticks + +let published_inbox_level + (_hash, (commitment : Sc_rollup_client.commitment), _pub_level, _incl_level) + = + commitment.inbox_level + +let published_number_of_ticks + (_hash, (commitment : Sc_rollup_client.commitment), _pub_level, _incl_level) = commitment.number_of_ticks @@ -336,12 +345,19 @@ let get_staked_on_commitment ~sc_rollup ~staker client = | Some hash -> return hash | None -> failwith (Format.sprintf "hash is missing %s" __LOC__) -let hash (hash, (_ : Sc_rollup_client.commitment), _level) = hash +let stored_hash (hash, (_ : Sc_rollup_client.commitment)) = hash -let first_published_at_level (_hash, (_ : Sc_rollup_client.commitment), level) = +let published_hash (hash, (_ : Sc_rollup_client.commitment), _level, _incl_level) + = + hash + +let first_published_at_level + (_hash, (_ : Sc_rollup_client.commitment), level, _incl_level) = level -let predecessor (_hash, {Sc_rollup_client.predecessor; _}, _level) = predecessor +let predecessor (_hash, {Sc_rollup_client.predecessor; _}, _level, _incl_level) + = + predecessor let cement_commitment ?(src = "bootstrap1") ?fail ~sc_rollup ~hash client = let p = @@ -1230,11 +1246,11 @@ let check_published_commitment_in_l1 ?(allow_non_published = false) if not allow_non_published then Test.fail "No commitment has been published" ; Lwt.return_none - | Some (hash, _commitment, _level) -> + | Some (hash, _commitment, _pub_level, _incl_level) -> tezos_client_get_commitment client sc_rollup hash in let published_commitment = - Option.map (fun (_, c, _) -> c) published_commitment + Option.map (fun (_, c, _, _) -> c) published_commitment in check_commitment_eq (commitment_in_l1, "in L1") @@ -1309,7 +1325,7 @@ let commitment_stored _protocol sc_rollup_node sc_rollup_client sc_rollup _node let*! stored_commitment = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client in - let stored_inbox_level = Option.map inbox_level stored_commitment in + let stored_inbox_level = Option.map stored_inbox_level stored_commitment in Check.(stored_inbox_level = Some (levels_to_commitment + init_level)) (Check.option Check.int) ~error_msg: @@ -1320,8 +1336,8 @@ let commitment_stored _protocol sc_rollup_node sc_rollup_client sc_rollup _node Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in check_commitment_eq - (Option.map (fun (_, c, _) -> c) stored_commitment, "stored") - (Option.map (fun (_, c, _) -> c) published_commitment, "published") ; + (Option.map (fun (_, c) -> c) stored_commitment, "stored") + (Option.map (fun (_, c, _, _) -> c) published_commitment, "published") ; check_published_commitment_in_l1 sc_rollup client published_commitment let mode_publish mode publishes _protocol sc_rollup_node sc_rollup_client @@ -1441,7 +1457,7 @@ let commitment_not_published_if_non_final _protocol sc_rollup_node let*! commitment = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client in - let stored_inbox_level = Option.map inbox_level commitment in + let stored_inbox_level = Option.map stored_inbox_level commitment in Check.(stored_inbox_level = Some store_commitment_level) (Check.option Check.int) ~error_msg: @@ -1449,7 +1465,7 @@ let commitment_not_published_if_non_final _protocol sc_rollup_node let*! commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in - let published_inbox_level = Option.map inbox_level commitment in + let published_inbox_level = Option.map published_inbox_level commitment in Check.(published_inbox_level = None) (Check.option Check.int) ~error_msg: @@ -1458,7 +1474,7 @@ let commitment_not_published_if_non_final _protocol sc_rollup_node unit let commitments_messages_reset kind _protocol sc_rollup_node sc_rollup_client - sc_rollup _node client = + sc_rollup node client = (* For `sc_rollup_commitment_period_in_blocks` levels after the sc rollup origination, i messages are sent to the rollup, for a total of `sc_rollup_commitment_period_in_blocks * @@ -1511,13 +1527,15 @@ let commitments_messages_reset kind _protocol sc_rollup_node sc_rollup_client let*! stored_commitment = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client in - let stored_inbox_level = Option.map inbox_level stored_commitment in + let stored_inbox_level = Option.map stored_inbox_level stored_commitment in Check.(stored_inbox_level = Some (init_level + (2 * levels_to_commitment))) (Check.option Check.int) ~error_msg: "Commitment has been stored at a level different than expected (%L = %R)" ; Log.info "levels_to_commitment: %d" levels_to_commitment ; - (let stored_number_of_ticks = Option.map number_of_ticks stored_commitment in + (let stored_number_of_ticks = + Option.map stored_number_of_ticks stored_commitment + in let expected = match kind with | "arith" -> 3 * levels_to_commitment @@ -1534,12 +1552,14 @@ let commitments_messages_reset kind _protocol sc_rollup_node sc_rollup_client ~error_msg: "Number of ticks processed by commitment is different from the number \ of ticks expected (%L = %R)") ; + let* () = Client.bake_for_and_wait client in + let* _ = Sc_rollup_node.wait_for_level sc_rollup_node (Node.get_level node) in let*! published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in check_commitment_eq - (Option.map (fun (_, c, _) -> c) stored_commitment, "stored") - (Option.map (fun (_, c, _) -> c) published_commitment, "published") ; + (Option.map (fun (_, c) -> c) stored_commitment, "stored") + (Option.map (fun (_, c, _, _) -> c) published_commitment, "published") ; check_published_commitment_in_l1 sc_rollup client published_commitment let commitment_stored_robust_to_failures _protocol sc_rollup_node @@ -1624,8 +1644,8 @@ let commitment_stored_robust_to_failures _protocol sc_rollup_node Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client' in check_commitment_eq - (Option.map (fun (_, c, _) -> c) stored_commitment, "stored in first node") - (Option.map (fun (_, c, _) -> c) stored_commitment', "stored in second node") ; + (Option.map snd stored_commitment, "stored in first node") + (Option.map snd stored_commitment', "stored in second node") ; unit let commitments_reorgs ~kind _protocol sc_rollup_node sc_rollup_client sc_rollup @@ -1731,13 +1751,15 @@ let commitments_reorgs ~kind _protocol sc_rollup_node sc_rollup_client sc_rollup let*! stored_commitment = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client in - let stored_inbox_level = Option.map inbox_level stored_commitment in + let stored_inbox_level = Option.map stored_inbox_level stored_commitment in Check.(stored_inbox_level = Some (init_level + levels_to_commitment)) (Check.option Check.int) ~error_msg: "Commitment has been stored at a level different than expected (%L = %R)" ; let () = Log.info "init_level: %d" init_level in - (let stored_number_of_ticks = Option.map number_of_ticks stored_commitment in + (let stored_number_of_ticks = + Option.map stored_number_of_ticks stored_commitment + in let expected_number_of_ticks = match kind with | "arith" -> @@ -1758,12 +1780,14 @@ let commitments_reorgs ~kind _protocol sc_rollup_node sc_rollup_client sc_rollup ~error_msg: "Number of ticks processed by commitment is different from the number \ of ticks expected (%L = %R)") ; + let* () = Client.bake_for_and_wait client in + let* _ = Sc_rollup_node.wait_for_level sc_rollup_node (Node.get_level node) in let*! published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in check_commitment_eq - (Option.map (fun (_, c, _) -> c) stored_commitment, "stored") - (Option.map (fun (_, c, _) -> c) published_commitment, "published") ; + (Option.map snd stored_commitment, "stored") + (Option.map (fun (_, c, _, _) -> c) published_commitment, "published") ; check_published_commitment_in_l1 sc_rollup client published_commitment type balances = {liquid : int; frozen : int} @@ -1850,13 +1874,13 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node in (* Bake `block_finality_time` additional level to ensure that block number `init_level + sc_rollup_commitment_period_in_blocks` is processed by - the rollup node as finalized. *) - let* () = bake_levels block_finality_time client in - let* commitment_finalized_level = + the rollup node as finalized and one additional for commitment inclusion. *) + let* () = bake_levels (block_finality_time + 1) client in + let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node - (commitment_inbox_level + block_finality_time) + (Node.get_level node) in let*! rollup_node1_stored_commitment = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client @@ -1866,7 +1890,7 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node in let () = Check.( - Option.map inbox_level rollup_node1_published_commitment + Option.map published_inbox_level rollup_node1_published_commitment = Some commitment_inbox_level) (Check.option Check.int) ~error_msg: @@ -1878,18 +1902,18 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node (that is at level `commitment_finalized_level`). Note that at this point we are already at level `commitment_finalized_level`, hence cementation of the commitment can happen. *) - let levels_to_cementation = challenge_window + 1 in + let levels_to_cementation = challenge_window in let cemented_commitment_hash = - Option.map hash rollup_node1_published_commitment + Option.map published_hash rollup_node1_published_commitment |> Option.value ~default:"src142qqoZP1iPSALZPSy7ip4StkiF5neWNWviQ8V6uRADsnvuegVH" in let* () = bake_levels levels_to_cementation client in - let* cemented_commitment_level = + let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node - (commitment_finalized_level + levels_to_cementation) + (Node.get_level node) in (* Withdraw stake before cementing should fail *) @@ -1906,11 +1930,11 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node let* () = cement_commitment client ~sc_rollup ~hash:cemented_commitment_hash in - let* level_after_cementation = + let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node - (cemented_commitment_level + 1) + (Node.get_level node) in (* Withdraw stake after cementing should succeed *) @@ -1934,28 +1958,23 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node in let* () = Sc_rollup_node.run sc_rollup_node' [] in - let* rollup_node2_catchup_level = + let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node' - level_after_cementation + (Node.get_level node) in - Check.(rollup_node2_catchup_level = level_after_cementation) - Check.int - ~error_msg:"Current level has moved past cementation inbox level (%L = %R)" ; (* Check that no commitment was published. *) let*! rollup_node2_last_published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client' in let rollup_node2_last_published_commitment_inbox_level = - Option.map inbox_level rollup_node2_last_published_commitment + Option.map published_inbox_level rollup_node2_last_published_commitment in let () = Check.(rollup_node2_last_published_commitment_inbox_level = None) (Check.option Check.int) - ~error_msg: - "Commitment has been published at a level different than expected (%L \ - = %R)" + ~error_msg:"Commitment has been published by node 2 at %L but shouldn't" in (* Check that the commitment stored by the second rollup node is the same commmitment stored by the first rollup node. *) @@ -1964,28 +1983,28 @@ let commitment_before_lcc_not_published _protocol sc_rollup_node in let () = Check.( - Option.map hash rollup_node1_stored_commitment - = Option.map hash rollup_node2_stored_commitment) + Option.map stored_hash rollup_node1_stored_commitment + = Option.map stored_hash rollup_node2_stored_commitment) (Check.option Check.string) ~error_msg: - "Commitment stored by first and second rollup nodes differ (%L = %R)" + "Commitments stored by first (%L) and second (%R) rollup nodes differ" in (* Bake other commitment_period levels and check that rollup_node2 is - able to publish a commitment. *) - let* () = bake_levels commitment_period client' in + able to publish a commitment (bake one extra to see commitment in block). *) + let* () = bake_levels (commitment_period + 1) client' in let commitment_inbox_level = commitment_inbox_level + commitment_period in let* _ = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node' - (level_after_cementation + commitment_period) + (Node.get_level node) in let*! rollup_node2_last_published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client' in let rollup_node2_last_published_commitment_inbox_level = - Option.map inbox_level rollup_node2_last_published_commitment + Option.map published_inbox_level rollup_node2_last_published_commitment in let () = Check.( @@ -2036,32 +2055,23 @@ let first_published_level_is_global _protocol sc_rollup_node sc_rollup_client (* Bake `block_finality_time` additional level to ensure that block number `init_level + sc_rollup_commitment_period_in_blocks` is processed by the rollup node as finalized. *) - let* () = bake_levels block_finality_time client in + let* () = bake_levels (block_finality_time + 1) client in let* _commitment_finalized_level = Sc_rollup_node.wait_for_level ~timeout:3. sc_rollup_node - (commitment_inbox_level + block_finality_time) + (Node.get_level node) in let*! rollup_node1_published_commitment = Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client in Check.( - Option.map inbox_level rollup_node1_published_commitment + Option.map published_inbox_level rollup_node1_published_commitment = Some commitment_inbox_level) (Check.option Check.int) ~error_msg: - "Commitment has been published at a level different than expected (%L = \ - %R)" ; - (* Bake an additional block for the commitment to be included. *) - let* () = Client.bake_for_and_wait client in + "Commitment has been published for a level %L different than expected %R" ; let commitment_publish_level = Node.get_level node in - let* _ = - Sc_rollup_node.wait_for_level sc_rollup_node commitment_publish_level - in - let*! rollup_node1_published_commitment = - Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client - in Check.( Option.bind rollup_node1_published_commitment first_published_at_level = Some commitment_publish_level) @@ -2105,9 +2115,9 @@ let first_published_level_is_global _protocol sc_rollup_node sc_rollup_client Sc_rollup_client.last_published_commitment ~hooks sc_rollup_client' in check_commitment_eq - ( Option.map (fun (_, c, _) -> c) rollup_node1_published_commitment, + ( Option.map (fun (_, c, _, _) -> c) rollup_node1_published_commitment, "published by rollup node 1" ) - ( Option.map (fun (_, c, _) -> c) rollup_node2_published_commitment, + ( Option.map (fun (_, c, _, _) -> c) rollup_node2_published_commitment, "published by rollup node 2" ) ; let () = Check.( @@ -3826,7 +3836,7 @@ let test_messages_processed_by_commitment ~kind = sc_rollup_node store_commitment_level in - let* _, {inbox_level; _}, _ = + let* _, {inbox_level; _} = let*! stored_commitment_opt = Sc_rollup_client.last_stored_commitment ~hooks sc_rollup_client in