diff --git a/CHANGES.rst b/CHANGES.rst index 586c2b24b05c2a48e69cf71a1c63e5cd487da911..522e9798f11c9d79a22314a3d76cc47019f544b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -217,6 +217,9 @@ Smart Rollup node - Fix a bug in how commitments are computed after a protocol migration where the the commitment period changes. (MR :gl:`!13588`) +- New command ``repair commitments`` which allows the rollup node to recompute + correct commitments for a protocol upgrade which did not. (MR :gl:`!13615`) + Smart Rollup WASM Debugger -------------------------- diff --git a/src/bin_smart_rollup_node/main_smart_rollup_node.ml b/src/bin_smart_rollup_node/main_smart_rollup_node.ml index f0b042423823f7c0f2dd0f5cd3c9cefa6c225eed..83df15295195461c005fa019fd118ca9b93c3d94 100644 --- a/src/bin_smart_rollup_node/main_smart_rollup_node.ml +++ b/src/bin_smart_rollup_node/main_smart_rollup_node.ml @@ -531,6 +531,7 @@ let sc_rollup_commands () = snapshot_info; openapi_command; ] + @ Repair.commands let select_commands _ctxt _ = Lwt_result_syntax.return (sc_rollup_commands ()) diff --git a/src/lib_smart_rollup_node/cli.ml b/src/lib_smart_rollup_node/cli.ml index 28fea6602e47a06fbe3846cba4d4e4852d331e3d..6d6cca995ed03b4ca4e1673c7db0185fb02d9a46 100644 --- a/src/lib_smart_rollup_node/cli.ml +++ b/src/lib_smart_rollup_node/cli.ml @@ -513,6 +513,13 @@ let protocol_hash_arg = between different versions of the node." protocol_hash_parameter +let protocol_hash_param next = + Tezos_clic.param + ~name:"protocol" + ~desc:"Protocol hash" + protocol_hash_parameter + next + let apply_unsafe_patches_switch : (bool, Client_context.full) Tezos_clic.arg = Tezos_clic.switch ~long:"apply-unsafe-patches" diff --git a/src/lib_smart_rollup_node/publisher.ml b/src/lib_smart_rollup_node/publisher.ml index aec6ccdbe44d70ff4de7eedb77eb91e21a23cf11..281567c54e095a58f87867bd7260d3e873fcd5e0 100644 --- a/src/lib_smart_rollup_node/publisher.ml +++ b/src/lib_smart_rollup_node/publisher.ml @@ -208,11 +208,6 @@ let create_commitment_if_necessary plugin (node_ctxt : _ Node_context.t) ~inbox_level:current_level ctxt in - let*! () = - Commitment_event.new_commitment - (Octez_smart_rollup.Commitment.hash commitment) - commitment.inbox_level - in return_some commitment else return_none @@ -231,6 +226,11 @@ let process_head plugin (node_ctxt : _ Node_context.t) ~predecessor match commitment with | None -> return_none | Some commitment -> + let*! () = + Commitment_event.new_commitment + (Octez_smart_rollup.Commitment.hash commitment) + commitment.inbox_level + in let* commitment_hash = Node_context.save_commitment node_ctxt commitment in diff --git a/src/lib_smart_rollup_node/publisher.mli b/src/lib_smart_rollup_node/publisher.mli index e6c89495c3c437cdd924501b8060238b7c085690..238c53bf45718707819a4302928dc835ad48942b 100644 --- a/src/lib_smart_rollup_node/publisher.mli +++ b/src/lib_smart_rollup_node/publisher.mli @@ -55,6 +55,17 @@ val process_head : Context.rw -> Commitment.Hash.t option tzresult Lwt.t +(** [create_commitment_if_necessary plugin node_ctxt ~predecessor level ctxt] + returns the commitment for inbox level [level] if there needs to be + one. [ctxt] should be the context checkouted for [level]. *) +val create_commitment_if_necessary : + (module Protocol_plugin_sig.S) -> + 'a Node_context.t -> + predecessor:Block_hash.t -> + int32 -> + 'a Context.t -> + (Commitment.t option, tztrace) result Lwt.t + (** [publish_single_commitment node_ctxt commitment] publishes a single [commitment] if it is missing. This function is meant to be used by the {e accuser} mode to sparingly publish commitments when it detects a diff --git a/src/lib_smart_rollup_node/repair.ml b/src/lib_smart_rollup_node/repair.ml new file mode 100644 index 0000000000000000000000000000000000000000..2b60a2009397c2259bde836ea758ad8f143b6ff9 --- /dev/null +++ b/src/lib_smart_rollup_node/repair.ml @@ -0,0 +1,213 @@ +(*****************************************************************************) +(* *) +(* SPDX-License-Identifier: MIT *) +(* Copyright (c) 2024 Functori *) +(* *) +(*****************************************************************************) + +let group = + { + Tezos_clic.name = "smart_rollup.node.repair"; + title = "Commands to repair the smart rollup node."; + } + +module Cli = struct + include Cli + + include Binary_dependent_args (struct + let binary_name = "smart rollup node" + end) +end + +type fix_action = + | Nothing + | Remove of Sc_rollup_block.t + | Add of Commitment.t * Sc_rollup_block.t + | Patch of Sc_rollup_block.t + +(** Recompute the commitment for [level] and store it if necessary. Commitments + that were incorrectly computed before are removed. Pre-condition: + commitments for inbox levels before [level] must be correct. *) +let fix_commitment (node_ctxt : Node_context.rw) level = + let open Lwt_result_syntax in + let* l2_block = Node_context.get_l2_block_by_level node_ctxt level in + let stored_commitment_hash = l2_block.header.commitment_hash in + let* plugin = Protocol_plugins.proto_plugin_for_level node_ctxt level in + let (module Plugin) = plugin in + let* constants = + let constants_level = Int32.max level node_ctxt.genesis_info.level in + Protocol_plugins.get_constants_of_protocol + node_ctxt + ~level:constants_level + Plugin.protocol + in + let protocol = + { + Node_context.hash = Plugin.protocol; + proto_level = (Reference.get node_ctxt.current_protocol).proto_level; + (* leave as is because unimportant for the fix *) + constants; + } + in + let* previous_commitment_hash = + if level = node_ctxt.genesis_info.level then + (* Previous commitment for rollup genesis is itself. *) + return node_ctxt.genesis_info.commitment_hash + else + let+ pred = + Node_context.get_l2_block node_ctxt l2_block.header.predecessor + in + Sc_rollup_block.most_recent_commitment pred.header + in + Reference.set node_ctxt.current_protocol protocol ; + let* ctxt = + Node_context.checkout_context node_ctxt l2_block.header.block_hash + in + let* new_commitment = + Publisher.create_commitment_if_necessary + plugin + node_ctxt + ~predecessor:l2_block.header.predecessor + level + ctxt + in + let l2_block, action = + if + Commitment.Hash.( + previous_commitment_hash = l2_block.header.previous_commitment_hash) + then (l2_block, Nothing) + else + let l2_block = + {l2_block with header = {l2_block.header with previous_commitment_hash}} + in + (l2_block, Patch l2_block) + in + let action = + match (new_commitment, stored_commitment_hash) with + | None, None -> action + | Some new_commitment, _ -> ( + let new_commitment_hash = Commitment.hash new_commitment in + match stored_commitment_hash with + | Some stored_commitment_hash + when Commitment.Hash.(new_commitment_hash = stored_commitment_hash) -> + action + | _ -> + let l2_block = + { + l2_block with + header = + { + l2_block.header with + commitment_hash = Some new_commitment_hash; + }; + } + in + Add (new_commitment, l2_block)) + | None, Some _stored_commitment_hash -> + let l2_block = + {l2_block with header = {l2_block.header with commitment_hash = None}} + in + Remove l2_block + in + let* () = + match action with + | Nothing -> return_unit + | Patch l2_block -> Node_context.save_l2_block node_ctxt l2_block + | Remove l2_block -> + Format.printf "Removing incorrect commitment for level %ld@." level ; + Node_context.save_l2_block node_ctxt l2_block + | Add (new_commitment, l2_block) -> + let* new_commitment_hash = + Node_context.save_commitment node_ctxt new_commitment + in + Format.printf + "Registering new commitment %a for level %ld@." + Commitment.Hash.pp + new_commitment_hash + new_commitment.inbox_level ; + Node_context.save_l2_block node_ctxt l2_block + in + return action + +(** Fixes commitments for inbox levels between [first_level] and the current + head. *) +let fix_commitments node_ctxt first_level = + let open Lwt_result_syntax in + Format.printf "Fixing commitments starting at level %ld@." first_level ; + let* l2_head = Node_context.last_processed_head_opt node_ctxt in + let l2_head = WithExceptions.Option.get ~loc:__LOC__ l2_head in + let*? () = + if first_level > l2_head.header.level then + error_with + "First level to fix commitments is %ld but head is at %ld" + first_level + l2_head.header.level + else Ok () + in + let levels = + Stdlib.List.init + (Int32.to_int (Int32.sub l2_head.header.level first_level) + 1) + (fun x -> Int32.add (Int32.of_int x) first_level) + in + let* (removed, added, patched), last = + List.fold_left_es + (fun ((removed, added, patched), _last) level -> + let+ action = fix_commitment node_ctxt level in + let counters = + match action with + | Nothing -> (removed, added, patched) + | Remove _ -> (removed + 1, added, patched) + | Add _ -> (removed, added + 1, patched) + | Patch _ -> (removed, added, patched + 1) + in + (counters, action)) + ((0, 0, 0), Nothing) + levels + in + let* () = + match last with + | (Add (_, b) | Remove b | Patch b) + when b.header.level = l2_head.header.level -> + Node_context.set_l2_head node_ctxt b + | _ -> return_unit + in + Format.printf + "Completed!\n\ + Removed commitments: %3d\n\ + Added commitments: %5d\n\ + Patched blocks: %8d@." + removed + added + patched ; + return_unit + +(** Fixes commitments for [protocol] (and the following ones) up to the current + head. *) +let fix_commitments_for_protocol node_ctxt protocol = + let open Lwt_result_syntax in + Format.printf "Fixing commitments for protocol %a@." Protocol_hash.pp protocol ; + let* act_level = Node_context.protocol_activation_level node_ctxt protocol in + let first_level = + match act_level with + | First_known l -> l + | Activation_level l -> Int32.succ l + in + fix_commitments node_ctxt first_level + +let command_fix_commitments_for_protocol cctxt ~data_dir protocol = + Snapshots.with_modify_data_dir cctxt ~data_dir @@ fun node_ctxt ~head:_ -> + fix_commitments_for_protocol node_ctxt protocol + +let repair_commitments_command = + let open Tezos_clic in + command + ~group + ~desc:"Repair commitments of L2 chain for a given protocol." + (args1 Cli.data_dir_arg) + (prefixes ["repair"; "commitments"; "for"] + @@ Cli.protocol_hash_param @@ stop) + (fun data_dir protocol cctxt -> + command_fix_commitments_for_protocol cctxt ~data_dir protocol) + +(** Commands exported by the [Repair] module. *) +let commands = [repair_commitments_command] diff --git a/src/lib_smart_rollup_node/repair.mli b/src/lib_smart_rollup_node/repair.mli new file mode 100644 index 0000000000000000000000000000000000000000..4518f7b0a9556cc05e844864206a75585f30ef53 --- /dev/null +++ b/src/lib_smart_rollup_node/repair.mli @@ -0,0 +1,9 @@ +(*****************************************************************************) +(* *) +(* SPDX-License-Identifier: MIT *) +(* Copyright (c) 2024 Functori *) +(* *) +(*****************************************************************************) + +(** Repair commands offered by the rollup node. *) +val commands : Client_context.full Tezos_clic.command list diff --git a/src/lib_smart_rollup_node/snapshots.ml b/src/lib_smart_rollup_node/snapshots.ml index 36148cc801f421b93b0613f5843b3ac503301035..0d8bf9c3f95ebe727a53050ea09aeb7fb7a336e9 100644 --- a/src/lib_smart_rollup_node/snapshots.ml +++ b/src/lib_smart_rollup_node/snapshots.ml @@ -455,7 +455,7 @@ let reconstruct_level_context rollup_ctxt ~predecessor return (block, ctxt) let reconstruct_context_from_first_available_level - (node_ctxt : _ Node_context.t) (head : Sc_rollup_block.t) = + (node_ctxt : _ Node_context.t) ~(head : Sc_rollup_block.t) = let open Lwt_result_syntax in let* {first_available_level = first_level; _} = Node_context.get_gc_levels node_ctxt @@ -485,7 +485,8 @@ let reconstruct_context_from_first_available_level in reconstruct_chain_from first_block first_ctxt -let maybe_reconstruct_context cctxt ~data_dir = +let with_modify_data_dir cctxt ~data_dir + ?(skip_condition = fun _ _ ~head:_ -> Lwt_result.return false) f = let open Lwt_result_syntax in let store_dir = Configuration.default_storage_dir data_dir in let context_dir = Configuration.default_context_dir data_dir in @@ -511,44 +512,52 @@ let maybe_reconstruct_context cctxt ~data_dir = let* context = Context.load (module C) ~cache_size:100 Read_write context_dir in - let*! head_ctxt = - Context.checkout + let* skip = skip_condition store context ~head in + unless skip @@ fun () -> + let* current_protocol = + Node_context.protocol_of_level_with_store store head.header.level + in + let*? (module Plugin) = + Protocol_plugins.proto_plugin_for_protocol current_protocol.protocol + in + let* constants = + Plugin.Layer1_helpers.retrieve_constants + cctxt + ~block:(`Level head.header.level) + in + let current_protocol = + { + Node_context.hash = current_protocol.protocol; + proto_level = current_protocol.proto_level; + constants; + } + in + let* node_ctxt = + Node_context_loader.For_snapshots.create_node_context + cctxt + current_protocol + store context - (Smart_rollup_context_hash.to_context_hash head.header.context) + ~data_dir in - match head_ctxt with - | Some _ -> return_unit - | None -> - let* current_protocol = - Node_context.protocol_of_level_with_store store head.header.level - in - let*? (module Plugin) = - Protocol_plugins.proto_plugin_for_protocol current_protocol.protocol - in - let* constants = - Plugin.Layer1_helpers.retrieve_constants - cctxt - ~block:(`Level head.header.level) - in - let current_protocol = - { - Node_context.hash = current_protocol.protocol; - proto_level = current_protocol.proto_level; - constants; - } - in - let* node_ctxt = - Node_context_loader.For_snapshots.create_node_context - cctxt - current_protocol - store + let* () = f node_ctxt ~head in + let*! () = Context.close context in + let* () = Store.close store in + return_unit + +let maybe_reconstruct_context cctxt ~data_dir = + with_modify_data_dir + cctxt + ~data_dir + ~skip_condition:(fun _store context ~head -> + let open Lwt_result_syntax in + let*! head_ctxt = + Context.checkout context - ~data_dir + (Smart_rollup_context_hash.to_context_hash head.header.context) in - let* () = reconstruct_context_from_first_available_level node_ctxt head in - let*! () = Context.close context in - let* () = Store.close store in - return_unit + return (Option.is_some head_ctxt)) + reconstruct_context_from_first_available_level let post_checks ~action ~message snapshot_metadata ~dest = let open Lwt_result_syntax in diff --git a/src/lib_smart_rollup_node/snapshots.mli b/src/lib_smart_rollup_node/snapshots.mli index 4efe9877ea236899e8d55bae437eb6b66478cdd8..2f9e394e4f0ee12b016879010ebed180076edbd6 100644 --- a/src/lib_smart_rollup_node/snapshots.mli +++ b/src/lib_smart_rollup_node/snapshots.mli @@ -62,3 +62,17 @@ val import : val info : snapshot_file:string -> Snapshot_utils.snapshot_metadata * [`Compressed | `Uncompressed] + +(** [with_modify_data_dir cctxt ~data_dir ?skip_condition f] applies [f] in a + read-write context that is created from [data-dir] (and a potential existing + configuration). The node context given to [f] does not follow the L1 chain + and [f] is only supposed to modify the data of the rollup node. It is used + internally by this module to reconstruct contexts from a snapshot but can + also be use by the {!Repair} module to apply fixes off-line. *) +val with_modify_data_dir : + #Client_context.full -> + data_dir:string -> + ?skip_condition: + (Store.rw -> Context.rw -> head:Sc_rollup_block.t -> bool tzresult Lwt.t) -> + (Node_context.rw -> head:Sc_rollup_block.t -> unit tzresult Lwt.t) -> + unit tzresult Lwt.t