diff --git a/src/lib_smart_rollup/l1_operation.ml b/src/lib_smart_rollup/l1_operation.ml index 74de8ffb2d7f15bc3c62f7bf30d265fbd79f301a..daf80dc28cf09f9ce3f869ddd9da7b9646074e10 100644 --- a/src/lib_smart_rollup/l1_operation.ml +++ b/src/lib_smart_rollup/l1_operation.ml @@ -33,6 +33,7 @@ type t = refutation : Game.refutation; } | Timeout of {rollup : Address.t; stakers : Game.index} + | Recover_bond of {rollup : Address.t; staker : Signature.Public_key_hash.t} let encoding : t Data_encoding.t = let open Data_encoding in @@ -95,6 +96,16 @@ let encoding : t Data_encoding.t = (function | Timeout {rollup; stakers} -> Some (rollup, stakers) | _ -> None) (fun (rollup, stakers) -> Timeout {rollup; stakers}); + case + 5 + "recover" + (obj2 + (req "rollup" Address.encoding) + (req "staker" Signature.Public_key_hash.encoding)) + (function + | Recover_bond {rollup; staker} -> Some (rollup, staker) + | _ -> None) + (fun (rollup, staker) -> Recover_bond {rollup; staker}); ] let pp ppf = function @@ -149,7 +160,8 @@ let pp ppf = function Signature.Public_key_hash.pp opponent | Timeout {rollup = _; stakers = _} -> Format.fprintf ppf "timeout" + | Recover_bond {rollup = _; staker = _} -> Format.fprintf ppf "recover" let unique = function | Add_messages _ | Cement _ -> false - | Publish _ | Refute _ | Timeout _ -> true + | Publish _ | Refute _ | Timeout _ | Recover_bond _ -> true diff --git a/src/lib_smart_rollup/l1_operation.mli b/src/lib_smart_rollup/l1_operation.mli index 5e111d5d60b269fb1ce788cddde37d1920645212..7ac0c415f5d68355fceed2a04a5c35997b5d9c10 100644 --- a/src/lib_smart_rollup/l1_operation.mli +++ b/src/lib_smart_rollup/l1_operation.mli @@ -34,8 +34,9 @@ type t = refutation : Game.refutation; } | Timeout of {rollup : Address.t; stakers : Game.index} + | Recover_bond of {rollup : Address.t; staker : Signature.Public_key_hash.t} + (** Encoding for L1 operations (used by injector for on-disk persistence). *) -(** Encoding for L1 operations (used by injector for on-disk persistence). *) val encoding : t Data_encoding.t (** Pretty printer (human readable) for L1 operations. *) diff --git a/src/lib_smart_rollup_node/commitment_event.ml b/src/lib_smart_rollup_node/commitment_event.ml index 4f87f9042d5d087cf978cab3b516a708dfa5e2cc..fb79a5208686ca16dcfb1b7c6f950b63f894def3 100644 --- a/src/lib_smart_rollup_node/commitment_event.ml +++ b/src/lib_smart_rollup_node/commitment_event.ml @@ -101,6 +101,14 @@ module Simple = struct ("hash", Commitment.Hash.encoding) ("level", Data_encoding.int32) + let recover_bond = + declare_1 + ~section + ~name:"sc_rollup_node_recover_bond" + ~msg:"Recover bond for {staker}." + ~level:Notice + ("staker", Signature.Public_key_hash.encoding) + let commitment_parent_is_not_lcc = declare_3 ~section @@ -197,6 +205,8 @@ let compute_commitment level = Simple.(emit compute_commitment level) let publish_commitment head level = Simple.(emit publish_commitment (head, level)) +let recover_bond staker = Simple.(emit recover_bond staker) + let commitment_parent_is_not_lcc level predecessor_hash lcc_hash = Simple.(emit commitment_parent_is_not_lcc (level, predecessor_hash, lcc_hash)) diff --git a/src/lib_smart_rollup_node/commitment_event.mli b/src/lib_smart_rollup_node/commitment_event.mli index 07a809a12d5b78b5558b9b9a2ba4d9526d9276f5..64a37c7dc2a85a7ad17fa6fdbda41dd1c9eb237b 100644 --- a/src/lib_smart_rollup_node/commitment_event.mli +++ b/src/lib_smart_rollup_node/commitment_event.mli @@ -71,6 +71,10 @@ val compute_commitment : int32 -> unit Lwt.t being published. *) val publish_commitment : Commitment.Hash.t -> int32 -> unit Lwt.t +(** [recover_bond staker] emits the event that a recover bond + operation is being submitted. *) +val recover_bond : Signature.Public_key_hash.t -> unit Lwt.t + (** Events emmitted by the Publisher worker *) module Publisher : sig (** [request_failed view status errors] emits the event that a worker diff --git a/src/lib_smart_rollup_node/configuration.ml b/src/lib_smart_rollup_node/configuration.ml index 761469244442efb460c6287fc6f9c547b8beff60..2e3f0bf410472082b13ae31beaaff1e7a06c8812 100644 --- a/src/lib_smart_rollup_node/configuration.ml +++ b/src/lib_smart_rollup_node/configuration.ml @@ -34,13 +34,19 @@ type mode = | Operator | Custom -type operation_kind = Publish | Add_messages | Cement | Timeout | Refute +type operation_kind = + | Publish + | Add_messages + | Cement + | Timeout + | Refute + | Recover -type purpose = Operating | Batching | Cementing +type purpose = Operating | Batching | Cementing | Recovering -let operation_kinds = [Publish; Add_messages; Cement; Timeout; Refute] +let operation_kinds = [Publish; Add_messages; Cement; Timeout; Refute; Recover] -let purposes = [Operating; Batching; Cementing] +let purposes = [Operating; Batching; Cementing; Recovering] module Operation_kind_map = Map.Make (struct type t = operation_kind @@ -176,6 +182,7 @@ let default_burn_cap = mutez 0L *) let default_fee = function | Cement -> tez 1 + | Recover -> tez 1 | Publish -> tez 2 | Add_messages -> (* We keep this limit even though it depends on the size of the message @@ -195,6 +202,7 @@ let default_burn = function tez 1 | Add_messages -> tez 0 | Cement -> tez 0 + | Recover -> tez 0 | Timeout -> tez 0 | Refute -> (* A refutation move can store data, e.g. opening a game. *) @@ -257,6 +265,7 @@ let operation_kinds_of_purpose = function | Batching -> [Add_messages] | Cementing -> [Cement] | Operating -> [Publish; Refute; Timeout] + | Recovering -> [Recover] let string_of_operation_kind = function | Publish -> "publish" @@ -264,6 +273,7 @@ let string_of_operation_kind = function | Cement -> "cement" | Timeout -> "timeout" | Refute -> "refute" + | Recover -> "recover" let operation_kind_of_string = function | "publish" -> Some Publish @@ -271,6 +281,7 @@ let operation_kind_of_string = function | "cement" -> Some Cement | "timeout" -> Some Timeout | "refute" -> Some Refute + | "recover" -> Some Recover | _ -> None let operation_kind_of_string_exn s = @@ -282,6 +293,7 @@ let string_of_purpose = function | Operating -> "operating" | Batching -> "batching" | Cementing -> "cementing" + | Recovering -> "recovering" let purpose_of_string = function (* For backward compability: @@ -292,6 +304,7 @@ let purpose_of_string = function | "operating" | "publish" | "refute" | "timeout" -> Some Operating | "batching" | "add_messages" -> Some Batching | "cementing" | "cement" -> Some Cementing + | "recovering" -> Some Recovering | _ -> None let purpose_of_string_exn s = @@ -759,7 +772,7 @@ let check_mode config = | Observer -> narrow_purposes [] | Batcher -> narrow_purposes [Batching] | Accuser -> narrow_purposes [Operating] - | Bailout -> narrow_purposes [Operating; Cementing] + | Bailout -> narrow_purposes [Operating; Cementing; Recovering] | Maintenance -> narrow_purposes [Operating; Cementing] | Operator -> narrow_purposes [Operating; Cementing; Batching] | Custom -> return config diff --git a/src/lib_smart_rollup_node/configuration.mli b/src/lib_smart_rollup_node/configuration.mli index b1d48adb751fcade492a5b62f189676a8e8815b0..083c02cebfeff17a2a4095576748b616e993e4f9 100644 --- a/src/lib_smart_rollup_node/configuration.mli +++ b/src/lib_smart_rollup_node/configuration.mli @@ -38,11 +38,17 @@ type mode = the signers *) (** The kind of operations that can be injected by the rollup node. *) -type operation_kind = Publish | Add_messages | Cement | Timeout | Refute +type operation_kind = + | Publish + | Add_messages + | Cement + | Timeout + | Refute + | Recover (** Purposes for operators, indicating their role and thus the kinds of operations that they sign. *) -type purpose = Operating | Batching | Cementing +type purpose = Operating | Batching | Cementing | Recovering module Operation_kind_map : Map.S with type key = operation_kind diff --git a/src/lib_smart_rollup_node/injector.ml b/src/lib_smart_rollup_node/injector.ml index 6e7c072f0bea5e476916507b9275f3bc491c3f0f..4bd24ce2c60e5d230bb9c2f72969e133c397d439 100644 --- a/src/lib_smart_rollup_node/injector.ml +++ b/src/lib_smart_rollup_node/injector.ml @@ -71,6 +71,7 @@ module Parameters : | Cement -> 1 | Timeout -> 1 | Refute -> 1 + | Recover -> 1 let operation_tag : Operation.t -> Tag.t = function | Add_messages _ -> Add_messages @@ -78,6 +79,7 @@ module Parameters : | Publish _ -> Publish | Timeout _ -> Timeout | Refute _ -> Refute + | Recover_bond _ -> Recover let fee_parameter {fee_parameters; _} operation = let operation_kind = operation_tag operation in diff --git a/src/lib_smart_rollup_node/publisher.ml b/src/lib_smart_rollup_node/publisher.ml index eb39ab47ead3185e643f2f408ad6c6296dd8ddda..8298bfb9f3402e6dbd53caed8fe1cf2d90078dcb 100644 --- a/src/lib_smart_rollup_node/publisher.ml +++ b/src/lib_smart_rollup_node/publisher.ml @@ -285,6 +285,16 @@ let publish_commitment (node_ctxt : _ Node_context.t) ~source let* _hash = Injector.add_pending_operation ~source publish_operation in return_unit +let inject_recover_bond (node_ctxt : _ Node_context.t) ~source + (staker : Signature.Public_key_hash.t) = + let open Lwt_result_syntax in + let recover_operation = + L1_operation.Recover_bond {rollup = node_ctxt.rollup_address; staker} + in + let*! () = Commitment_event.recover_bond staker in + let* _hash = Injector.add_pending_operation ~source recover_operation in + return_unit + let on_publish_commitments (node_ctxt : state) = let open Lwt_result_syntax in let operator = Node_context.get_operator node_ctxt Operating in @@ -294,7 +304,7 @@ let on_publish_commitments (node_ctxt : state) = else match operator with | None -> - (* Configured to not publish commitments *) + (* No known operator we can recover bond for. *) return_unit | Some source -> let* commitments = missing_commitments node_ctxt in @@ -313,6 +323,18 @@ let publish_single_commitment node_ctxt when_ (commitment.inbox_level > lcc.level) @@ fun () -> publish_commitment node_ctxt ~source commitment +let recover_bond node_ctxt = + let open Lwt_result_syntax in + let operator = Node_context.get_operator node_ctxt Operating in + let recovery_operator = Node_context.get_operator node_ctxt Recovering in + match operator with + | None -> + (* No known operator to recover bond for. *) + return_unit + | Some committer -> + let source = Option.value recovery_operator ~default:committer in + inject_recover_bond node_ctxt ~source committer + (* 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 = diff --git a/src/lib_smart_rollup_node/publisher.mli b/src/lib_smart_rollup_node/publisher.mli index af5afb221c72d08b3b7049fecdbf2f66201efb85..15bf4a00886750b77684bd5dabd475c54be89a7a 100644 --- a/src/lib_smart_rollup_node/publisher.mli +++ b/src/lib_smart_rollup_node/publisher.mli @@ -60,6 +60,12 @@ val process_head : val publish_single_commitment : _ Node_context.t -> Commitment.t -> unit tzresult Lwt.t +(** [recover_bond node_ctxt] publishes a recover bond operator for the + Operating key. The submitter is either the operator or another + address depending of the rollup node configuration. This function + is intended to be used by the {e bailout} mode. *) +val recover_bond : _ Node_context.t -> unit tzresult Lwt.t + (** Initialize worker for publishing and cementing commitments. *) val init : _ Node_context.t -> unit tzresult Lwt.t diff --git a/src/proto_017_PtNairob/lib_sc_rollup_node/daemon_helpers.ml b/src/proto_017_PtNairob/lib_sc_rollup_node/daemon_helpers.ml index 291b988c9598bf2c2fbdde3cb630a84be37720c8..1aa60be37de5ed592c43958f5db3211570741ff3 100644 --- a/src/proto_017_PtNairob/lib_sc_rollup_node/daemon_helpers.ml +++ b/src/proto_017_PtNairob/lib_sc_rollup_node/daemon_helpers.ml @@ -102,6 +102,27 @@ let accuser_publish_commitment_when_refutable node_ctxt ~other rollup assert (Octez_smart_rollup.Address.(node_ctxt.rollup_address = rollup)) ; Publisher.publish_single_commitment node_ctxt our_commitment +(** If in bailout mode and when the operator is not staked on any + commitment, the bond is recovered. *) +let maybe_recover_bond node_ctxt = + let open Lwt_result_syntax in + if Node_context.is_bailout node_ctxt then + let operating_pkh = Node_context.get_operator node_ctxt Operating in + match operating_pkh with + | None -> return_unit + | Some operating_pkh -> ( + let* staked_on_commitment = + RPC.Sc_rollup.staked_on_commitment + (new Protocol_client_context.wrap_full node_ctxt.cctxt) + (node_ctxt.cctxt#chain, `Head 0) + (Sc_rollup_proto_types.Address.of_octez node_ctxt.rollup_address) + operating_pkh + in + match staked_on_commitment with + | None -> Publisher.recover_bond node_ctxt + | Some _ (* operator still staked on something *) -> return_unit) + else return_unit + (** Process an L1 SCORU operation (for the node's rollup) which is included for the first time. {b Note}: this function does not process inboxes for the rollup, which is done instead by {!Inbox.process_head}. *) @@ -216,6 +237,7 @@ let process_included_l1_operation (type kind) (node_ctxt : Node_context.rw) inbox_level) else Lwt.return_unit in + let* () = maybe_recover_bond node_ctxt in return_unit | ( Sc_rollup_refute _, Sc_rollup_refute_result {game_status = Ended end_status; _} ) diff --git a/src/proto_017_PtNairob/lib_sc_rollup_node/sc_rollup_injector.ml b/src/proto_017_PtNairob/lib_sc_rollup_node/sc_rollup_injector.ml index 7d85baf92c3731dbb9c662b0f248d841e9d5b5b4..d45ec0291c172b098f303f77ea03d7fb4ff55889 100644 --- a/src/proto_017_PtNairob/lib_sc_rollup_node/sc_rollup_injector.ml +++ b/src/proto_017_PtNairob/lib_sc_rollup_node/sc_rollup_injector.ml @@ -54,6 +54,9 @@ let injector_operation_to_manager : let rollup = Sc_rollup_proto_types.Address.of_octez rollup in let stakers = Sc_rollup_proto_types.Game.index_of_octez stakers in Manager (Sc_rollup_timeout {rollup; stakers}) + | Recover_bond {rollup; staker} -> + let rollup = Sc_rollup_proto_types.Address.of_octez rollup in + Manager (Sc_rollup_recover_bond {sc_rollup = rollup; staker}) let injector_operation_of_manager : type kind. diff --git a/src/proto_018_Proxford/lib_sc_rollup_node/daemon_helpers.ml b/src/proto_018_Proxford/lib_sc_rollup_node/daemon_helpers.ml index 5e4a5a9851028233e7eeb89b2b5dee0d773a9741..4c072573b6a4c95cf7151715270bd01502f3bf35 100644 --- a/src/proto_018_Proxford/lib_sc_rollup_node/daemon_helpers.ml +++ b/src/proto_018_Proxford/lib_sc_rollup_node/daemon_helpers.ml @@ -105,6 +105,27 @@ let accuser_publish_commitment_when_refutable node_ctxt ~other rollup assert (Sc_rollup.Address.(node_ctxt.rollup_address = rollup)) ; Publisher.publish_single_commitment node_ctxt our_commitment +(** If in bailout mode and when the operator is not staked on any + commitment, the bond is recovered. *) +let maybe_recover_bond node_ctxt = + let open Lwt_result_syntax in + if Node_context.is_bailout node_ctxt then + let operating_pkh = Node_context.get_operator node_ctxt Operating in + match operating_pkh with + | None -> return_unit + | Some operating_pkh -> ( + let* staked_on_commitment = + RPC.Sc_rollup.staked_on_commitment + (new Protocol_client_context.wrap_full node_ctxt.cctxt) + (node_ctxt.cctxt#chain, `Head 0) + node_ctxt.rollup_address + operating_pkh + in + match staked_on_commitment with + | None -> Publisher.recover_bond node_ctxt + | Some _ (* operator still staked on something *) -> return_unit) + else return_unit + (** Process an L1 SCORU operation (for the node's rollup) which is included for the first time. {b Note}: this function does not process inboxes for the rollup, which is done instead by {!Inbox.process_head}. *) @@ -213,6 +234,7 @@ let process_included_l1_operation (type kind) (node_ctxt : Node_context.rw) inbox_level) else Lwt.return_unit in + let* () = maybe_recover_bond node_ctxt in return_unit | ( Sc_rollup_refute _, Sc_rollup_refute_result {game_status = Ended end_status; _} ) diff --git a/src/proto_018_Proxford/lib_sc_rollup_node/sc_rollup_injector.ml b/src/proto_018_Proxford/lib_sc_rollup_node/sc_rollup_injector.ml index bb28bab19547ae12c9304f09a002f7aa3f0fed9b..72e4357d5d3a2110611e1990aa358ea28f324e6c 100644 --- a/src/proto_018_Proxford/lib_sc_rollup_node/sc_rollup_injector.ml +++ b/src/proto_018_Proxford/lib_sc_rollup_node/sc_rollup_injector.ml @@ -51,6 +51,9 @@ let injector_operation_to_manager : let rollup = Sc_rollup_proto_types.Address.of_octez rollup in let stakers = Sc_rollup_proto_types.Game.index_of_octez stakers in Manager (Sc_rollup_timeout {rollup; stakers}) + | Recover_bond {rollup; staker} -> + let rollup = Sc_rollup_proto_types.Address.of_octez rollup in + Manager (Sc_rollup_recover_bond {sc_rollup = rollup; staker}) let injector_operation_of_manager : type kind. diff --git a/src/proto_alpha/lib_sc_rollup_node/daemon_helpers.ml b/src/proto_alpha/lib_sc_rollup_node/daemon_helpers.ml index 5e4a5a9851028233e7eeb89b2b5dee0d773a9741..4c072573b6a4c95cf7151715270bd01502f3bf35 100644 --- a/src/proto_alpha/lib_sc_rollup_node/daemon_helpers.ml +++ b/src/proto_alpha/lib_sc_rollup_node/daemon_helpers.ml @@ -105,6 +105,27 @@ let accuser_publish_commitment_when_refutable node_ctxt ~other rollup assert (Sc_rollup.Address.(node_ctxt.rollup_address = rollup)) ; Publisher.publish_single_commitment node_ctxt our_commitment +(** If in bailout mode and when the operator is not staked on any + commitment, the bond is recovered. *) +let maybe_recover_bond node_ctxt = + let open Lwt_result_syntax in + if Node_context.is_bailout node_ctxt then + let operating_pkh = Node_context.get_operator node_ctxt Operating in + match operating_pkh with + | None -> return_unit + | Some operating_pkh -> ( + let* staked_on_commitment = + RPC.Sc_rollup.staked_on_commitment + (new Protocol_client_context.wrap_full node_ctxt.cctxt) + (node_ctxt.cctxt#chain, `Head 0) + node_ctxt.rollup_address + operating_pkh + in + match staked_on_commitment with + | None -> Publisher.recover_bond node_ctxt + | Some _ (* operator still staked on something *) -> return_unit) + else return_unit + (** Process an L1 SCORU operation (for the node's rollup) which is included for the first time. {b Note}: this function does not process inboxes for the rollup, which is done instead by {!Inbox.process_head}. *) @@ -213,6 +234,7 @@ let process_included_l1_operation (type kind) (node_ctxt : Node_context.rw) inbox_level) else Lwt.return_unit in + let* () = maybe_recover_bond node_ctxt in return_unit | ( Sc_rollup_refute _, Sc_rollup_refute_result {game_status = Ended end_status; _} ) diff --git a/src/proto_alpha/lib_sc_rollup_node/sc_rollup_injector.ml b/src/proto_alpha/lib_sc_rollup_node/sc_rollup_injector.ml index 67aa70089c7875c5d9989ec88fb581bd057a70c8..00883fc02d21476310addcbdeda5344cac8000a3 100644 --- a/src/proto_alpha/lib_sc_rollup_node/sc_rollup_injector.ml +++ b/src/proto_alpha/lib_sc_rollup_node/sc_rollup_injector.ml @@ -51,6 +51,9 @@ let injector_operation_to_manager : let rollup = Sc_rollup_proto_types.Address.of_octez rollup in let stakers = Sc_rollup_proto_types.Game.index_of_octez stakers in Manager (Sc_rollup_timeout {rollup; stakers}) + | Recover_bond {rollup; staker} -> + let rollup = Sc_rollup_proto_types.Address.of_octez rollup in + Manager (Sc_rollup_recover_bond {sc_rollup = rollup; staker}) let injector_operation_of_manager : type kind. diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 307ad94670c22ad6cb200bb24646cafb58aa7c70..08fcd27d0f0373071e0b615aa24a360777d61e68 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -5895,12 +5895,12 @@ let test_rollup_whitelist_outdated_update ~kind = ~msg:(rex ".*Outdated whitelist update: got outbox level") process -(** This test uses the rollup node, first it is running in an - Operator mode, it bakes some blocks, then terminate. Then we - restart the node in a Bailout mode, and make sure that there are - no new commitments have been published *) +(** This test uses the rollup node, first it is running in an Operator + mode, it bakes some blocks, then terminate. Then we restart the + node in a Bailout mode, initiate the recover_bond process, and + make sure that no new commitments are published. *) let bailout_mode_not_publish ~kind = - let operator = Constant.bootstrap1.public_key_hash in + let operator = Constant.bootstrap5.public_key_hash in let commitment_period = 5 in let challenge_window = 5 in test_full_scenario @@ -5955,7 +5955,6 @@ let bailout_mode_not_publish ~kind = [] ~mode:Bailout in - let* () = Sc_rollup_node.wait_for_ready sc_rollup_node in (* The challenge window is neded to compute the correct number of block before cementation, we also add 2 times of commitment period to make sure no commit are published. *) @@ -5963,6 +5962,11 @@ let bailout_mode_not_publish ~kind = repeat ((2 * commitment_period) + challenge_window) (fun () -> Client.bake_for_and_wait tezos_client) + and* () = + Sc_rollup_node.wait_for + sc_rollup_node + "sc_rollup_node_recover_bond.v0" + (Fun.const (Some ())) in let* _ = Sc_rollup_node.wait_sync sc_rollup_node ~timeout:100. in let* published_commitment_after = @@ -5988,14 +5992,16 @@ let bailout_mode_not_publish ~kind = Check.string ~error_msg:"Last published commitment have been updated." in - let* () = Sc_rollup_node.terminate sc_rollup_node in - Log.info "Client submits the recover_bond operation." ; - let*! () = - Client.Sc_rollup.submit_recover_bond - ~rollup:sc_rollup - ~src:operator - ~staker:operator - tezos_client + Log.info + "The node has submitted the recover_bond operation, and the operator is no \ + longer staked." ; + let* operator_balance = contract_balances ~pkh:operator tezos_client in + let () = + Check.( + (operator_balance.frozen = 0) + int + ~error_msg: + "The operator should not have a stake nor holds a frozen balance.") in unit