diff --git a/manifest/main.ml b/manifest/main.ml index 45520a3f6ef55b8aad1782be7ace9dd109047787..871f2e67cbb4cb024f3e46c3ef5afd2268f16f0a 100644 --- a/manifest/main.ml +++ b/manifest/main.ml @@ -3394,6 +3394,29 @@ end = struct let _baker = daemon "baker" in let _accuser = daemon "accuser" in let _endorser = some_if N.(number <= 011) @@ fun () -> daemon "endorser" in + let rollups = + some_if N.(number >= 013) @@ fun () -> + public_lib + (sf "tezos-rollups-%s" name_dash) + ~path:(sf "src/proto_%s/lib_rollups" name_underscore) + ~synopsis:"Tezos/Protocol: protocol specific library for rollups" + ~deps: + [ + tezos_base |> open_ ~m:"TzPervasives" + |> open_ ~m:"TzPervasives.Error_monad.Legacy_monad_globals" + |> open_; + tezos_crypto |> open_; + main |> open_; + environment |> open_; + tezos_micheline |> open_; + client |> if_some |> open_; + tezos_client_base |> open_; + tezos_workers |> open_; + tezos_shell; + ] + ~inline_tests:ppx_inline_test + ~linkall:true + in let sc_rollup = some_if N.(number >= 013) @@ fun () -> public_lib @@ -3404,6 +3427,7 @@ end = struct ~deps: [ tezos_base |> open_ ~m:"TzPervasives"; + environment |> if_ N.(number >= 014) |> open_; main |> open_; plugin |> if_some |> open_; parameters |> if_some |> open_; @@ -3453,6 +3477,7 @@ end = struct tezos_context_encoding; tezos_context_helpers; main |> open_; + environment |> if_ N.(number >= 014) |> open_; plugin |> if_some |> open_; parameters |> if_some |> open_; tezos_rpc |> open_; @@ -3466,6 +3491,7 @@ end = struct irmin; ringo; ringo_lwt; + rollups |> if_some |> open_; ] in let tx_rollup = @@ -3499,6 +3525,7 @@ end = struct tezos_shell; tezos_store; tezos_workers |> open_; + rollups |> if_some |> open_; ] ~inline_tests:ppx_inline_test ~linkall:true diff --git a/src/proto_013_PtJakart/bin_sc_rollup_node/dune b/src/proto_013_PtJakart/bin_sc_rollup_node/dune index 955c99a2de2e8b60249dfd2b6b0b8841dc881f5d..504a8dd66a99ca0dfffeeb286cb353bf55c05db7 100644 --- a/src/proto_013_PtJakart/bin_sc_rollup_node/dune +++ b/src/proto_013_PtJakart/bin_sc_rollup_node/dune @@ -30,7 +30,8 @@ irmin-pack.unix irmin ringo - ringo-lwt) + ringo-lwt + tezos-rollups-013-PtJakart) (flags (:standard -open Tezos_base.TzPervasives @@ -45,4 +46,5 @@ -open Tezos_protocol_013_PtJakart_parameters -open Tezos_rpc -open Tezos_shell_services - -open Tezos_sc_rollup_013_PtJakart))) + -open Tezos_sc_rollup_013_PtJakart + -open Tezos_rollups_013_PtJakart))) diff --git a/src/proto_013_PtJakart/bin_sc_rollup_node/tezos-sc-rollup-node-013-PtJakart.opam b/src/proto_013_PtJakart/bin_sc_rollup_node/tezos-sc-rollup-node-013-PtJakart.opam index b346c36f25614855c0eaa72197d9197d5fd01432..e38a5bce6f40ecae1377a4978e812263971b6997 100644 --- a/src/proto_013_PtJakart/bin_sc_rollup_node/tezos-sc-rollup-node-013-PtJakart.opam +++ b/src/proto_013_PtJakart/bin_sc_rollup_node/tezos-sc-rollup-node-013-PtJakart.opam @@ -29,6 +29,7 @@ depends: [ "irmin" { >= "3.2.0" & < "3.3.0" } "ringo" { = "0.8" } "ringo-lwt" { = "0.8" } + "tezos-rollups-013-PtJakart" ] build: [ ["dune" "build" "-p" name "-j" jobs] diff --git a/src/proto_013_PtJakart/lib_rollups/.ocamlformat b/src/proto_013_PtJakart/lib_rollups/.ocamlformat new file mode 100644 index 0000000000000000000000000000000000000000..5e1158919e85acc2cdca272c2e521f4d69f1594e --- /dev/null +++ b/src/proto_013_PtJakart/lib_rollups/.ocamlformat @@ -0,0 +1,17 @@ +version=0.18.0 +wrap-fun-args=false +let-binding-spacing=compact +field-space=loose +break-separators=after +space-around-arrays=false +space-around-lists=false +space-around-records=false +space-around-variants=false +dock-collection-brackets=true +space-around-records=false +sequence-style=separator +doc-comments=before +margin=80 +module-item-spacing=sparse +parens-tuple=always +parens-tuple-patterns=always diff --git a/src/proto_013_PtJakart/lib_rollups/dune b/src/proto_013_PtJakart/lib_rollups/dune new file mode 100644 index 0000000000000000000000000000000000000000..f178914b7f80f563a388c12ddf1245e5a0b05981 --- /dev/null +++ b/src/proto_013_PtJakart/lib_rollups/dune @@ -0,0 +1,32 @@ +; This file was automatically generated, do not edit. +; Edit file manifest/main.ml instead. + +(library + (name tezos_rollups_013_PtJakart) + (public_name tezos-rollups-013-PtJakart) + (instrumentation (backend bisect_ppx)) + (libraries + tezos-base + tezos-crypto + tezos-protocol-013-PtJakart + tezos-protocol-013-PtJakart.environment + tezos-micheline + tezos-client-013-PtJakart + tezos-client-base + tezos-workers + tezos-shell) + (inline_tests (flags -verbose) (modes native)) + (preprocess (pps ppx_inline_test)) + (library_flags (:standard -linkall)) + (flags + (:standard + -open Tezos_base.TzPervasives + -open Tezos_base.TzPervasives.Error_monad.Legacy_monad_globals + -open Tezos_base + -open Tezos_crypto + -open Tezos_protocol_013_PtJakart + -open Tezos_protocol_environment_013_PtJakart + -open Tezos_micheline + -open Tezos_client_013_PtJakart + -open Tezos_client_base + -open Tezos_workers))) diff --git a/src/proto_013_PtJakart/lib_rollups/dune-project b/src/proto_013_PtJakart/lib_rollups/dune-project new file mode 100644 index 0000000000000000000000000000000000000000..d895be4582e6aea423cc37b26527af448ded3d08 --- /dev/null +++ b/src/proto_013_PtJakart/lib_rollups/dune-project @@ -0,0 +1,4 @@ +(lang dune 2.9) +(formatting (enabled_for ocaml)) +; This file was automatically generated, do not edit. +; Edit file manifest/manifest.ml instead. diff --git a/src/proto_013_PtJakart/lib_rollups/tezos-rollups-013-PtJakart.opam b/src/proto_013_PtJakart/lib_rollups/tezos-rollups-013-PtJakart.opam new file mode 100644 index 0000000000000000000000000000000000000000..29ca1902d25cb59a96fe5b932295393a7a9f1ff4 --- /dev/null +++ b/src/proto_013_PtJakart/lib_rollups/tezos-rollups-013-PtJakart.opam @@ -0,0 +1,26 @@ +# This file was automatically generated, do not edit. +# Edit file manifest/main.ml instead. +opam-version: "2.0" +maintainer: "contact@tezos.com" +authors: ["Tezos devteam"] +homepage: "https://www.tezos.com/" +bug-reports: "https://gitlab.com/tezos/tezos/issues" +dev-repo: "git+https://gitlab.com/tezos/tezos.git" +license: "MIT" +depends: [ + "dune" { >= "2.9" } + "ppx_inline_test" + "tezos-base" + "tezos-crypto" + "tezos-protocol-013-PtJakart" + "tezos-micheline" + "tezos-client-013-PtJakart" + "tezos-client-base" + "tezos-workers" + "tezos-shell" +] +build: [ + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] +synopsis: "Tezos/Protocol: protocol specific library for rollups" diff --git a/src/proto_013_PtJakart/lib_tx_rollup/dune b/src/proto_013_PtJakart/lib_tx_rollup/dune index b4a4148d3a369d34c2dcd0a412cf24336a72e451..2a7ad72c88c72a1e17726996fad065b0b18daceb 100644 --- a/src/proto_013_PtJakart/lib_tx_rollup/dune +++ b/src/proto_013_PtJakart/lib_tx_rollup/dune @@ -25,7 +25,8 @@ tezos-client-base-unix tezos-shell tezos-store - tezos-workers) + tezos-workers + tezos-rollups-013-PtJakart) (inline_tests (flags -verbose) (modes native)) (preprocess (pps ppx_inline_test)) (library_flags (:standard -linkall)) @@ -48,4 +49,5 @@ -open Tezos_micheline -open Tezos_client_base -open Tezos_client_base_unix - -open Tezos_workers))) + -open Tezos_workers + -open Tezos_rollups_013_PtJakart))) diff --git a/src/proto_013_PtJakart/lib_tx_rollup/tezos-tx-rollup-013-PtJakart.opam b/src/proto_013_PtJakart/lib_tx_rollup/tezos-tx-rollup-013-PtJakart.opam index c6aae8b32aaaadfb6b858b6a0a1b8e8ab84f240a..803765aa36462749cd4236a38d7f844d5f27f249 100644 --- a/src/proto_013_PtJakart/lib_tx_rollup/tezos-tx-rollup-013-PtJakart.opam +++ b/src/proto_013_PtJakart/lib_tx_rollup/tezos-tx-rollup-013-PtJakart.opam @@ -29,6 +29,7 @@ depends: [ "tezos-shell" "tezos-store" "tezos-workers" + "tezos-rollups-013-PtJakart" ] build: [ ["dune" "build" "-p" name "-j" jobs] diff --git a/src/proto_alpha/bin_sc_rollup_client/RPC.ml b/src/proto_alpha/bin_sc_rollup_client/RPC.ml index b61d72d2f07fed2711007595023bcc895786ac71..352deae23a1868de2ce31c708aa59af31951d9d4 100644 --- a/src/proto_alpha/bin_sc_rollup_client/RPC.ml +++ b/src/proto_alpha/bin_sc_rollup_client/RPC.ml @@ -29,3 +29,6 @@ let call (cctxt : #sc_client_context) = cctxt#call_service let get_sc_rollup_addresses_command cctxt = call cctxt (Sc_rollup_services.sc_rollup_address ()) () () () + +let publish_message cctxt (message : Sc_rollup_messages.t) = + call cctxt (Sc_rollup_services.inject_message ()) () () message diff --git a/src/proto_alpha/bin_sc_rollup_client/commands.ml b/src/proto_alpha/bin_sc_rollup_client/commands.ml index 4750ef242393223c0b9038d1107c9de02d1b1dcb..d0c60f8fd89d9a9b4847a3361dabcd0bf5a0022b 100644 --- a/src/proto_alpha/bin_sc_rollup_client/commands.ml +++ b/src/proto_alpha/bin_sc_rollup_client/commands.ml @@ -26,6 +26,30 @@ open Clic open Protocol.Alpha_context +type tz4_account = + Aggregate_signature.public_key_hash + * Aggregate_signature.public_key + * Aggregate_signature.secret_key + +let sign_message (account : tz4_account) message = + let (_, Aggregate_signature.Bls12_381 pk, Aggregate_signature.Bls12_381 sk) = + account + in + (* FIXME: encoding - decoding to go from one type to another is not really + efficient. Any other solution? *) + let pk = + Data_encoding.Binary.to_bytes_exn Bls.Public_key.encoding pk + |> Bls12_381.Signature.MinPk.pk_of_bytes_exn + in + let sk = + Data_encoding.Binary.to_bytes_exn Bls.Secret_key.encoding sk + |> Bls12_381.Signature.sk_of_bytes_exn + in + let signature = + Bls12_381.Signature.MinPk.Aug.sign sk (Bytes.of_string message) + in + (Sc_rollup_messages.{signer = pk; contents = message}, signature) + let get_sc_rollup_addresses_command () = command ~desc: @@ -36,6 +60,37 @@ let get_sc_rollup_addresses_command () = RPC.get_sc_rollup_addresses_command cctxt >>=? fun addr -> cctxt#message "@[%a@]" Sc_rollup.Address.pp addr >>= fun () -> return_unit) +let publish_message () = + command + ~desc:"Publish a message to the rollup node" + no_options + (prefixes ["publish"; "message"] + @@ param ~name:"message" ~desc:"" (parameter (fun _ s -> return s)) + @@ stop) + (fun () message (cctxt : #Configuration.sc_client_context) -> + let open Lwt_result_syntax in + let (message, signature) = + sign_message Configuration.dummy_bls_account message + in + let message = + Sc_rollup_messages. + {messages = [message]; aggregated_signatures = signature} + in + let* (hash, state_hash, status, ticks_diff) = + RPC.publish_message cctxt message + in + let*! () = + cctxt#message + "@[Operation hash: %a\nState hash: %s\nStatus: %s\nTicks diff: %a@]" + Sc_rollup_messages.Hash.pp + hash + state_hash + status + Z.pp_print + ticks_diff + in + return_unit) + (** [display_answer cctxt answer] prints an RPC answer. *) let display_answer (cctxt : #Configuration.sc_client_context) : RPC_context.generic_call_result -> unit Lwt.t = function @@ -143,6 +198,7 @@ end let all () = [ get_sc_rollup_addresses_command (); + publish_message (); rpc_get_command; Keys.generate_keys (); Keys.list_keys (); diff --git a/src/proto_alpha/bin_sc_rollup_client/configuration.ml b/src/proto_alpha/bin_sc_rollup_client/configuration.ml index e5cd10551cee36101ded144d015f47327149c195..73a9d64835ff43652b1ae8dc0b93eb9a9c5f5305 100644 --- a/src/proto_alpha/bin_sc_rollup_client/configuration.ml +++ b/src/proto_alpha/bin_sc_rollup_client/configuration.ml @@ -111,3 +111,14 @@ let make_unix_client_context {base_dir; endpoint} = {Tezos_rpc_http_client_unix.RPC_client_unix.default_config with endpoint} in new unix_sc_client_context ~base_dir ~rpc_config ~password_filename:None + +(* taken from `src/lib_crypto/test/key_encoding_vectors`, first seed from + bls12_381_key_encodings *) +let dummy_bls_account = + let seed = + Cstruct.( + to_bytes + (of_hex + "72fb8a8eec04f982f2da16b99b6a04fe267c9354c3423939b4b8bf956d6ebb90")) + in + Aggregate_signature.generate_key ~seed () diff --git a/src/proto_alpha/bin_sc_rollup_client/configuration.mli b/src/proto_alpha/bin_sc_rollup_client/configuration.mli index 9aebfc3ba4b741d568d529020d5e650b630004d5..f5fcfb65ecbdfa89acbaeda9c329c62c2d70fbad 100644 --- a/src/proto_alpha/bin_sc_rollup_client/configuration.mli +++ b/src/proto_alpha/bin_sc_rollup_client/configuration.mli @@ -62,3 +62,12 @@ class unix_sc_client_context : (** [make_unix_client_context config] generates a unix_sc_client_context from the client configuration. *) val make_unix_client_context : t -> unix_sc_client_context + +(** TODO: remove once https://gitlab.com/tezos/tezos/-/merge_requests/4741 has + been merged, only used for tests. The keys are extracted from + `src/lib_crypto/test/key_encoding_vectors`, first seed from + bls12_381_key_encodings *) +val dummy_bls_account : + Aggregate_signature.Public_key_hash.t + * Aggregate_signature.Public_key.t + * Aggregate_signature.Secret_key.t 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 0e7bf48aa02c6937cf9bed0c3a30fab114e5c40f..47218ea7238ac486f1f661e99054d48619be88c8 100644 --- a/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml +++ b/src/proto_alpha/bin_sc_rollup_node/RPC_server.ml @@ -119,21 +119,30 @@ end module type S = sig module PVM : Pvm.S + module Batcher : Batcher.S with module PVM = PVM + val shutdown : RPC_server.server -> unit Lwt.t val register : - Node_context.t -> PVM.context -> Configuration.t -> unit RPC_directory.t + Node_context.t -> + PVM.context -> + Configuration.t -> + Batcher.t -> + unit RPC_directory.t val start : Node_context.t -> PVM.context -> Configuration.t -> + Batcher.t -> RPC_server.server tzresult Lwt.t end -module Make (PVM : Pvm.S) : S with module PVM = PVM = struct +module Make (PVM : Pvm.S) (Batcher : Batcher.S with module PVM = PVM) : + S with module PVM = PVM and module Batcher = Batcher = struct include Common module PVM = PVM + module Batcher = Batcher let register_current_total_ticks store dir = RPC_directory.register0 @@ -191,7 +200,27 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct let*! status = PVM.get_status state in return (PVM.string_of_status status)) - let register node_ctxt store configuration = + let register_inject_messages node_ctxt store batcher dir = + RPC_directory.register + dir + (Sc_rollup_services.inject_message ()) + (fun () () message -> + let open Lwt_result_syntax in + let+ (hash, {state_hash; status; ticks_diff}) = + Batcher.register_message node_ctxt store batcher message + in + ( hash, + Protocol.Alpha_context.Sc_rollup.State_hash.to_b58check state_hash, + PVM.string_of_status status, + ticks_diff )) + + let register_batcher_messages batcher dir = + RPC_directory.register + dir + (Sc_rollup_services.batcher_messages ()) + (fun () () () -> Lwt.return_ok @@ Batcher.to_list batcher) + + let register node_ctxt store configuration batcher = RPC_directory.empty |> register_sc_rollup_address configuration |> register_current_tezos_head store @@ -203,7 +232,9 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct |> register_current_status store |> register_last_stored_commitment store |> register_last_published_commitment store + |> register_inject_messages node_ctxt store batcher + |> register_batcher_messages batcher - let start node_ctxt store configuration = - Common.start configuration (register node_ctxt store configuration) + let start node_ctxt store configuration batcher = + Common.start configuration (register node_ctxt store configuration batcher) end diff --git a/src/proto_alpha/bin_sc_rollup_node/batcher.ml b/src/proto_alpha/bin_sc_rollup_node/batcher.ml new file mode 100644 index 0000000000000000000000000000000000000000..0925e4ebb9596d255cd766c6475dcbb1440303cc --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/batcher.ml @@ -0,0 +1,251 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Protocol.Alpha_context +open Sc_rollup_messages + +type batch = Sc_rollup_messages.t + +module Queue = Hash_queue.Make (Hash) (Sc_rollup_messages) + +type error += Full_queue | Signature_mismatch + +let () = + register_error_kind + `Temporary + ~id:"sc_rollup.node.full_queue" + ~title:"The node's message queue is full" + ~description: + "An message could not be registered since the queue of the node's \ + batcher is full." + ~pp:(fun ppf () -> + Format.fprintf + ppf + "An message could not be registered since the queue of the node's \ + batcher is full.") + Data_encoding.unit + (function Full_queue -> Some () | _ -> None) + (fun () -> Full_queue) ; + register_error_kind + `Temporary + ~id:"sc_rollup.node.signature_mismatch" + ~title:"A signature is invalid" + ~description: + "A signature does not correspond to its associated message and signer." + ~pp:(fun ppf () -> + Format.fprintf + ppf + "A signature does not correspond to its associated message and signer.") + Data_encoding.unit + (function Signature_mismatch -> Some () | _ -> None) + (fun () -> Signature_mismatch) + +module type S = sig + module PVM : Pvm.S + + module Interpreter : Interpreter.S with module PVM = PVM + + (** Internal type of the batcher *) + type t + + (** [init ~capacity ~maximum_batch_size ()] creates a new batcher that can hold + a maximum of [capacity] messages and produces batches of + [maximum_batch_size] messages. *) + val init : ?capacity:int -> ?maximum_batch_size:int -> unit -> t Lwt.t + + (** [register_message node_ctxt store batcher message] registers a given + message into the batcher queue. Fails with [Full_queue] if the queue holds + as much message as its capacity. *) + val register_message : + Node_context.t -> + Store.t -> + t -> + batch -> + (Sc_rollup_messages.Hash.t * Interpreter.simulation_result) tzresult Lwt.t + + (** [to_list batcher] returns the list of messages held by the batcher from + oldest to newest. *) + val to_list : t -> Sc_rollup_messages.full list + + (** [take_and_publish node_ctxt batcher] takes at most [maximum_batch_size] from + the batcher, build the corresponding batch and publish it to the L1 using + the operator signature. Does nothing if the queue is empty. *) + val take_and_publish : + Node_context.t -> + Store.t -> + t -> + (Sc_rollup_messages.Hash.t list * Interpreter.simulation_result) option + tzresult + Lwt.t + + (** [process_head node_ctxt batcher] publishes a new batch each time a + new head is pushed on L1. *) + val process_head : Node_context.t -> Store.t -> t -> unit tzresult Lwt.t +end + +module Make (PVM : Pvm.S) : S with module PVM = PVM = struct + module PVM = PVM + module Interpreter = Interpreter.Make (PVM) + + type t = {queue : Queue.t; maximum_batch_size : int} + + (* FIXME: to determine *) + let default_capacity = 100 + + (* FIXME: to determine, a batch size should not probably in terms of messages + but effective size of the messages *) + let default_batch_size = 10 + + let init ?(capacity = default_capacity) + ?(maximum_batch_size = default_batch_size) () = + let open Lwt_syntax in + let+ () = Batcher_event.starting () in + {queue = Queue.create capacity; maximum_batch_size} + + let check_message batch = + let messages = + List.map + (fun {signer; contents} -> (signer, Bytes.of_string contents)) + batch.messages + in + Environment.Bls_signature.aggregate_verify + messages + batch.aggregated_signatures + + let register_message node_ctxt store batcher batch = + let open Lwt_result_syntax in + if + Queue.length batcher.queue + >= Queue.capacity batcher.queue + List.length batch.messages + then tzfail Full_queue + else if not (check_message batch) then tzfail Signature_mismatch + else + let messages = List.map (fun {contents; _} -> contents) batch.messages in + let* evaluation_result = Interpreter.simulate node_ctxt store messages in + let hash = hash batch in + Queue.replace batcher.queue hash batch ; + let*! () = Batcher_event.register_messages ~hash in + return (hash, evaluation_result) + + let to_list batcher = + Queue.fold (fun hash batch acc -> {hash; batch} :: acc) batcher.queue [] + |> List.rev + + let aggregate aggregated signature = + match aggregated with + | Some aggregated -> + Environment.Bls_signature.aggregate_signature_opt + [aggregated; signature] + | None -> Some signature + + let build_batch t = + (* Allows exiting the fold early *) + let exception + Stop of (Sc_rollup_messages.message list * signature option * Hash.t list) + in + let (rev_messages, aggregated_signature, hashes) = + try + let (msgs, sgs, hs, _) = + Queue.fold + (fun hash + {messages; aggregated_signatures} + (rev_messages, prev_aggregation, hashes, max_remaining) -> + let messages_number = List.length messages in + if messages_number > max_remaining then + raise (Stop (rev_messages, prev_aggregation, hashes)) + else + (* Signatures are aggregated at this point, as such if one + signature makes the batch invalid, we can drop it. This works + thanks to the associativity of the signature aggregation. *) + let aggregation_result = + aggregate prev_aggregation aggregated_signatures + in + (* If the aggregation is invalid, we can continue and skip the + current batch in the queue. *) + let (messages, aggregation) = + Option.fold + ~some:(fun s -> + (List.rev_append messages rev_messages, Some s)) + ~none:(rev_messages, prev_aggregation) + aggregation_result + in + ( messages, + aggregation, + hash :: hashes, + (* The hash is always added to remove the message from + the queue whether it is valid or not *) + max_remaining - messages_number )) + t.queue + ([], None, [], t.maximum_batch_size) + in + (msgs, sgs, hs) + with Stop res -> res + in + ( Option.map + (fun aggregated_signatures -> + {messages = List.rev rev_messages; aggregated_signatures}) + aggregated_signature, + hashes ) + + let publish node_ctxt store messages = + let open Lwt_result_syntax in + let* evaluation_result = Interpreter.simulate node_ctxt store messages in + let operation = + Sc_rollup_add_messages {rollup = node_ctxt.rollup_address; messages} + in + let+ () = + Injector.add_pending_operation ~source:node_ctxt.operator operation + in + evaluation_result + + (* FIXME: to remove once the protocol accepts batches with signatures *) + let temporary_extract_messages batch = + List.map (fun {contents; _} -> contents) batch.messages + + let remove_sent_messages batcher hashes = + List.iter (Queue.remove batcher.queue) hashes + + let take_and_publish node_ctxt store batcher = + let open Lwt_result_syntax in + (* FIXME: should we send batched manager operations if there remains messages in the queue? *) + let (batch, hashes) = build_batch batcher in + match batch with + | None -> return_none + | Some batch -> + (* FIXME: The protocol does not expect signed messages yet *) + let* evaluation_result = + publish node_ctxt store (temporary_extract_messages batch) + in + let*! () = + Batcher_event.publish_sc_rollup_messages ~msgs:(List.length hashes) + in + return_some (hashes, evaluation_result) + + let process_head node_ctxt store batcher = + let open Lwt_result_syntax in + let* res = take_and_publish node_ctxt store batcher in + Option.iter (fun (hashes, _) -> remove_sent_messages batcher hashes) res ; + return_unit +end diff --git a/src/proto_alpha/bin_sc_rollup_node/batcher.mli b/src/proto_alpha/bin_sc_rollup_node/batcher.mli new file mode 100644 index 0000000000000000000000000000000000000000..0255cea2d0a9e67e027b875bf7b75910e8c2742a --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/batcher.mli @@ -0,0 +1,80 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** The rollup node can accumulate layer 2 messages, batch and send them to the + layer 1. + + The batcher queue is contained in memory. +*) + +type error += Full_queue | Signature_mismatch + +type batch = Sc_rollup_messages.t + +module type S = sig + module PVM : Pvm.S + + module Interpreter : Interpreter.S with module PVM = PVM + + (** Internal type of the batcher *) + type t + + (** [init ~capacity ~maximum_batch_size ()] creates a new batcher that can hold + a maximum of [capacity] messages and produces batches of + [maximum_batch_size] messages. *) + val init : ?capacity:int -> ?maximum_batch_size:int -> unit -> t Lwt.t + + (** [register_message node_ctxt store batcher message] registers a given + message into the batcher queue. Fails with [Full_queue] if the queue holds + as much message as its capacity. *) + val register_message : + Node_context.t -> + Store.t -> + t -> + batch -> + (Sc_rollup_messages.Hash.t * Interpreter.simulation_result) tzresult Lwt.t + + (** [to_list batcher] returns the list of messages held by the batcher from + oldest to newest. *) + val to_list : t -> Sc_rollup_messages.full list + + (** [take_and_publish node_ctxt store batcher] takes at most + [maximum_batch_size] from the batcher, build the corresponding batch and + publish it to the L1 using the operator signature. Does nothing if the + queue is empty. *) + val take_and_publish : + Node_context.t -> + Store.t -> + t -> + (Sc_rollup_messages.Hash.t list * Interpreter.simulation_result) option + tzresult + Lwt.t + + (** [process_head node_ctxt store batcher] publishes a new batch each + time a new head is pushed on L1. *) + val process_head : Node_context.t -> Store.t -> t -> unit tzresult Lwt.t +end + +module Make (PVM : Pvm.S) : S with module PVM = PVM diff --git a/src/proto_alpha/bin_sc_rollup_node/batcher_event.ml b/src/proto_alpha/bin_sc_rollup_node/batcher_event.ml new file mode 100644 index 0000000000000000000000000000000000000000..4efaf33cdc795862d9e4f62941addad6eed5663b --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/batcher_event.ml @@ -0,0 +1,71 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +module Simple = struct + include Internal_event.Simple + + let section = ["sc_rollup_node"; "batcher"] + + let starting = + declare_0 + ~section + ~name:"sc_rollup_node_batcher_starting" + ~msg:"Starting batcher of the smart contract rollup node" + ~level:Notice + () + + let stopping = + declare_0 + ~section + ~name:"sc_rollup_node_batcher_stopping" + ~msg:"Stopping batcher of the smart contract rollup node" + ~level:Notice + () + + let register_messages = + declare_1 + ~section + ~name:"sc_rollup_node_register_messages" + ~msg:"adding {msg_hash} to queue" + ~level:Notice + ("msg_hash", Sc_rollup_messages.Hash.encoding) + + let publish_sc_rollup_messages = + declare_1 + ~section + ~name:"publish" + ~msg:"layer 1 operation with {msgs} messages added for injection" + ~level:Notice + ("msgs", Data_encoding.int31) +end + +let starting = Simple.(emit starting) + +let stopping = Simple.(emit stopping) + +let register_messages ~hash = Simple.(emit register_messages hash) + +let publish_sc_rollup_messages ~msgs = + Simple.(emit publish_sc_rollup_messages msgs) diff --git a/src/proto_alpha/bin_sc_rollup_node/batcher_event.mli b/src/proto_alpha/bin_sc_rollup_node/batcher_event.mli new file mode 100644 index 0000000000000000000000000000000000000000..f96ee5bea4749a1810d90d81f14b39c435f98cbd --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/batcher_event.mli @@ -0,0 +1,57 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +module Simple : sig + type 'a event := 'a Internal_event.Simple.t + + val section : string list + + (** Event for batcher initialization. *) + val starting : unit event + + (** Event for batcher interruption. *) + val stopping : unit event + + (** Event triggered when the batcher receives a message, with the L2 operation + hash. *) + val register_messages : Sc_rollup_messages.Hash.t event + + (** Event triggered when the batcher sends a message to the L1 node, with the + operation hash. *) + val publish_sc_rollup_messages : int event +end + +(** [starting ()] triggers a [Simple.starting] event. *) +val starting : unit -> unit Lwt.t + +(** [stopping ()] triggers a [Simple.stopping] event. *) +val stopping : unit -> unit Lwt.t + +(** [register_messages ~hash] triggers a [Simple.register_messages] event. *) +val register_messages : hash:Sc_rollup_messages.Hash.t -> unit Lwt.t + +(** [publish_sc_rollup_messages ~hash] triggers a + [Simple.publish_sc_rollup_messages] event. *) +val publish_sc_rollup_messages : msgs:int -> unit Lwt.t diff --git a/src/proto_alpha/bin_sc_rollup_node/commitment.ml b/src/proto_alpha/bin_sc_rollup_node/commitment.ml index 66b093103e890fc8af35b16099fe11b3e06f17ba..5cd6fab27c90f0615280163db1d3d4c94e3e226c 100644 --- a/src/proto_alpha/bin_sc_rollup_node/commitment.ml +++ b/src/proto_alpha/bin_sc_rollup_node/commitment.ml @@ -23,19 +23,19 @@ (* *) (*****************************************************************************) -(** The rollup node stores and publishes commitments for the PVM +(** The rollup node stores and publishes commitments for the PVM every 20 levels. - Every time a finalized block is processed by the rollup node, - the latter determines whether the last commitment that the node - has produced referred to 20 blocks earlier. In this case, it - computes and stores a new commitment in a level-indexed map. + Every time a finalized block is processed by the rollup node, + the latter determines whether the last commitment that the node + has produced referred to 20 blocks earlier. In this case, it + computes and stores a new commitment in a level-indexed map. - Stored commitments are signed by the rollup node operator - and published on the layer1 chain. To ensure that commitments - produced by the rollup node are eventually published, - storing and publishing commitments are decoupled. Every time - a new head is processed, the node tries to publish the oldest + Stored commitments are signed by the rollup node operator + and published on the layer1 chain. To ensure that commitments + produced by the rollup node are eventually published, + storing and publishing commitments are decoupled. Every time + a new head is processed, the node tries to publish the oldest commitment that was not published already. *) @@ -45,9 +45,9 @@ open Alpha_context module type Mutable_level_store = Store.Mutable_value with type value = Raw_level.t -(* We keep the number of messages and ticks to be included in the - next commitment in memory. Note that we do not risk to increase - these counters when the wrong branch is tracked by the rollup +(* We keep the number of messages and ticks to be included in the + next commitment in memory. Note that we do not risk to increase + these counters when the wrong branch is tracked by the rollup node, as only finalized heads are processed to build commitments. *) @@ -240,8 +240,6 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct let* () = update_ticks_and_messages store hash in store_commitment_if_necessary ~origination_level store current_level hash - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2869 - use the Injector to publish commitments. *) let publish_commitment (node_ctxt : Node_context.t) store = let origination_level = node_ctxt.initial_level in let open Lwt_result_syntax in @@ -257,42 +255,15 @@ module Make (PVM : Pvm.S) : S with module PVM = PVM = struct in if is_commitment_available then let*! commitment = Store.Commitments.get store next_level_to_publish in - let cctxt = node_ctxt.cctxt in - let sc_rollup_address = node_ctxt.rollup_address in - let fee_parameter = node_ctxt.fee_parameter in - let* (source, src_pk, src_sk) = - Node_context.get_operator_keys node_ctxt + let rollup = node_ctxt.rollup_address in + let publish_operation = Sc_rollup_publish {rollup; commitment} in + let* () = + Injector.add_pending_operation + ~source:node_ctxt.operator + publish_operation in - let* (_, _, Manager_operation_result {operation_result; _}) = - Client_proto_context.sc_rollup_publish - cctxt - ~chain:cctxt#chain - ~block:cctxt#block - ~commitment - ~source - ~rollup:sc_rollup_address - ~src_pk - ~src_sk - ~fee_parameter - () - in - let open Apply_results in let*! () = - match operation_result with - | Applied (Sc_rollup_publish_result _) -> - let open Lwt_syntax in - let* () = - Store.Last_published_commitment_level.set - store - commitment.inbox_level - in - Commitment_event.commitment_published commitment - | Failed (Sc_rollup_publish_manager_kind, _errors) -> - Commitment_event.commitment_failed commitment - | Backtracked (Sc_rollup_publish_result _, _errors) -> - Commitment_event.commitment_backtracked commitment - | Skipped Sc_rollup_publish_manager_kind -> - Commitment_event.commitment_skipped commitment + Store.Last_published_commitment_level.set store commitment.inbox_level in return_unit else return_unit diff --git a/src/proto_alpha/bin_sc_rollup_node/components.ml b/src/proto_alpha/bin_sc_rollup_node/components.ml index e6cb34157926154f1eb8bba8abf7c7b3dd2b833f..86fa41923ce6d674166f2d283479c2394ea5eb77 100644 --- a/src/proto_alpha/bin_sc_rollup_node/components.ml +++ b/src/proto_alpha/bin_sc_rollup_node/components.ml @@ -30,14 +30,18 @@ module type S = sig module Commitment : Commitment.S with module PVM = PVM - module RPC_server : RPC_server.S with module PVM = PVM + module Batcher : Batcher.S with module PVM = PVM + + module RPC_server : + RPC_server.S with module PVM = PVM and module Batcher = Batcher end module Make (PVM : Pvm.S) : S with module PVM = PVM = struct module PVM = PVM module Interpreter = Interpreter.Make (PVM) module Commitment = Commitment.Make (PVM) - module RPC_server = RPC_server.Make (PVM) + module Batcher = Batcher.Make (PVM) + module RPC_server = RPC_server.Make (PVM) (Batcher) end let pvm_of_kind : Protocol.Alpha_context.Sc_rollup.Kind.t -> (module Pvm.S) = diff --git a/src/proto_alpha/bin_sc_rollup_node/daemon.ml b/src/proto_alpha/bin_sc_rollup_node/daemon.ml index cb55f4b33aaea639f639a5ff87aeff8c2da6ac33..b08425c1c70ff067a7fc82d12fa2fad80a1d39db 100644 --- a/src/proto_alpha/bin_sc_rollup_node/daemon.ml +++ b/src/proto_alpha/bin_sc_rollup_node/daemon.ml @@ -63,7 +63,7 @@ let categorise_heads (node_ctxt : Node_context.t) old_heads new_heads = module Make (PVM : Pvm.S) = struct module Components = Components.Make (PVM) - let process_head node_ctxt store head_state = + let process_head node_ctxt store batcher head_state = (* Because we keep track of finalized heads using transaction finality time, rather than block finality time, it is possible that heads with the same level are processed as finalized. Individual modules that process heads @@ -79,7 +79,8 @@ module Make (PVM : Pvm.S) = struct let* () = Inbox.process_head node_ctxt store head in (* Avoid storing and publishing commitments if the head is not final *) (* Avoid triggering the pvm execution if this has been done before for this head *) - Components.Interpreter.process_head node_ctxt store head + let* () = Components.Interpreter.process_head node_ctxt store head in + Components.Batcher.process_head node_ctxt store batcher in let* () = if finalized then Components.Commitment.process_head node_ctxt store head @@ -88,7 +89,18 @@ module Make (PVM : Pvm.S) = struct (* Publishing a commitment when one is available does not depend on the state of the current head, but we still need to ensure that the node only published one commitment per block. *) - Components.Commitment.publish_commitment node_ctxt store + let* () = Components.Commitment.publish_commitment node_ctxt store in + let*! () = Injector.inject () in + return_unit + + let notify_injector l1_ctxt store chain_event = + let open Lwt_result_syntax in + let open Layer1 in + let hash = chain_event_head_hash chain_event in + let* head = fetch_tezos_block l1_ctxt hash in + let* reorg = get_tezos_reorg_for_new_head l1_ctxt store hash in + let*! () = Injector.new_tezos_head head reorg in + return_unit (* [on_layer_1_chain_event node_ctxt store chain_event old_heads] processes a list of heads, coming from either a list of [old_heads] or from the current @@ -104,9 +116,11 @@ module Make (PVM : Pvm.S) = struct needs to be returned to be included as the rollup node started tracking a new branch. *) - let on_layer_1_chain_event node_ctxt store chain_event old_heads = + let on_layer_1_chain_event l1_ctxt node_ctxt store batcher chain_event + old_heads = let open Lwt_result_syntax in let open Layer1 in + let* () = notify_injector l1_ctxt store chain_event in let* non_final_heads = match chain_event with | SameBranch {new_head; intermediate_heads} -> @@ -116,7 +130,9 @@ module Make (PVM : Pvm.S) = struct old_heads (intermediate_heads @ [new_head]) in - let* () = List.iter_es (process_head node_ctxt store) head_states in + let* () = + List.iter_es (process_head node_ctxt store batcher) head_states + in (* Return new_head to be processed as finalized head if the next chain event is of type SameBranch. *) @@ -149,6 +165,7 @@ module Make (PVM : Pvm.S) = struct process_head node_ctxt store + batcher {head; finalized = true; seen_before = true}) final_heads in @@ -168,10 +185,9 @@ module Make (PVM : Pvm.S) = struct in go [] - let daemonize node_ctxt store layer_1_chain_events = - Lwt.no_cancel - @@ iter_stream layer_1_chain_events - @@ on_layer_1_chain_event node_ctxt store + let daemonize node_ctxt store (l1_ctxt : Layer1.t) batcher = + Lwt.no_cancel @@ iter_stream l1_ctxt.events + @@ on_layer_1_chain_event l1_ctxt node_ctxt store batcher let install_finalizer store rpc_server = let open Lwt_syntax in @@ -184,14 +200,24 @@ module Make (PVM : Pvm.S) = struct let run node_ctxt configuration store = let open Lwt_result_syntax in let start () = + let*! batcher = Components.Batcher.init () in let* rpc_server = - Components.RPC_server.start node_ctxt store configuration - in - let* tezos_heads = - Layer1.start configuration node_ctxt.Node_context.cctxt store + Components.RPC_server.start node_ctxt store configuration batcher in + let* l1_ctxt = Layer1.start configuration node_ctxt store in let*! () = Inbox.start () in let*! () = Components.Commitment.start () in + let* () = + Injector.init + node_ctxt.cctxt + node_ctxt + ~signers: + [ + ( node_ctxt.operator, + `Each_block, + [Injector.Publish; Injector.Add_messages] ); + ] + in let _ = install_finalizer store rpc_server in let*! () = @@ -199,7 +225,7 @@ module Make (PVM : Pvm.S) = struct ~rpc_addr:configuration.rpc_addr ~rpc_port:configuration.rpc_port in - daemonize node_ctxt store tezos_heads + daemonize node_ctxt store l1_ctxt batcher in start () end diff --git a/src/proto_alpha/bin_sc_rollup_node/dune b/src/proto_alpha/bin_sc_rollup_node/dune index 9c76ba42e6b61252e12e48638fed0c51b81aa59c..88da530b71dba94eff646318858a3ef220125c5e 100644 --- a/src/proto_alpha/bin_sc_rollup_node/dune +++ b/src/proto_alpha/bin_sc_rollup_node/dune @@ -18,6 +18,7 @@ tezos-context.encoding tezos-context.helpers tezos-protocol-alpha + tezos-protocol-alpha.environment tezos-protocol-plugin-alpha tezos-protocol-alpha-parameters tezos-rpc @@ -30,7 +31,8 @@ irmin-pack.unix irmin ringo - ringo-lwt) + ringo-lwt + tezos-rollups-alpha) (flags (:standard -open Tezos_base.TzPervasives @@ -41,8 +43,10 @@ -open Tezos_client_base_unix -open Tezos_client_alpha -open Tezos_protocol_alpha + -open Tezos_protocol_environment_alpha -open Tezos_protocol_plugin_alpha -open Tezos_protocol_alpha_parameters -open Tezos_rpc -open Tezos_shell_services - -open Tezos_sc_rollup_alpha))) + -open Tezos_sc_rollup_alpha + -open Tezos_rollups_alpha))) diff --git a/src/proto_alpha/bin_sc_rollup_node/injector.ml b/src/proto_alpha/bin_sc_rollup_node/injector.ml new file mode 100644 index 0000000000000000000000000000000000000000..6da20a5bfe82d56c97d3f4f15699ac5f2436eee2 --- /dev/null +++ b/src/proto_alpha/bin_sc_rollup_node/injector.ml @@ -0,0 +1,113 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Protocol.Alpha_context +open Injector_sigs + +type tag = Publish | Add_messages | Cement + +module Parameters : + PARAMETERS with type rollup_node_state = Node_context.t and type Tag.t = tag = +struct + type rollup_node_state = Node_context.t + + let events_section = ["sc_rollup"] + + module Tag : TAG with type t = tag = struct + type t = tag + + let compare = Stdlib.compare + + let string_of_tag = function + | Publish -> "publish" + | Add_messages -> "add_messages" + | Cement -> "cement" + + let pp ppf t = Format.pp_print_string ppf (string_of_tag t) + + let encoding : t Data_encoding.t = + let open Data_encoding in + string_enum + (List.map + (fun t -> (string_of_tag t, t)) + [Publish; Add_messages; Cement]) + end + + (* TODO: to determine + Very coarse approximation for the number of operation we + expect for each block *) + let table_estimated_size : Tag.t -> int = function + | Publish -> 1 + | Add_messages -> 100 + | Cement -> 1 + + let fee_parameter {Node_context.fee_parameter; _} _ = fee_parameter + + (* Below are dummy values that are only used to approximate the + size. It is thus important that they remain above the real + values if we want the computed size to be an over_approximation + (without having to do a simulation first). *) + (* TODO: + See TORU issue: https://gitlab.com/tezos/tezos/-/issues/2812 + check the size, or compute them wrt operation kind *) + let approximate_fee_bound _ _ = + { + fee = Tez.of_mutez_exn 3_000_000L; + counter = Z.of_int 500_000; + gas_limit = Gas.Arith.integral_of_int_exn 500_000; + storage_limit = Z.of_int 500_000; + } + + (* TODO: Decide if some batches must have all the operation to succeed. See + {!Injector_sigs.Parameter.batch_must_succeed}. *) + let batch_must_succeed _ = `At_least_one + + let ignore_failing_operation : + type kind. + kind manager_operation -> [`Ignore_keep | `Ignore_drop | `Don't_ignore] = + function + | _ -> `Don't_ignore + + (** Returns [true] if an included operation should be re-queued for injection + when the block in which it is included is reverted (due to a + reorganization). *) + let requeue_reverted_operation (type kind) _ + (operation : kind manager_operation) = + let open Lwt_syntax in + match operation with + | Sc_rollup_publish _ -> + (* Commitments are always produced on finalized blocks. They don't need + to be recomputed. *) + return_true + | Sc_rollup_cement _ -> + (* TODO *) + return_true + | Sc_rollup_add_messages _ -> + (* TODO *) + return_true + | _ -> return_true +end + +include Injector_functor.Make (Parameters) diff --git a/src/proto_alpha/bin_sc_rollup_node/interpreter.ml b/src/proto_alpha/bin_sc_rollup_node/interpreter.ml index ba769393f1af90ec3d5ecc4f1e54b7249581ead9..d6ff62a042faa1a26bf76171a1c9409eb8b628d4 100644 --- a/src/proto_alpha/bin_sc_rollup_node/interpreter.ml +++ b/src/proto_alpha/bin_sc_rollup_node/interpreter.ml @@ -28,6 +28,19 @@ open Alpha_context module Inbox = Store.Inbox module type S = sig + module PVM : Pvm.S + + type simulation_result = { + state_hash : PVM.hash; + status : PVM.status; + ticks_diff : Z.t; + } + + (** [simulate node_ctxt store messages] interprets a list of messages and + checks its applicability. *) + val simulate : + Node_context.t -> Store.t -> string list -> simulation_result tzresult Lwt.t + (** [process_head node_ctxt store head] interprets the messages associated with a [head] from a chain [event]. This requires the inbox to be updated beforehand. *) @@ -35,9 +48,15 @@ module type S = sig Node_context.t -> Store.t -> Layer1.head -> unit tzresult Lwt.t end -module Make (PVM : Pvm.S) : S = struct +module Make (PVM : Pvm.S) : S with module PVM = PVM = struct module PVM = PVM + type simulation_result = { + state_hash : PVM.hash; + status : PVM.status; + ticks_diff : Z.t; + } + (** [eval_until_input state] advances a PVM [state] until it wants more inputs. *) let eval_until_input state = let open Lwt_syntax in @@ -61,13 +80,11 @@ module Make (PVM : Pvm.S) : S = struct let* state = eval_until_input state in return state - (** [transition_pvm node_ctxt store predecessor_hash hash] runs a PVM at the previous state from block - [predecessor_hash] by consuming as many messages as possible from block [hash]. *) - let transition_pvm node_ctxt store predecessor_hash hash = - let open Node_context in + let evaluate (node_ctxt : Node_context.t) store current_hash inbox_level + messages = let open Lwt_result_syntax in (* Retrieve the previous PVM state from store. *) - let*! predecessor_state = Store.PVMState.find store predecessor_hash in + let*! predecessor_state = Store.PVMState.find store current_hash in let* predecessor_state = match predecessor_state with | None -> @@ -88,11 +105,6 @@ module Make (PVM : Pvm.S) : S = struct | Some predecessor_state -> return predecessor_state in - (* Obtain inbox and its messages for this block. *) - let*! inbox = Store.Inboxes.get store hash in - let inbox_level = Inbox.inbox_level inbox in - let*! messages = Store.Messages.get store hash in - (* Iterate the PVM state with all the messages for this level. *) let*! state = List.fold_left_i_s @@ -105,10 +117,6 @@ module Make (PVM : Pvm.S) : S = struct predecessor_state messages in - - (* Write final state to store. *) - let*! () = Store.PVMState.set store hash state in - (* Compute extra information about the state. *) let*! initial_tick = PVM.get_tick predecessor_state in let*! last_tick = PVM.get_tick state in @@ -116,6 +124,25 @@ module Make (PVM : Pvm.S) : S = struct The number of ticks should not be an arbitrarily-sized integer. *) let num_ticks = Sc_rollup.Tick.distance initial_tick last_tick in + Lwt.return_ok (state, num_ticks) + + (** [transition_pvm node_ctxt store predecessor_hash hash] runs a PVM at the + previous state from block [predecessor_hash] by consuming as many messages + as possible from block [hash]. *) + let transition_pvm node_ctxt store predecessor_hash hash = + let open Lwt_result_syntax in + (* Obtain inbox and its messages for this block. *) + let*! inbox = Store.Inboxes.get store hash in + let inbox_level = Inbox.inbox_level inbox in + let*! messages = Store.Messages.get store hash in + + let* (state, num_ticks) = + evaluate node_ctxt store predecessor_hash inbox_level messages + in + + (* Write final state to store. *) + let*! () = Store.PVMState.set store hash state in + (* TODO: #2717 The length of messages here can potentially overflow the [int] returned from [List.length]. *) @@ -132,4 +159,15 @@ module Make (PVM : Pvm.S) : S = struct let open Lwt_result_syntax in let*! predecessor_hash = Layer1.predecessor store head in transition_pvm node_ctxt store predecessor_hash hash + + (** [simulate store messages] interprets a list of messages and checks its + applicability. *) + let simulate node_ctxt store messages = + let open Lwt_result_syntax in + let* (Head {hash; level}) = Layer1.current_head store in + let*? level = Environment.wrap_tzresult @@ Raw_level.of_int32 level in + let* (state, ticks_diff) = evaluate node_ctxt store hash level messages in + let*! state_hash = PVM.state_hash state in + let*! status = PVM.get_status state in + Lwt.return_ok {state_hash; status; ticks_diff} end diff --git a/src/proto_alpha/bin_sc_rollup_node/layer1.ml b/src/proto_alpha/bin_sc_rollup_node/layer1.ml index 4431df47a20469c69c26d521c42346eff2ea1ce2..5fa777da3dcb6d75ffb66b4f451252f32ec50598 100644 --- a/src/proto_alpha/bin_sc_rollup_node/layer1.ml +++ b/src/proto_alpha/bin_sc_rollup_node/layer1.ml @@ -26,6 +26,7 @@ open Configuration open Protocol.Alpha_context open Plugin +open Common (** @@ -40,6 +41,39 @@ let synchronization_failure e = e ; Lwt_exit.exit_and_raise 1 +type error += Non_initialized_rollup_node | Cannot_find_block of Block_hash.t + +let () = + register_error_kind + `Temporary + ~id:"sc_rollup.node.non_initialized_node" + ~title:"The rollup node is not initialized" + ~description: + "Some component of the node tried to access the store while the node has \ + never been initialized." + ~pp:(fun ppf () -> + Format.fprintf + ppf + "Some component of the node tried to access the store while the node \ + has never been initialized.") + Data_encoding.unit + (function Non_initialized_rollup_node -> Some () | _ -> None) + (fun () -> Non_initialized_rollup_node) ; + register_error_kind + ~id:"sc_rollup.node.cannot_find_block" + ~title:"Cannot find block from L1" + ~description:"A block couldn't be found from the L1 node" + ~pp:(fun ppf hash -> + Format.fprintf + ppf + "Block with hash %a was not found on the L1 node." + Block_hash.pp + hash) + `Temporary + Data_encoding.(obj1 (req "hash" Block_hash.encoding)) + (function Cannot_find_block hash -> Some hash | _ -> None) + (fun hash -> Cannot_find_block hash) + (** State @@ -105,8 +139,17 @@ module State = struct let store_block = Store.Blocks.add let block_of_hash = Store.Blocks.get + + module Blocks_cache = + Ringo_lwt.Functors.Make_opt + ((val Ringo.( + map_maker ~replacement:LRU ~overflow:Strong ~accounting:Precise)) + (Block_hash)) end +type blocks_cache = + Protocol_client_context.Alpha_block_services.block_info State.Blocks_cache.t + (** Chain events @@ -123,6 +166,12 @@ let same_branch new_head intermediate_heads = let rollback new_head = Rollback {new_head} +type t = { + blocks_cache : blocks_cache; + events : chain_event Lwt_stream.t; + node_ctxt : Node_context.t; +} + (** Helpers @@ -134,6 +183,11 @@ let genesis_hash = Block_hash.of_b58check_exn "BLockGenesisGenesisGenesisGenesisGenesisf79b5d1CoW2" +let chain_event_head_hash = function + | SameBranch {new_head = Head {hash; _}; _} + | Rollback {new_head = Head {hash; _}} -> + hash + (** [blocks_of_heads base heads] given a list of successive heads connected to [base], returns an associative list mapping block hash to block. This list is only used for traversal, not lookup. The @@ -323,24 +377,37 @@ let discard_pre_origination_blocks info chain_events = in Lwt_stream.filter_map at_or_after_origination chain_events -let start configuration (cctxt : Protocol_client_context.full) store = +let start configuration (node_ctxt : Node_context.t) store = let open Lwt_result_syntax in let*! () = Layer1_event.starting () in let* () = - check_sc_rollup_address_exists configuration.sc_rollup_address cctxt + check_sc_rollup_address_exists + configuration.sc_rollup_address + node_ctxt.cctxt in - let* event_stream = chain_events cctxt store `Main in - let* info = gather_info cctxt configuration.sc_rollup_address in - return (discard_pre_origination_blocks info event_stream) + let* event_stream = chain_events node_ctxt.cctxt store `Main in + let+ info = gather_info node_ctxt.cctxt configuration.sc_rollup_address in + let events = discard_pre_origination_blocks info event_stream in + {node_ctxt; events; blocks_cache = State.Blocks_cache.create 32} + +let current_head_opt store = State.last_seen_head store + +let current_head store = + let open Lwt_syntax in + let+ head = current_head_opt store in + Option.fold + head + ~some:ok + ~none:(Result_syntax.tzfail Non_initialized_rollup_node) let current_head_hash store = let open Lwt_syntax in - let+ head = State.last_seen_head store in + let+ head = current_head_opt store in Option.map (fun (Head {hash; _}) -> hash) head let current_level store = let open Lwt_syntax in - let+ head = State.last_seen_head store in + let+ head = current_head_opt store in Option.map (fun (Head {level; _}) -> level) head let predecessor store (Head {hash; _}) = @@ -355,3 +422,33 @@ let processed = function | SameBranch {new_head; intermediate_heads} -> List.iter_s processed_head (intermediate_heads @ [new_head]) | Rollback {new_head} -> processed_head new_head + +(** [fetch_tezos_block l1_ctxt hash] returns a block info given a block + hash. Looks for the block in the blocks cache first, and fetches it from the + L1 node otherwise. *) +let fetch_tezos_block l1_ctxt hash = + let open Lwt_result_syntax in + let find_in_cache hash fetch = + let fetch hash = + let*! block = fetch hash in + Lwt.return @@ Result.to_option block + in + let*! block = + State.Blocks_cache.find_or_replace l1_ctxt.blocks_cache hash fetch + in + match block with Some b -> return b | _ -> tzfail (Cannot_find_block hash) + in + fetch_tezos_block ~find_in_cache l1_ctxt.node_ctxt.cctxt hash + +(** Returns the reorganization of L1 blocks (if any) for [new_head]. *) +let get_tezos_reorg_for_new_head l1_state store new_head_hash = + let open Lwt_result_syntax in + let*! old_head_hash = current_head_hash store in + match old_head_hash with + | None -> + (* No known tezos head, consider the new head as being on top of a previous + tezos block. *) + let+ new_head = fetch_tezos_block l1_state new_head_hash in + {old_chain = []; new_chain = [new_head]} + | Some old_head_hash -> + tezos_reorg (fetch_tezos_block l1_state) ~old_head_hash ~new_head_hash diff --git a/src/proto_alpha/bin_sc_rollup_node/layer1.mli b/src/proto_alpha/bin_sc_rollup_node/layer1.mli index 27100c6a4030d65e41a1a81d29df2ac6a64b5afe..418fc1567ab2e479fbf8c75f0bd79314bc7fc234 100644 --- a/src/proto_alpha/bin_sc_rollup_node/layer1.mli +++ b/src/proto_alpha/bin_sc_rollup_node/layer1.mli @@ -33,6 +33,8 @@ reorganization occurred. *) +type error += Non_initialized_rollup_node + type head = Head of {hash : Block_hash.t; level : int32} val head_encoding : head Data_encoding.t @@ -46,15 +48,33 @@ type chain_event = (** A chain reorganization occurred since the previous synchronization. The rollback set [new_head] to an old block. *) -(** [start configuration cctxt store] returns a stream of [chain_event] - obtained from the monitoring of the Tezos node set up by the client - [cctxt]. The layer 1 state is stored in the data directory declared - in [configuration]. *) -val start : - Configuration.t -> - Protocol_client_context.full -> - Store.t -> - chain_event Lwt_stream.t tzresult Lwt.t +(** Type of cache holding the last 32 blocks, with their operations. *) +type blocks_cache + +type t = private { + blocks_cache : blocks_cache; + events : chain_event Lwt_stream.t; + node_ctxt : Node_context.t; +} + +val chain_event_head_hash : chain_event -> Block_hash.t + +(** [start configuration node_ctxt store] returns a stream of [chain_event] + obtained from the monitoring of the Tezos node set up by the client + [node_ctxt.cctxt]. The layer 1 state is stored in the data directory + declared in [configuration]. *) +val start : Configuration.t -> Node_context.t -> Store.t -> t tzresult Lwt.t + +(** [current_head_opt store] is the current hash and level of the head of the + Tezos chain as far as the smart-contract rollup node knows from the latest + synchronization. Returns [None] if no synchronization has ever been made. *) +val current_head_opt : Store.t -> head option Lwt.t + +(** [current_head store] is the current hash and level of the head of the Tezos + chain as far as the smart-contract rollup node knows from the latest + synchronization. Returns an error [Non_initialized_rollup_node] if no + synchronization has ever been made. *) +val current_head : Store.t -> head tzresult Lwt.t (** [current_head_hash store] is the current hash of the head of the Tezos chain as far as the smart-contract rollup node knows from the @@ -77,3 +97,20 @@ val genesis_hash : Block_hash.t (** [processed chain_event] emits a log event to officialize the processing of some layer 1 [chain_event]. *) val processed : chain_event -> unit Lwt.t + +(** [fetch_tezos_block l1_ctxt hash] returns a block info given a block hash. + Looks for the block in the blocks cache first, and fetches it from the L1 + node otherwise. *) +val fetch_tezos_block : + t -> + Block_hash.t -> + Protocol_client_context.Alpha_block_services.block_info tzresult Lwt.t + +(** [get_tezos_reorg_for_new_head l1_ctxt store hash] returns the reorganization + of L1 blocks (if any) for [new_head]. *) +val get_tezos_reorg_for_new_head : + t -> + Store.t -> + Block_hash.t -> + Protocol_client_context.Alpha_block_services.block_info Common.reorg tzresult + Lwt.t diff --git a/src/proto_alpha/bin_sc_rollup_node/tezos-sc-rollup-node-alpha.opam b/src/proto_alpha/bin_sc_rollup_node/tezos-sc-rollup-node-alpha.opam index e0bf8c7b7447408d407cd308dcd145fe0d50d9ee..b7a955219bcfcca9c83f29c42e6a607933a3a328 100644 --- a/src/proto_alpha/bin_sc_rollup_node/tezos-sc-rollup-node-alpha.opam +++ b/src/proto_alpha/bin_sc_rollup_node/tezos-sc-rollup-node-alpha.opam @@ -29,6 +29,7 @@ depends: [ "irmin" { >= "3.2.0" & < "3.3.0" } "ringo" { = "0.8" } "ringo-lwt" { = "0.8" } + "tezos-rollups-alpha" ] build: [ ["dune" "build" "-p" name "-j" jobs] diff --git a/src/proto_alpha/lib_rollups/.ocamlformat b/src/proto_alpha/lib_rollups/.ocamlformat new file mode 100644 index 0000000000000000000000000000000000000000..5e1158919e85acc2cdca272c2e521f4d69f1594e --- /dev/null +++ b/src/proto_alpha/lib_rollups/.ocamlformat @@ -0,0 +1,17 @@ +version=0.18.0 +wrap-fun-args=false +let-binding-spacing=compact +field-space=loose +break-separators=after +space-around-arrays=false +space-around-lists=false +space-around-records=false +space-around-variants=false +dock-collection-brackets=true +space-around-records=false +sequence-style=separator +doc-comments=before +margin=80 +module-item-spacing=sparse +parens-tuple=always +parens-tuple-patterns=always diff --git a/src/proto_alpha/lib_rollups/common.ml b/src/proto_alpha/lib_rollups/common.ml new file mode 100644 index 0000000000000000000000000000000000000000..72be7960b004932a92ed80758b4793f309bc6e39 --- /dev/null +++ b/src/proto_alpha/lib_rollups/common.ml @@ -0,0 +1,99 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Protocol_client_context + +type signer = { + alias : string; + pkh : Signature.public_key_hash; + pk : Signature.public_key; + sk : Client_keys.sk_uri; +} + +let get_signer cctxt pkh = + let open Lwt_result_syntax in + let* (alias, pk, sk) = Client_keys.get_key cctxt pkh in + return {alias; pkh; pk; sk} + +type 'block reorg = {old_chain : 'block list; new_chain : 'block list} + +let no_reorg = {old_chain = []; new_chain = []} + +let reorg_encoding block_encoding = + let open Data_encoding in + conv + (fun {old_chain; new_chain} -> (old_chain, new_chain)) + (fun (old_chain, new_chain) -> {old_chain; new_chain}) + @@ obj2 + (req "old_chain" (list block_encoding)) + (req "new_chain" (list block_encoding)) + +let fetch_tezos_block ~find_in_cache (cctxt : #full) hash : + (Alpha_block_services.block_info, error trace) result Lwt.t = + let fetch hash = + Alpha_block_services.info + cctxt + ~chain:cctxt#chain + ~block:(`Hash (hash, 0)) + () + in + find_in_cache hash fetch + +(* Compute the reorganization of L1 blocks from the chain whose head is + [old_head_hash] and the chain whose head [new_head_hash]. *) +let tezos_reorg fetch_tezos_block ~old_head_hash ~new_head_hash = + let open Alpha_block_services in + let open Lwt_result_syntax in + let rec loop old_chain new_chain old_head_hash new_head_hash = + if Block_hash.(old_head_hash = new_head_hash) then + return {old_chain = List.rev old_chain; new_chain = List.rev new_chain} + else + let* new_head = fetch_tezos_block new_head_hash in + let* old_head = fetch_tezos_block old_head_hash in + let old_level = old_head.header.shell.level in + let new_level = new_head.header.shell.level in + let diff = Int32.sub new_level old_level in + let (old_chain, new_chain, old, new_) = + if diff = 0l then + (* Heads at same level *) + let new_chain = new_head :: new_chain in + let old_chain = old_head :: old_chain in + let new_head_hash = new_head.header.shell.predecessor in + let old_head_hash = old_head.header.shell.predecessor in + (old_chain, new_chain, old_head_hash, new_head_hash) + else if diff > 0l then + (* New chain is longer *) + let new_chain = new_head :: new_chain in + let new_head_hash = new_head.header.shell.predecessor in + (old_chain, new_chain, old_head_hash, new_head_hash) + else + (* Old chain was longer *) + let old_chain = old_head :: old_chain in + let old_head_hash = old_head.header.shell.predecessor in + (old_chain, new_chain, old_head_hash, new_head_hash) + in + loop old_chain new_chain old new_ + in + loop [] [] old_head_hash new_head_hash diff --git a/src/proto_alpha/lib_tx_rollup/common.mli b/src/proto_alpha/lib_rollups/common.mli similarity index 74% rename from src/proto_alpha/lib_tx_rollup/common.mli rename to src/proto_alpha/lib_rollups/common.mli index 8be676d7ccde4c9f796850c167958a97117a2d4d..a1f4a55e2c7d766bffbd20e7cc091d680a761652 100644 --- a/src/proto_alpha/lib_tx_rollup/common.mli +++ b/src/proto_alpha/lib_rollups/common.mli @@ -23,7 +23,9 @@ (* *) (*****************************************************************************) -(** The type of signers for operations injected by the Tx rollup node *) +open Protocol_client_context + +(** The type of signers for operations injected by the injector *) type signer = { alias : string; pkh : Signature.public_key_hash; @@ -33,9 +35,6 @@ type signer = { (** Type of chain reorganizations. *) type 'block reorg = { - ancestor : 'block option; - (** The common ancestor of the two chains. Can be [None] if the chains have no - common ancestor, in which case all the blocks are changed *) old_chain : 'block list; (** The blocks that were in the old chain and which are not in the new one. *) new_chain : 'block list; @@ -50,3 +49,26 @@ val get_signer : val no_reorg : 'a reorg val reorg_encoding : 'a Data_encoding.t -> 'a reorg Data_encoding.t + +type block_info := Alpha_block_services.block_info + +(** [fetch_tezos_block ~find_in_cache cctxt hash] returns a block info given a + block hash. Looks for the block using [find_in_cache] first, and fetches + it from the L1 node otherwise. *) +val fetch_tezos_block : + find_in_cache: + (Block_hash.t -> + (Block_hash.t -> block_info tzresult Lwt.t) -> + block_info tzresult Lwt.t) -> + #full -> + Block_hash.t -> + block_info tzresult Lwt.t + +(** [tezos_reorg fetch ~old_head_hash ~new_head_hash] computes the + reorganization of L1 blocks from the chain whose head is [old_head_hash] and + the chain whose head [new_head_hash]. *) +val tezos_reorg : + (Block_hash.t -> block_info tzresult Lwt.t) -> + old_head_hash:Block_hash.t -> + new_head_hash:Block_hash.t -> + block_info reorg tzresult Lwt.t diff --git a/src/proto_alpha/lib_rollups/dune b/src/proto_alpha/lib_rollups/dune new file mode 100644 index 0000000000000000000000000000000000000000..170d24ffe6a44234bae17efe41a01654e940b2dd --- /dev/null +++ b/src/proto_alpha/lib_rollups/dune @@ -0,0 +1,32 @@ +; This file was automatically generated, do not edit. +; Edit file manifest/main.ml instead. + +(library + (name tezos_rollups_alpha) + (public_name tezos-rollups-alpha) + (instrumentation (backend bisect_ppx)) + (libraries + tezos-base + tezos-crypto + tezos-protocol-alpha + tezos-protocol-alpha.environment + tezos-micheline + tezos-client-alpha + tezos-client-base + tezos-workers + tezos-shell) + (inline_tests (flags -verbose) (modes native)) + (preprocess (pps ppx_inline_test)) + (library_flags (:standard -linkall)) + (flags + (:standard + -open Tezos_base.TzPervasives + -open Tezos_base.TzPervasives.Error_monad.Legacy_monad_globals + -open Tezos_base + -open Tezos_crypto + -open Tezos_protocol_alpha + -open Tezos_protocol_environment_alpha + -open Tezos_micheline + -open Tezos_client_alpha + -open Tezos_client_base + -open Tezos_workers))) diff --git a/src/proto_alpha/lib_rollups/dune-project b/src/proto_alpha/lib_rollups/dune-project new file mode 100644 index 0000000000000000000000000000000000000000..d895be4582e6aea423cc37b26527af448ded3d08 --- /dev/null +++ b/src/proto_alpha/lib_rollups/dune-project @@ -0,0 +1,4 @@ +(lang dune 2.9) +(formatting (enabled_for ocaml)) +; This file was automatically generated, do not edit. +; Edit file manifest/manifest.ml instead. diff --git a/src/proto_alpha/lib_rollups/injector_errors.ml b/src/proto_alpha/lib_rollups/injector_errors.ml new file mode 100644 index 0000000000000000000000000000000000000000..ce3f8318ad044d8d9d0564d875363c0ed443560e --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_errors.ml @@ -0,0 +1,43 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type error += No_worker_for_source of Signature.Public_key_hash.t + +let () = + register_error_kind + ~id:"tx_rollup.node.no_worker_for_source" + ~title:"No injecting queue for source" + ~description: + "An L1 operation could not be queued because its source has no worker." + ~pp:(fun ppf s -> + Format.fprintf + ppf + "No worker for source %a" + Signature.Public_key_hash.pp + s) + `Permanent + Data_encoding.(obj1 (req "source" Signature.Public_key_hash.encoding)) + (function No_worker_for_source s -> Some s | _ -> None) + (fun s -> No_worker_for_source s) diff --git a/src/proto_alpha/lib_rollups/injector_errors.mli b/src/proto_alpha/lib_rollups/injector_errors.mli new file mode 100644 index 0000000000000000000000000000000000000000..86bde783fb9cff6b203eeee34d7a88d0397d44de --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_errors.mli @@ -0,0 +1,28 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Error when the injector has no worker for the source which must inject an + operation. *) +type error += No_worker_for_source of Signature.Public_key_hash.t diff --git a/src/proto_alpha/lib_rollups/injector_events.ml b/src/proto_alpha/lib_rollups/injector_events.ml new file mode 100644 index 0000000000000000000000000000000000000000..a1f2682fe7c98dd2ad89126928b3c70dd4cad691 --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_events.ml @@ -0,0 +1,214 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Injector_worker_types + +module Make (Rollup : Injector_sigs.PARAMETERS) = struct + module Tags = Injector_tags.Make (Rollup.Tag) + include Internal_event.Simple + + let section = Rollup.events_section + + let declare_1 ~name ~msg ~level ?pp1 enc1 = + declare_3 + ~section + ~name + ~msg:("[{signer}: {tags}] " ^ msg) + ~level + ("signer", Signature.Public_key_hash.encoding) + ("tags", Tags.encoding) + enc1 + ~pp1:Signature.Public_key_hash.pp_short + ~pp2:Tags.pp + ?pp3:pp1 + + let declare_2 ~name ~msg ~level ?pp1 ?pp2 enc1 enc2 = + declare_4 + ~section + ~name + ~msg:("[{signer}: {tags}] " ^ msg) + ~level + ("signer", Signature.Public_key_hash.encoding) + ("tags", Tags.encoding) + enc1 + enc2 + ~pp1:Signature.Public_key_hash.pp_short + ~pp2:Tags.pp + ?pp3:pp1 + ?pp4:pp2 + + let declare_3 ~name ~msg ~level ?pp1 ?pp2 ?pp3 enc1 enc2 enc3 = + declare_5 + ~section + ~name + ~msg:("[{signer}: {tags}] " ^ msg) + ~level + ("signer", Signature.Public_key_hash.encoding) + ("tags", Tags.encoding) + enc1 + enc2 + enc3 + ~pp1:Signature.Public_key_hash.pp_short + ~pp2:Tags.pp + ?pp3:pp1 + ?pp4:pp2 + ?pp5:pp3 + + let request_failed = + declare_3 + ~name:"request_failed" + ~msg:"request {view} failed ({worker_status}): {errors}" + ~level:Warning + ("view", Request.encoding) + ~pp1:Request.pp + ("worker_status", Worker_types.request_status_encoding) + ~pp2:Worker_types.pp_status + ("errors", Error_monad.trace_encoding) + ~pp3:Error_monad.pp_print_trace + + let request_completed_notice = + declare_2 + ~name:"request_completed_notice" + ~msg:"{view} {worker_status}" + ~level:Notice + ("view", Request.encoding) + ("worker_status", Worker_types.request_status_encoding) + ~pp1:Request.pp + ~pp2:Worker_types.pp_status + + let request_completed_debug = + declare_2 + ~name:"request_completed_debug" + ~msg:"{view} {worker_status}" + ~level:Debug + ("view", Request.encoding) + ("worker_status", Worker_types.request_status_encoding) + ~pp1:Request.pp + ~pp2:Worker_types.pp_status + + let new_tezos_head = + declare_1 + ~name:"new_tezos_head" + ~msg:"processing new Tezos head {head}" + ~level:Debug + ("head", Block_hash.encoding) + + let injecting_pending = + declare_1 + ~name:"injecting_pending" + ~msg:"Injecting {count} pending operations" + ~level:Notice + ("count", Data_encoding.int31) + + let pp_operations_list ppf operations = + Format.fprintf + ppf + "@[%a@]" + (Format.pp_print_list L1_operation.pp) + operations + + let pp_operations_hash_list ppf operations = + Format.fprintf + ppf + "@[%a@]" + (Format.pp_print_list L1_operation.Hash.pp) + operations + + let injecting_operations = + declare_1 + ~name:"injecting_operations" + ~msg:"Injecting operations: {operations}" + ~level:Notice + ("operations", Data_encoding.list L1_operation.encoding) + ~pp1:pp_operations_list + + let simulating_operations = + declare_2 + ~name:"simulating_operations" + ~msg:"Simulating operations (force = {force}): {operations}" + ~level:Debug + ("operations", Data_encoding.list L1_operation.encoding) + ("force", Data_encoding.bool) + ~pp1:pp_operations_list + + let dropping_operation = + declare_2 + ~name:"dropping_operation" + ~msg:"Dropping operation {operation} failing with {error}" + ~level:Notice + ("operation", L1_operation.encoding) + ~pp1:L1_operation.pp + ("error", Environment.Error_monad.trace_encoding) + ~pp2:Environment.Error_monad.pp_trace + + let injected = + declare_1 + ~name:"injected" + ~msg:"Injected in {oph}" + ~level:Notice + ("oph", Operation_hash.encoding) + + let add_pending = + declare_1 + ~name:"add_pending" + ~msg:"Add {operation} to pending" + ~level:Notice + ("operation", L1_operation.encoding) + ~pp1:L1_operation.pp + + let included = + declare_3 + ~name:"included" + ~msg:"Included operations of {block} at level {level}: {operations}" + ~level:Notice + ("block", Block_hash.encoding) + ("level", Data_encoding.int32) + ("operations", Data_encoding.list L1_operation.Hash.encoding) + ~pp3:pp_operations_hash_list + + let revert_operations = + declare_1 + ~name:"revert_operations" + ~msg:"Reverting operations: {operations}" + ~level:Notice + ("operations", Data_encoding.list L1_operation.Hash.encoding) + ~pp1:pp_operations_hash_list + + let confirmed_level = + declare_1 + ~name:"confirmed_level" + ~msg:"Confirmed Tezos level {level}" + ~level:Notice + ("level", Data_encoding.int32) + + let confirmed_operations = + declare_2 + ~name:"confirmed_operations" + ~msg:"Confirmed operations of level {level}: {operations}" + ~level:Notice + ("level", Data_encoding.int32) + ("operations", Data_encoding.list L1_operation.Hash.encoding) + ~pp2:pp_operations_hash_list +end diff --git a/src/proto_alpha/lib_rollups/injector_functor.ml b/src/proto_alpha/lib_rollups/injector_functor.ml new file mode 100644 index 0000000000000000000000000000000000000000..c37f4fd42d1b97891b45fbb457c47e2ea25e2067 --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_functor.ml @@ -0,0 +1,836 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Protocol_client_context +open Protocol +open Alpha_context +open Common +open Injector_worker_types +open Injector_sigs +open Injector_errors + +(* This is the Tenderbake finality for blocks. *) +(* TODO: https://gitlab.com/tezos/tezos/-/issues/2815 + Centralize this and maybe make it configurable. *) +let confirmations = 2 + +module Op_queue = Hash_queue.Make (L1_operation.Hash) (L1_operation) + +type injection_strategy = [`Each_block | `Delay_block] + +(** Information stored about an L1 operation that was injected on a Tezos + node. *) +type injected_info = { + op : L1_operation.t; (** The L1 manager operation. *) + oph : Operation_hash.t; + (** The hash of the operation which contains [op] (this can be an L1 batch of + several manager operations). *) +} + +(** The part of the state which gathers information about injected + operations (but not included). *) +type injected_state = { + injected_operations : injected_info L1_operation.Hash.Table.t; + (** A table mapping L1 manager operation hashes to the injection info for that + operation. *) + injected_ophs : L1_operation.Hash.t list Operation_hash.Table.t; + (** A mapping of all L1 manager operations contained in a L1 batch (i.e. an L1 + operation). *) +} + +(** Information stored about an L1 operation that was included in a Tezos + block. *) +type included_info = { + op : L1_operation.t; (** The L1 manager operation. *) + oph : Operation_hash.t; + (** The hash of the operation which contains [op] (this can be an L1 batch of + several manager operations). *) + l1_block : Block_hash.t; + (** The hash of the L1 block in which the operation was included. *) + l1_level : int32; (** The level of [l1_block]. *) +} + +(** The part of the state which gathers information about + operations which are included in the L1 chain (but not confirmed). *) +type included_state = { + included_operations : included_info L1_operation.Hash.Table.t; + included_in_blocks : (int32 * L1_operation.Hash.t list) Block_hash.Table.t; +} + +(* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2755 + Persist injector data on disk *) + +(** Builds a client context from another client context but uses logging instead + of printing on stdout directly. This client context cannot make the injector + exit. *) +let injector_context (cctxt : #Protocol_client_context.full) = + let log _channel msg = Logs_lwt.info (fun m -> m "%s" msg) in + object + inherit + Protocol_client_context.wrap_full + (new Client_context.proxy_context (cctxt :> Client_context.full)) + + inherit! Client_context.simple_printer log + + method! exit code = + Format.ksprintf Stdlib.failwith "Injector client wants to exit %d" code + end + +module Make (Rollup : PARAMETERS) = struct + module Tags = Injector_tags.Make (Rollup.Tag) + + (** The internal state of each injector worker. *) + type state = { + cctxt : Protocol_client_context.full; + (** The client context which is used to perform the injections. *) + signer : signer; (** The signer for this worker. *) + tags : Tags.t; + (** The tags of this worker, for both informative and identification + purposes. *) + strategy : injection_strategy; + (** The strategy of this worker for injecting the pending operations. *) + queue : Op_queue.t; + (** The queue of pending operations for this injector. *) + injected : injected_state; + (** The information about injected operations. *) + included : included_state; + (** The information about included operations. {b Note}: Operations which + are confirmed are simply removed from the state and do not appear + anymore. *) + rollup_node_state : Rollup.rollup_node_state; + (** The state of the rollup node. *) + } + + let init_injector cctxt rollup_node_state ~signer strategy tags = + let open Lwt_result_syntax in + let+ signer = get_signer cctxt signer in + let queue = Op_queue.create 50_000 in + (* Very coarse approximation for the number of operation we expect for each + block *) + let n = + Tags.fold (fun t acc -> acc + Rollup.table_estimated_size t) tags 0 + in + { + cctxt = injector_context (cctxt :> #Protocol_client_context.full); + signer; + tags; + strategy; + queue; + injected = + { + injected_operations = L1_operation.Hash.Table.create n; + injected_ophs = Operation_hash.Table.create n; + }; + included = + { + included_operations = + L1_operation.Hash.Table.create (confirmations * n); + included_in_blocks = Block_hash.Table.create (confirmations * n); + }; + rollup_node_state; + } + + module Event = struct + include Injector_events.Make (Rollup) + + let emit1 e state x = emit e (state.signer.pkh, state.tags, x) + + let emit2 e state x y = emit e (state.signer.pkh, state.tags, x, y) + + let emit3 e state x y z = emit e (state.signer.pkh, state.tags, x, y, z) + end + + (** Add an operation to the pending queue corresponding to the signer for this + operation. *) + let add_pending_operation state op = + let open Lwt_syntax in + let+ () = Event.(emit1 add_pending) state op in + Op_queue.replace state.queue op.L1_operation.hash op + + (** Mark operations as injected (in [oph]). *) + let add_injected_operations state oph operations = + let infos = + List.map (fun op -> (op.L1_operation.hash, {op; oph})) operations + in + L1_operation.Hash.Table.replace_seq + state.injected.injected_operations + (List.to_seq infos) ; + Operation_hash.Table.replace + state.injected.injected_ophs + oph + (List.map fst infos) + + (** [add_included_operations state oph l1_block l1_level operations] marks the + [operations] as included (in the L1 batch [oph]) in the Tezos block + [l1_block] of level [l1_level]. *) + let add_included_operations state oph l1_block l1_level operations = + let open Lwt_syntax in + let+ () = + Event.(emit3 included) + state + l1_block + l1_level + (List.map (fun o -> o.L1_operation.hash) operations) + in + let infos = + List.map + (fun op -> (op.L1_operation.hash, {op; oph; l1_block; l1_level})) + operations + in + L1_operation.Hash.Table.replace_seq + state.included.included_operations + (List.to_seq infos) ; + Block_hash.Table.replace + state.included.included_in_blocks + l1_block + (l1_level, List.map fst infos) + + (** [remove state oph] removes the operations that correspond to the L1 batch + [oph] from the injected operations in the injector state. This function is + used to move operations from injected to included. *) + let remove_injected_operation state oph = + match Operation_hash.Table.find state.injected.injected_ophs oph with + | None -> + (* Nothing removed *) + [] + | Some mophs -> + Operation_hash.Table.remove state.injected.injected_ophs oph ; + List.fold_left + (fun removed moph -> + match + L1_operation.Hash.Table.find + state.injected.injected_operations + moph + with + | None -> removed + | Some info -> + L1_operation.Hash.Table.remove + state.injected.injected_operations + moph ; + info :: removed) + [] + mophs + + (** [remove state block] removes the included operations that correspond to all + the L1 batches included in [block]. This function is used when [block] is on + an alternative chain in the case of a reorganization. *) + let remove_included_operation state block = + match Block_hash.Table.find state.included.included_in_blocks block with + | None -> + (* Nothing removed *) + [] + | Some (_level, mophs) -> + Block_hash.Table.remove state.included.included_in_blocks block ; + List.fold_left + (fun removed moph -> + match + L1_operation.Hash.Table.find + state.included.included_operations + moph + with + | None -> removed + | Some info -> + L1_operation.Hash.Table.remove + state.included.included_operations + moph ; + info :: removed) + [] + mophs + + let fee_parameter_of_operations state ops = + List.fold_left + (fun acc {L1_operation.manager_operation = Manager op; _} -> + let param = Rollup.fee_parameter state op in + Injection. + { + minimal_fees = Tez.max acc.minimal_fees param.minimal_fees; + minimal_nanotez_per_byte = + Q.max acc.minimal_nanotez_per_byte param.minimal_nanotez_per_byte; + minimal_nanotez_per_gas_unit = + Q.max + acc.minimal_nanotez_per_gas_unit + param.minimal_nanotez_per_gas_unit; + force_low_fee = acc.force_low_fee || param.force_low_fee; + fee_cap = + WithExceptions.Result.get_ok + ~loc:__LOC__ + Tez.(acc.fee_cap +? param.fee_cap); + burn_cap = + WithExceptions.Result.get_ok + ~loc:__LOC__ + Tez.(acc.burn_cap +? param.burn_cap); + }) + Injection. + { + minimal_fees = Tez.zero; + minimal_nanotez_per_byte = Q.zero; + minimal_nanotez_per_gas_unit = Q.zero; + force_low_fee = false; + fee_cap = Tez.zero; + burn_cap = Tez.zero; + } + ops + + (** Simulate the injection of [operations]. See {!inject_operations} for the + specification of [must_succeed]. *) + let simulate_operations ~must_succeed state (operations : L1_operation.t list) + = + let open Lwt_result_syntax in + let open Annotated_manager_operation in + let force = + match operations with + | [] -> assert false + | [_] -> + (* If there is only one operation, fail when simulation fails *) + false + | _ -> ( + (* We want to see which operation failed in the batch if not all must + succeed *) + match must_succeed with `All -> false | `At_least_one -> true) + in + let*! () = Event.(emit2 simulating_operations) state operations force in + let fee_parameter = + fee_parameter_of_operations state.rollup_node_state operations + in + let operations = + List.map + (fun {L1_operation.manager_operation = Manager operation; _} -> + Annotated_manager_operation + (Injection.prepare_manager_operation + ~fee:Limit.unknown + ~gas_limit:Limit.unknown + ~storage_limit:Limit.unknown + operation)) + operations + in + let (Manager_list annot_op) = + Annotated_manager_operation.manager_of_list operations + in + let* (oph, op, result) = + Injection.inject_manager_operation + state.cctxt + ~simulation:true (* Only simulation here *) + ~force + ~chain:state.cctxt#chain + ~block:(`Head 0) + ~source:state.signer.pkh + ~src_pk:state.signer.pk + ~src_sk:state.signer.sk + ~successor_level: + true (* Needed to simulate tx_rollup operations in the next block *) + ~fee:Limit.unknown + ~gas_limit:Limit.unknown + ~storage_limit:Limit.unknown + ~fee_parameter + annot_op + in + return (oph, Contents_list op, Apply_results.Contents_result_list result) + + let inject_on_node state packed_contents = + let open Lwt_result_syntax in + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2815 *) + (* Branch to head - 2 for tenderbake *) + let* branch = + Tezos_shell_services.Shell_services.Blocks.hash + state.cctxt + ~chain:state.cctxt#chain + ~block:(`Head 2) + () + in + let unsigned_op_bytes = + Data_encoding.Binary.to_bytes_exn + Operation.unsigned_encoding + ({branch}, packed_contents) + in + let* signature = + Client_keys.sign + state.cctxt + ~watermark:Signature.Generic_operation + state.signer.sk + unsigned_op_bytes + in + let (Contents_list contents) = packed_contents in + let op : _ Operation.t = + {shell = {branch}; protocol_data = {contents; signature = Some signature}} + in + let op_bytes = + Data_encoding.Binary.to_bytes_exn Operation.encoding (Operation.pack op) + in + Tezos_shell_services.Shell_services.Injection.operation + state.cctxt + ~chain:state.cctxt#chain + op_bytes + >>=? fun oph -> + let*! () = Event.(emit1 injected) state oph in + return oph + + (** Inject the given [operations] in an L1 batch. If [must_succeed] is [`All] + then all the operations must succeed in the simulation of injection. If + [must_succeed] is [`At_least_one] at least one operation in the list + [operations] must be successful in the simulation. In any case, only + operations which are known as successful will be included in the injected L1 + batch. {b Note}: [must_succeed = `At_least_one] allows to incrementally build + "or-batches" by iteratively removing operations that fail from the desired + batch. *) + let rec inject_operations ~must_succeed state + (operations : L1_operation.t list) = + let open Lwt_result_syntax in + let* (_oph, packed_contents, result) = + simulate_operations ~must_succeed state operations + in + let results = Apply_results.to_list result in + let failure = ref false in + let* rev_non_failing_operations = + List.fold_left2_s + ~when_different_lengths: + [ + Exn + (Failure + "Unexpected error: length of operations and result differ in \ + simulation"); + ] + (fun acc op (Apply_results.Contents_result result) -> + match result with + | Apply_results.Manager_operation_result + {operation_result = Failed (_, error); _} -> + let*! () = Event.(emit2 dropping_operation) state op error in + failure := true ; + Lwt.return acc + | Apply_results.Manager_operation_result + {operation_result = Applied _ | Backtracked _ | Skipped _; _} -> + (* Not known to be failing *) + Lwt.return (op :: acc) + | _ -> + (* Only manager operations *) + assert false) + [] + operations + results + in + if !failure then + (* Invariant: must_succeed = `At_least_one, otherwise the simulation would have + returned an error. We try to inject without the failing operation. *) + let operations = List.rev rev_non_failing_operations in + inject_operations ~must_succeed state operations + else + (* Inject on node for real *) + let+ oph = inject_on_node state packed_contents in + (oph, operations) + + (** Returns the (upper bound on) the size of an L1 batch of operations composed + of the manager operations [rev_ops]. *) + let size_l1_batch state rev_ops = + let contents_list = + List.map + (fun (op : L1_operation.t) -> + let (Manager operation) = op.manager_operation in + let {fee; counter; gas_limit; storage_limit} = + Rollup.approximate_fee_bound state.rollup_node_state operation + in + let contents = + Manager_operation + { + source = state.signer.pkh; + operation; + fee; + counter; + gas_limit; + storage_limit; + } + in + Contents contents) + rev_ops + in + let (Contents_list contents) = + match Operation.of_list contents_list with + | Error _ -> + (* Cannot happen: rev_ops is non empty and contains only manager + operations *) + assert false + | Ok packed_contents_list -> packed_contents_list + in + let signature = + match state.signer.pkh with + | Signature.Ed25519 _ -> Signature.of_ed25519 Ed25519.zero + | Secp256k1 _ -> Signature.of_secp256k1 Secp256k1.zero + | P256 _ -> Signature.of_p256 P256.zero + in + let branch = Block_hash.zero in + let operation = + { + shell = {branch}; + protocol_data = Operation_data {contents; signature = Some signature}; + } + in + Data_encoding.Binary.length Operation.encoding operation + + (** Retrieve as many operations from the queue while remaining below the size + limit. *) + let get_operations_from_queue ~size_limit state = + let exception Reached_limit of L1_operation.t list in + let rev_ops = + try + Op_queue.fold + (fun _oph op ops -> + let new_ops = op :: ops in + let new_size = size_l1_batch state new_ops in + if new_size > size_limit then raise (Reached_limit ops) ; + new_ops) + state.queue + [] + with Reached_limit ops -> ops + in + List.rev rev_ops + + (* Ignore the failures of finalize and remove commitment operations. These + operations fail when there are either no commitment to finalize or to remove + (which can happen when there are no inbox for instance). *) + let ignore_ignorable_failing_operations operations = function + | Ok res -> Ok (`Injected res) + | Error _ as res -> + let open Result_syntax in + let+ operations_to_drop = + List.fold_left_e + (fun to_drop op -> + let (Manager operation) = op.L1_operation.manager_operation in + match Rollup.ignore_failing_operation operation with + | `Don't_ignore -> res + | `Ignore_keep -> Ok to_drop + | `Ignore_drop -> Ok (op :: to_drop)) + [] + operations + in + `Ignored operations_to_drop + + (** [inject_pending_operations_for ~size_limit state pending] injects + operations from the pending queue [pending], whose total size does + not exceed [size_limit]. Upon successful injection, the + operations are removed from the queue and marked as injected. *) + let inject_pending_operations + ?(size_limit = Constants.max_operation_data_length) state = + let open Lwt_result_syntax in + (* Retrieve and remove operations from pending *) + let operations_to_inject = get_operations_from_queue ~size_limit state in + match operations_to_inject with + | [] -> return_unit + | _ -> ( + let*! () = + Event.(emit1 injecting_pending) + state + (List.length operations_to_inject) + in + let must_succeed = + Rollup.batch_must_succeed + @@ List.map + (fun op -> op.L1_operation.manager_operation) + operations_to_inject + in + let*! res = + inject_operations ~must_succeed state operations_to_inject + in + let*? res = + ignore_ignorable_failing_operations operations_to_inject res + in + match res with + | `Injected (oph, injected_operations) -> + (* Injection succeeded, remove from pending and add to injected *) + List.iter + (fun op -> Op_queue.remove state.queue op.L1_operation.hash) + injected_operations ; + add_injected_operations state oph operations_to_inject ; + return_unit + | `Ignored operations_to_drop -> + (* Injection failed but we ignore the failure. *) + List.iter + (fun op -> Op_queue.remove state.queue op.L1_operation.hash) + operations_to_drop ; + return_unit) + + (** [register_included_operation state block level oph] marks the manager + operations contained in the L1 batch [oph] as being included in the [block] + of level [level], by moving them from the "injected" state to the "included" + state. *) + let register_included_operation state block level oph = + match remove_injected_operation state oph with + | [] -> Lwt.return_unit + | injected_infos -> + let included_mops = + List.map (fun (i : injected_info) -> i.op) injected_infos + in + add_included_operations state oph block level included_mops + + (** [register_included_operations state block level oph] marks the known (by + this injector) manager operations contained in [block] as being included. *) + let register_included_operations state + (block : Alpha_block_services.block_info) = + List.iter_s + (List.iter_s (fun (op : Alpha_block_services.operation) -> + register_included_operation + state + block.hash + block.header.shell.level + op.hash + (* TODO/TORU: Handle operations for rollup_id here with + callback *))) + block.Alpha_block_services.operations + + (** [revert_included_operations state block] marks the known (by this injector) + manager operations contained in [block] as not being included any more, + typically in the case of a reorganization where [block] is on an alternative + chain. The operations are put back in the pending queue. *) + let revert_included_operations state block = + let open Lwt_syntax in + let included_infos = remove_included_operation state block in + let* () = + Event.(emit1 revert_operations) + state + (List.map (fun o -> o.op.hash) included_infos) + in + (* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2814 + maybe put at the front of the queue for re-injection. *) + List.iter_s + (fun {op; _} -> + let {L1_operation.manager_operation = Manager mop; _} = op in + let* requeue = + Rollup.requeue_reverted_operation state.rollup_node_state mop + in + if requeue then add_pending_operation state op else return_unit) + included_infos + + (** [register_confirmed_level state confirmed_level] is called when the level + [confirmed_level] is known as confirmed. In this case, the operations of + block which are below this level are also considered as confirmed and are + removed from the "included" state. These operations cannot be part of a + reorganization so there will be no need to re-inject them anymore. *) + let register_confirmed_level state confirmed_level = + let open Lwt_syntax in + let* () = + Event.(emit confirmed_level) + (state.signer.pkh, state.tags, confirmed_level) + in + Block_hash.Table.iter_s + (fun block (level, _operations) -> + if level <= confirmed_level then + let confirmed_ops = remove_included_operation state block in + Event.(emit2 confirmed_operations) + state + level + (List.map (fun o -> o.op.hash) confirmed_ops) + else Lwt.return_unit) + state.included.included_in_blocks + + (** [on_new_tezos_head state head reorg] is called when there is a new Tezos + head (with a potential reorganization [reorg]). It first reverts any blocks + that are in the alternative branch of the reorganization and then registers + the effect of the new branch (the newly included operation and confirmed + operations). *) + let on_new_tezos_head state (head : Alpha_block_services.block_info) + (reorg : Alpha_block_services.block_info reorg) = + let open Lwt_result_syntax in + let*! () = Event.(emit1 new_tezos_head) state head.hash in + let*! () = + List.iter_s + (fun removed_block -> + revert_included_operations + state + removed_block.Alpha_block_services.hash) + (List.rev reorg.old_chain) + in + let*! () = + List.iter_s + (fun added_block -> register_included_operations state added_block) + reorg.new_chain + in + (* Head is already included in the reorganization, so no need to process it + separately. *) + let confirmed_level = + Int32.sub + head.Alpha_block_services.header.shell.level + (Int32.of_int confirmations) + in + let*! () = + if confirmed_level >= 0l then + register_confirmed_level state confirmed_level + else Lwt.return_unit + in + return_unit + + (* The request {Request.Inject} triggers an injection of the operations + the pending queue. *) + let on_inject state = inject_pending_operations state + + module Types = struct + type nonrec state = state + + type parameters = { + cctxt : Protocol_client_context.full; + rollup_node_state : Rollup.rollup_node_state; + strategy : injection_strategy; + tags : Tags.t; + } + end + + (* The worker for the injector. *) + module Worker = Worker.Make (Name) (Dummy_event) (Request) (Types) (Logger) + + (* The queue for the requests to the injector worker is infinite. *) + type worker = Worker.infinite Worker.queue Worker.t + + module Handlers = struct + type self = worker + + let on_request : type r. worker -> r Request.t -> r tzresult Lwt.t = + fun w request -> + let open Lwt_result_syntax in + let state = Worker.state w in + match request with + | Request.Add_pending op -> + let*! () = add_pending_operation state op in + return_unit + | Request.New_tezos_head (head, reorg) -> + on_new_tezos_head state head reorg + | Request.Inject -> on_inject state + + let on_request w r = + (* The execution of the request handler is protected to avoid stopping the + worker in case of an exception. *) + protect @@ fun () -> on_request w r + + let on_launch _w signer Types.{cctxt; rollup_node_state; strategy; tags} = + init_injector cctxt rollup_node_state ~signer strategy tags + + let on_error w r st errs = + let open Lwt_result_syntax in + let state = Worker.state w in + (* Errors do not stop the worker but emit an entry in the log. *) + let*! () = Event.(emit3 request_failed) state r st errs in + return_unit + + let on_completion w r _ st = + let state = Worker.state w in + match Request.view r with + | Request.View (Add_pending _ | New_tezos_head _) -> + Event.(emit2 request_completed_debug) state (Request.view r) st + | View Inject -> + Event.(emit2 request_completed_notice) state (Request.view r) st + + let on_no_request _ = return_unit + + let on_close _w = Lwt.return_unit + end + + let table = Worker.create_table Queue + + (* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2754 + Injector worker in a separate process *) + let init (cctxt : #Protocol_client_context.full) rollup_node_state ~signers = + let open Lwt_result_syntax in + let signers_map = + List.fold_left + (fun acc (signer, strategy, tags) -> + let tags = Tags.of_list tags in + let (strategy, tags) = + match Signature.Public_key_hash.Map.find_opt signer acc with + | None -> (strategy, tags) + | Some (other_strategy, other_tags) -> + let strategy = + match (strategy, other_strategy) with + | (`Each_block, `Each_block) -> `Each_block + | (`Delay_block, _) | (_, `Delay_block) -> + (* Delay_block strategy takes over because we can always wait a + little bit more to inject operation which are to be injected + "each block". *) + `Delay_block + in + (strategy, Tags.union other_tags tags) + in + Signature.Public_key_hash.Map.add signer (strategy, tags) acc) + Signature.Public_key_hash.Map.empty + signers + in + Signature.Public_key_hash.Map.iter_es + (fun signer (strategy, tags) -> + let+ worker = + Worker.launch + table + signer + { + cctxt = (cctxt :> Protocol_client_context.full); + rollup_node_state; + strategy; + tags; + } + (module Handlers) + in + ignore worker) + signers_map + + let worker_of_signer signer_pkh = + match Worker.find_opt table signer_pkh with + | None -> + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2818 + maybe lazily start worker here *) + error (No_worker_for_source signer_pkh) + | Some worker -> ok worker + + let add_pending_operation ~source op = + let open Lwt_result_syntax in + let*? w = worker_of_signer source in + let l1_operation = L1_operation.make op in + let*! () = Worker.Queue.push_request w (Request.Add_pending l1_operation) in + return_unit + + let new_tezos_head h reorg = + let workers = Worker.list table in + List.iter_p + (fun (_signer, w) -> + Worker.Queue.push_request w (Request.New_tezos_head (h, reorg))) + workers + + let has_tag_in ~tags state = + match tags with + | None -> + (* Not filtering on tags *) + true + | Some tags -> not (Tags.disjoint state.tags tags) + + let has_strategy ~strategy state = + match strategy with + | None -> + (* Not filtering on strategy *) + true + | Some strategy -> state.strategy = strategy + + let inject ?tags ?strategy () = + let workers = Worker.list table in + let tags = Option.map Tags.of_list tags in + List.iter_p + (fun (_signer, w) -> + let worker_state = Worker.state w in + if has_tag_in ~tags worker_state && has_strategy ~strategy worker_state + then Worker.Queue.push_request w Request.Inject + else Lwt.return_unit) + workers +end diff --git a/src/proto_alpha/lib_rollups/injector_functor.mli b/src/proto_alpha/lib_rollups/injector_functor.mli new file mode 100644 index 0000000000000000000000000000000000000000..183066b02a9a379d9a7e3dac3a57762ee18ae091 --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_functor.mli @@ -0,0 +1,29 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Injector_sigs + +module Make (P : PARAMETERS) : + S with type rollup_node_state := P.rollup_node_state and type tag := P.Tag.t diff --git a/src/proto_alpha/lib_rollups/injector_sigs.ml b/src/proto_alpha/lib_rollups/injector_sigs.ml new file mode 100644 index 0000000000000000000000000000000000000000..1bee0bd721705d42c04eb9d76cb0b447161dab7a --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_sigs.ml @@ -0,0 +1,145 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Protocol.Alpha_context + +(** Type to represent {e appoximate upper-bounds} for the fee and limits, used + to compute an upper bound on the size (in bytes) of an operation. *) +type approximate_fee_bound = { + fee : Tez.t; + counter : Z.t; + gas_limit : Gas.Arith.integral; + storage_limit : Z.t; +} + +type injection_strategy = + [ `Each_block (** Inject pending operations after each new L1 block *) + | `Delay_block + (** Wait for some time after the L1 block is produced to inject pending + operations. This strategy allows for maximizing the number of the same + kind of operations to include in a block. *) + ] + +(** Signature for tags used in injector *) +module type TAG = sig + include Stdlib.Set.OrderedType + + val pp : Format.formatter -> t -> unit + + val encoding : t Data_encoding.t +end + +(** Module type for parameter of functor {!Injector_functor.Make}. *) +module type PARAMETERS = sig + (** The type of the state for the rollup node that the injector can access *) + type rollup_node_state + + (** A module which contains the different tags for the injector *) + module Tag : TAG + + (** Where to put the events for this injector *) + val events_section : string list + + (** Coarse approximation for the number of operation of each tag we expect to + inject for each block. *) + val table_estimated_size : Tag.t -> int + + (** [requeue_reverted_operation state op] should return [true] if an included + operation should be re-queued for injection when the block in which it is + included is reverted (due to a reorganization). *) + val requeue_reverted_operation : + rollup_node_state -> 'a manager_operation -> bool Lwt.t + + (** [ignore_failing_operation op] specifies if the injector should + ignore this operation when its simulation fails when trying to inject. + Returns: + - [`Ignore_keep] if the operation should be ignored but kept in the + pending queue, + - [`Ignore_drop] if the operation should be ignored and dropped from the + pending queue, + - [`Don't_ignore] if the failing operation should not be ignored and the + failure reported. + *) + val ignore_failing_operation : + 'a manager_operation -> [`Ignore_keep | `Ignore_drop | `Don't_ignore] + + (** Returns the {e appoximate upper-bounds} for the fee and limits of an + operation, used to compute an upper bound on the size (in bytes) for this + operation. *) + val approximate_fee_bound : + rollup_node_state -> 'a manager_operation -> approximate_fee_bound + + (** Returns the fee_parameter (to compute fee w.r.t. gas, size, etc.) and the + caps of fee and burn for each operation. *) + val fee_parameter : + rollup_node_state -> 'a manager_operation -> Injection.fee_parameter + + (** When injecting the given [operations] in an L1 batch, if + [batch_must_succeed operations] returns [`All] then all the operations must + succeed in the simulation of injection. If it returns [`At_least_one], at + least one operation in the list [operations] must be successful in the + simulation. In any case, only operations which are known as successful will + be included in the injected L1 batch. {b Note}: Returning [`At_least_one] + allows to incrementally build "or-batches" by iteratively removing + operations that fail from the desired batch. *) + val batch_must_succeed : + packed_manager_operation list -> [`All | `At_least_one] +end + +(** Output signature for functor {!Injector_functor.Make}. *) +module type S = sig + type rollup_node_state + + type tag + + (** Initializes the injector with the rollup node state, for a list of + signers, and start the workers. Each signer has its own worker with a + queue of operations to inject. *) + val init : + #Protocol_client_context.full -> + rollup_node_state -> + signers:(public_key_hash * injection_strategy * tag list) list -> + unit tzresult Lwt.t + + (** Add an operation as pending injection in the injector. *) + val add_pending_operation : + source:public_key_hash -> 'a manager_operation -> unit tzresult Lwt.t + + (** Notify the injector of a new Tezos head. The injector marks the operations + appropriately (for instance reverted operations that are part of a + reorganization are put back in the pending queue). When an operation is + considered as {e confirmed}, it disappears from the injector. *) + val new_tezos_head : + Protocol_client_context.Alpha_block_services.block_info -> + Protocol_client_context.Alpha_block_services.block_info Common.reorg -> + unit Lwt.t + + (** Trigger an injection of the pending operations for all workers. If [tags] + is given, only the workers which have a tag in [tags] inject their pending + operations. If [strategy] is given, only workers which have this strategy + inject their pending operations. *) + val inject : + ?tags:tag list -> ?strategy:injection_strategy -> unit -> unit Lwt.t +end diff --git a/src/proto_alpha/lib_tx_rollup/common.ml b/src/proto_alpha/lib_rollups/injector_tags.ml similarity index 70% rename from src/proto_alpha/lib_tx_rollup/common.ml rename to src/proto_alpha/lib_rollups/injector_tags.ml index 491478b2c1c790ea015b6f067a521814febc0109..1e92ff5de2cb9fe0886d3f5fd183264c482f18e7 100644 --- a/src/proto_alpha/lib_tx_rollup/common.ml +++ b/src/proto_alpha/lib_rollups/injector_tags.ml @@ -23,32 +23,17 @@ (* *) (*****************************************************************************) -type signer = { - alias : string; - pkh : Signature.public_key_hash; - pk : Signature.public_key; - sk : Client_keys.sk_uri; -} +module Make (Tag : Injector_sigs.TAG) = struct + include Set.Make (Tag) -let get_signer cctxt pkh = - let open Lwt_result_syntax in - let* (alias, pk, sk) = Client_keys.get_key cctxt pkh in - return {alias; pkh; pk; sk} + let pp ppf tags = + Format.pp_print_list + ~pp_sep:(fun ppf () -> Format.fprintf ppf ",@ ") + Tag.pp + ppf + (elements tags) -type 'block reorg = { - ancestor : 'block option; - old_chain : 'block list; - new_chain : 'block list; -} - -let no_reorg = {ancestor = None; old_chain = []; new_chain = []} - -let reorg_encoding block_encoding = - let open Data_encoding in - conv - (fun {ancestor; old_chain; new_chain} -> (ancestor, old_chain, new_chain)) - (fun (ancestor, old_chain, new_chain) -> {ancestor; old_chain; new_chain}) - @@ obj3 - (opt "ancestor" block_encoding) - (req "old_chain" (list block_encoding)) - (req "new_chain" (list block_encoding)) + let encoding = + let open Data_encoding in + conv elements of_list (list Tag.encoding) +end diff --git a/src/proto_alpha/lib_rollups/injector_tags.mli b/src/proto_alpha/lib_rollups/injector_tags.mli new file mode 100644 index 0000000000000000000000000000000000000000..9efa6842376a88f740150adb776f19ba3a3f3d8c --- /dev/null +++ b/src/proto_alpha/lib_rollups/injector_tags.mli @@ -0,0 +1,35 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Make a set of tags given a module for tags. *) +module Make (Tag : Injector_sigs.TAG) : sig + include Set.S with type elt = Tag.t + + (** Pretty print a set of tags *) + val pp : Format.formatter -> t -> unit + + (** Encoding for sets of tags *) + val encoding : t Data_encoding.t +end diff --git a/src/proto_alpha/lib_tx_rollup/injector_worker_types.ml b/src/proto_alpha/lib_rollups/injector_worker_types.ml similarity index 77% rename from src/proto_alpha/lib_tx_rollup/injector_worker_types.ml rename to src/proto_alpha/lib_rollups/injector_worker_types.ml index 43e8449785defa740b30ac39b2f5441b168db6d4..2396da7a9702b6d2fdc245c1820d3561862cf727 100644 --- a/src/proto_alpha/lib_tx_rollup/injector_worker_types.ml +++ b/src/proto_alpha/lib_rollups/injector_worker_types.ml @@ -28,55 +28,6 @@ open Protocol open Alpha_context open Common -type tag = - [ `Commitment - | `Submit_batch - | `Finalize_commitment - | `Remove_commitment - | `Rejection - | `Dispatch_withdrawals ] - -module Tags = Set.Make (struct - type t = tag - - let compare = Stdlib.compare -end) - -type tags = Tags.t - -let string_of_tag : tag -> string = function - | `Submit_batch -> "submit_batch" - | `Commitment -> "commitment" - | `Finalize_commitment -> "finalize_commitment" - | `Remove_commitment -> "remove_commitment" - | `Rejection -> "rejection" - | `Dispatch_withdrawals -> "dispatch_withdrawals" - -let pp_tags ppf tags = - Format.pp_print_list - ~pp_sep:(fun ppf () -> Format.fprintf ppf ",@ ") - (fun ppf t -> Format.pp_print_string ppf (string_of_tag t)) - ppf - (Tags.elements tags) - -let tag_encoding : tag Data_encoding.t = - let open Data_encoding in - string_enum - (List.map - (fun t -> (string_of_tag t, t)) - [ - `Submit_batch; - `Commitment; - `Finalize_commitment; - `Remove_commitment; - `Rejection; - `Dispatch_withdrawals; - ]) - -let tags_encoding : tags Data_encoding.t = - let open Data_encoding in - conv Tags.elements Tags.of_list (list tag_encoding) - module Request = struct type 'a t = | Add_pending : L1_operation.t -> unit t @@ -126,11 +77,9 @@ module Request = struct | Add_pending op -> Format.fprintf ppf - "request add %a by %a to pending queue" + "request add %a to pending queue" L1_operation.Hash.pp op.hash - Signature.Public_key_hash.pp - op.source | New_tezos_head (b, r) -> Format.fprintf ppf diff --git a/src/proto_alpha/lib_tx_rollup/injector_worker_types.mli b/src/proto_alpha/lib_rollups/injector_worker_types.mli similarity index 89% rename from src/proto_alpha/lib_tx_rollup/injector_worker_types.mli rename to src/proto_alpha/lib_rollups/injector_worker_types.mli index d7aa1fb2f378f7951828ad5156216e38e725ff4e..ddca3d714f5487b7ed59521e921279dfac9e80d9 100644 --- a/src/proto_alpha/lib_tx_rollup/injector_worker_types.mli +++ b/src/proto_alpha/lib_rollups/injector_worker_types.mli @@ -28,22 +28,6 @@ open Protocol open Alpha_context open Common -type tag = - [ `Commitment - | `Submit_batch - | `Finalize_commitment - | `Remove_commitment - | `Rejection - | `Dispatch_withdrawals ] - -module Tags : Set.S with type elt = tag - -type tags = Tags.t - -val tags_encoding : tags Data_encoding.t - -val pp_tags : Format.formatter -> tags -> unit - module Request : sig type 'a t = | Add_pending : L1_operation.t -> unit t diff --git a/src/proto_alpha/lib_tx_rollup/l1_operation.ml b/src/proto_alpha/lib_rollups/l1_operation.ml similarity index 85% rename from src/proto_alpha/lib_tx_rollup/l1_operation.ml rename to src/proto_alpha/lib_rollups/l1_operation.ml index 178bafdd1c262276a2538ebbd5bd0b454be4a819..7e9bca11247d410bcc7fa5fbc5f63786b6071620 100644 --- a/src/proto_alpha/lib_tx_rollup/l1_operation.ml +++ b/src/proto_alpha/lib_rollups/l1_operation.ml @@ -142,6 +142,30 @@ module Manager_operation = struct level (Format.pp_print_list pp_rollup_reveal) tickets_info + | Sc_rollup_add_messages {rollup; messages} -> + Format.fprintf + ppf + "publishing %d messages to rollup %a inbox" + (List.length messages) + Sc_rollup.Address.pp + rollup + | Sc_rollup_cement {rollup; commitment} -> + Format.fprintf + ppf + "cementing commitment %a of rollup %a" + Sc_rollup.Commitment_hash.pp + commitment + Sc_rollup.Address.pp + rollup + | Sc_rollup_publish + {rollup; commitment = Sc_rollup.Commitment.{inbox_level; _}} -> + Format.fprintf + ppf + "publish commitment for level %a of rollup %a" + Raw_level.pp + inbox_level + Sc_rollup.Address.pp + rollup | _ -> pp_kind ppf op end @@ -162,35 +186,31 @@ let () = Base58.check_encoded_prefix Hash.b58check_encoding "mop" 53 type hash = Hash.t -type t = { - hash : hash; - source : public_key_hash; - manager_operation : packed_manager_operation; -} +type t = {hash : hash; manager_operation : packed_manager_operation} let hash_manager_operation op = Hash.hash_bytes [Data_encoding.Binary.to_bytes_exn Manager_operation.encoding op] -let hash op = - (* Hashing only manager operation *) - hash_manager_operation op.manager_operation +let make manager_operation = + let manager_operation = Manager manager_operation in + let hash = hash_manager_operation manager_operation in + {hash; manager_operation} let encoding = let open Data_encoding in conv - (fun {hash; source; manager_operation} -> (hash, source, manager_operation)) - (fun (hash, source, manager_operation) -> {hash; source; manager_operation}) - @@ obj3 + (fun {hash; manager_operation} -> (hash, manager_operation)) + (fun (hash, manager_operation) -> {hash; manager_operation}) + @@ obj2 (req "hash" Hash.encoding) - (req "source" Signature.Public_key_hash.encoding) (req "manager_operation" Manager_operation.encoding) -let pp ppf op = +let pp ppf {hash; manager_operation} = Format.fprintf ppf "%a (%a)" Manager_operation.pp - op.manager_operation + manager_operation Hash.pp - op.hash + hash diff --git a/src/proto_alpha/lib_tx_rollup/l1_operation.mli b/src/proto_alpha/lib_rollups/l1_operation.mli similarity index 84% rename from src/proto_alpha/lib_tx_rollup/l1_operation.mli rename to src/proto_alpha/lib_rollups/l1_operation.mli index a335e2b9d2661f3b223daef642eff789bbe9105c..73f1886ed737314c486d7d57bd88b152dd8110a9 100644 --- a/src/proto_alpha/lib_tx_rollup/l1_operation.mli +++ b/src/proto_alpha/lib_rollups/l1_operation.mli @@ -32,21 +32,13 @@ module Hash : S.HASH type hash = Hash.t (** The type of L1 operations that are injected on Tezos by the rollup node *) -type t = { +type t = private { hash : hash; (** The hash of the L1 manager operation (without the source) *) - source : public_key_hash; - (** The source of the operation, i.e., the key that will sign the - operation for injection. Note: the source is decided when the - operation is queued in the injector at the moment. *) manager_operation : packed_manager_operation; (** The manager operation *) } -(** Hash a manager operation *) -val hash_manager_operation : packed_manager_operation -> hash - -(** Hash an L1 operation. This is the same as hashing the corresponding manager - operation. *) -val hash : t -> hash +(** [make op] returns an L1 operation with the corresponding hash. *) +val make : 'a manager_operation -> t (** Encoding for L1 operations *) val encoding : t Data_encoding.t diff --git a/src/proto_alpha/lib_rollups/tezos-rollups-alpha.opam b/src/proto_alpha/lib_rollups/tezos-rollups-alpha.opam new file mode 100644 index 0000000000000000000000000000000000000000..4f7711a2f66ac7c4e02edeb6be4439cd1bb2ad89 --- /dev/null +++ b/src/proto_alpha/lib_rollups/tezos-rollups-alpha.opam @@ -0,0 +1,26 @@ +# This file was automatically generated, do not edit. +# Edit file manifest/main.ml instead. +opam-version: "2.0" +maintainer: "contact@tezos.com" +authors: ["Tezos devteam"] +homepage: "https://www.tezos.com/" +bug-reports: "https://gitlab.com/tezos/tezos/issues" +dev-repo: "git+https://gitlab.com/tezos/tezos.git" +license: "MIT" +depends: [ + "dune" { >= "2.9" } + "ppx_inline_test" + "tezos-base" + "tezos-crypto" + "tezos-protocol-alpha" + "tezos-micheline" + "tezos-client-alpha" + "tezos-client-base" + "tezos-workers" + "tezos-shell" +] +build: [ + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] +synopsis: "Tezos/Protocol: protocol specific library for rollups" diff --git a/src/proto_alpha/lib_sc_rollup/dune b/src/proto_alpha/lib_sc_rollup/dune index a680588fc8d6685351128b97e92a177363a6659c..0da57d96fb86a8f7a29c536f014a4779be2cd011 100644 --- a/src/proto_alpha/lib_sc_rollup/dune +++ b/src/proto_alpha/lib_sc_rollup/dune @@ -7,6 +7,7 @@ (instrumentation (backend bisect_ppx)) (libraries tezos-base + tezos-protocol-alpha.environment tezos-protocol-alpha tezos-protocol-plugin-alpha tezos-protocol-alpha-parameters @@ -17,6 +18,7 @@ (flags (:standard -open Tezos_base.TzPervasives + -open Tezos_protocol_environment_alpha -open Tezos_protocol_alpha -open Tezos_protocol_plugin_alpha -open Tezos_protocol_alpha_parameters diff --git a/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.ml b/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.ml new file mode 100644 index 0000000000000000000000000000000000000000..3053823ec218120f68ed57990c5f439cdff4df80 --- /dev/null +++ b/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.ml @@ -0,0 +1,94 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type signer = Environment.Bls_signature.pk + +type signature = Environment.Bls_signature.signature + +type message = {signer : signer; contents : string} + +type t = {messages : message list; aggregated_signatures : signature} + +module Hash = + Blake2B.Make + (Base58) + (struct + let name = "sc_rollup_l2_message_hash" + + let title = "An sc_rollup L2 message" + + (* FIXME: placeholder *) + let b58check_prefix = "\017\143\169\088" (* scL2(54) *) + + let size = None + end) + +let bls_pk_encoding = + Data_encoding.( + conv_with_guard + Environment.Bls_signature.pk_to_bytes + (fun x -> + Option.to_result + ~none:"not a valid bls public key" + (Environment.Bls_signature.pk_of_bytes_opt x)) + bytes) + +let message_encoding = + let open Data_encoding in + conv + (fun {signer; contents} -> (signer, contents)) + (fun (signer, contents) -> {signer; contents}) + (obj2 (req "signer" bls_pk_encoding) (req "contents" string)) + +let signature_encoding = + let open Data_encoding in + conv_with_guard + (fun signature -> Environment.Bls_signature.signature_to_bytes signature) + (fun bytes -> + match Environment.Bls_signature.signature_of_bytes_opt bytes with + | Some x -> Ok x + | None -> Error "Not a valid bls_signature") + (Fixed.bytes Environment.Bls_signature.signature_size_in_bytes) + +let encoding = + let open Data_encoding in + conv + (fun {messages; aggregated_signatures} -> (messages, aggregated_signatures)) + (fun (messages, aggregated_signatures) -> {messages; aggregated_signatures}) + (obj2 + (req "messages" (list message_encoding)) + (req "aggregated_signature" signature_encoding)) + +let hash batch = + Hash.hash_bytes [Data_encoding.Binary.to_bytes_exn encoding batch] + +type full = {hash : Hash.t; batch : t} + +let full_encoding = + let open Data_encoding in + conv + (fun {hash; batch} -> (hash, batch)) + (fun (hash, batch) -> {hash; batch}) + (obj2 (req "hash" Hash.encoding) (req "batch" encoding)) diff --git a/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.mli b/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.mli new file mode 100644 index 0000000000000000000000000000000000000000..bdd7f531ad92d4e2b490694076c1a40169b1b54a --- /dev/null +++ b/src/proto_alpha/lib_sc_rollup/sc_rollup_messages.mli @@ -0,0 +1,46 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** An L2 public key *) +type signer = Environment.Bls_signature.pk + +type signature = Environment.Bls_signature.signature + +(** A message is identified by its signer and its contents. The signature itself + is contained by the batch. *) +type message = {signer : signer; contents : string} + +(** Batch containing messages and the aggregated signature for these messages *) +type t = {messages : message list; aggregated_signatures : signature} + +module Hash : S.HASH + +val encoding : t Data_encoding.t + +val hash : t -> Hash.t + +type full = {hash : Hash.t; batch : t} + +val full_encoding : full Data_encoding.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 2ce16171a58446224fc60edab27c32490264524a..acfe4e4db47edf323a6a922c9cfd665b187cd4e4 100644 --- a/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml +++ b/src/proto_alpha/lib_sc_rollup/sc_rollup_services.ml @@ -109,3 +109,19 @@ let current_status () = ~query:RPC_query.empty ~output:Data_encoding.string RPC_path.(open_root / "status") + +let inject_message () = + RPC_service.post_service + ~description:"Inject an L2 message" + ~query:RPC_query.empty + ~input:Sc_rollup_messages.encoding + ~output: + Data_encoding.(tup4 Sc_rollup_messages.Hash.encoding string string z) + RPC_path.(open_root / "injection" / "messages") + +let batcher_messages () = + RPC_service.get_service + ~description:"Current messages waiting in the batcher" + ~query:RPC_query.empty + ~output:(Data_encoding.list Sc_rollup_messages.full_encoding) + RPC_path.(open_root / "batcher" / "messages") diff --git a/src/proto_alpha/lib_tx_rollup/accuser.ml b/src/proto_alpha/lib_tx_rollup/accuser.ml index bd54b01c137904ec4b6b58af49dd192ad4060a80..9e255f001c7c99ee46ed9062d2eec13994e2f41f 100644 --- a/src/proto_alpha/lib_tx_rollup/accuser.ml +++ b/src/proto_alpha/lib_tx_rollup/accuser.ml @@ -250,7 +250,4 @@ let reject_bad_commitment ~source (state : State.t) proof; } in - let manager_operation = Manager rejection_operation in - let hash = L1_operation.hash_manager_operation manager_operation in - Injector.add_pending_operation - {L1_operation.hash; source; manager_operation} + Injector.add_pending_operation ~source rejection_operation diff --git a/src/proto_alpha/lib_tx_rollup/batcher.ml b/src/proto_alpha/lib_tx_rollup/batcher.ml index f622994f9934e8083874422f5d6132c84700d3f0..33d007ca5d08845f01b3666e1bd5de620429398c 100644 --- a/src/proto_alpha/lib_tx_rollup/batcher.ml +++ b/src/proto_alpha/lib_tx_rollup/batcher.ml @@ -47,29 +47,15 @@ let encode_batch batch = let inject_batches state batches = let open Lwt_result_syntax in - let*? operations = - List.map_e - (fun batch -> - let open Result_syntax in - let+ batch_content = encode_batch batch in - let manager_operation = - Manager - (Tx_rollup_submit_batch - { - tx_rollup = state.rollup; - content = batch_content; - burn_limit = None; - }) - in - { - L1_operation.hash = - L1_operation.hash_manager_operation manager_operation; - source = state.signer; - manager_operation; - }) - batches - in - Injector.add_pending_operations operations + List.iter_es + (fun batch -> + let*? batch_content = encode_batch batch in + let batch_operation = + Tx_rollup_submit_batch + {tx_rollup = state.rollup; content = batch_content; burn_limit = None} + in + Injector.add_pending_operation ~source:state.signer batch_operation) + batches (** [is_batch_valid] returns whether the batch is valid or not based on two criteria: diff --git a/src/proto_alpha/lib_tx_rollup/committer.ml b/src/proto_alpha/lib_tx_rollup/committer.ml index ba31a9f5ec2c72ded820f7bb43fbc1b92be5cb02..cd817ea2f3bffe099c6d4296363d943bc2521047 100644 --- a/src/proto_alpha/lib_tx_rollup/committer.ml +++ b/src/proto_alpha/lib_tx_rollup/committer.ml @@ -40,9 +40,5 @@ let commit_block ~operator tx_rollup block = match block.L2block.commitment with | None -> return_unit | Some commitment -> - let manager_operation = - Manager (Tx_rollup_commit {tx_rollup; commitment}) - in - let hash = L1_operation.hash_manager_operation manager_operation in - Injector.add_pending_operation - {L1_operation.hash; source = operator; manager_operation} + let commit_operation = Tx_rollup_commit {tx_rollup; commitment} in + Injector.add_pending_operation ~source:operator commit_operation diff --git a/src/proto_alpha/lib_tx_rollup/daemon.ml b/src/proto_alpha/lib_tx_rollup/daemon.ml index 30601446d6530fcee8b2f752e76e32e4913aaa46..11118dfed68e21caadbaf7329d3170b3607d0d00 100644 --- a/src/proto_alpha/lib_tx_rollup/daemon.ml +++ b/src/proto_alpha/lib_tx_rollup/daemon.ml @@ -437,21 +437,21 @@ let notify_head state head reorg = let queue_gc_operations state = let open Lwt_result_syntax in let tx_rollup = state.State.rollup_info.rollup_id in - let inject source op = - let manager_operation = Manager op in - let hash = L1_operation.hash_manager_operation manager_operation in - Injector.add_pending_operation - {L1_operation.hash; source; manager_operation} - in let queue_finalize_commitment state = match state.State.signers.finalize_commitment with | None -> return_unit - | Some source -> inject source (Tx_rollup_finalize_commitment {tx_rollup}) + | Some source -> + Injector.add_pending_operation + ~source + (Tx_rollup_finalize_commitment {tx_rollup}) in let queue_remove_commitment state = match state.State.signers.remove_commitment with | None -> return_unit - | Some source -> inject source (Tx_rollup_remove_commitment {tx_rollup}) + | Some source -> + Injector.add_pending_operation + ~source + (Tx_rollup_remove_commitment {tx_rollup}) in let* () = queue_finalize_commitment state in queue_remove_commitment state @@ -497,14 +497,14 @@ let trigger_injection state header = let* () = if delay <= 0. then return_unit else - let* () = Event.(emit Injector.wait) delay in + let* () = Event.(emit inject_wait) delay in Lwt_unix.sleep delay in - Injector.inject ~strategy:Injector.Delay_block () + Injector.inject ~strategy:`Delay_block () in ignore promise ; (* Queue request for injection of operation that must be injected each block *) - Injector.inject ~strategy:Injector.Each_block () + Injector.inject ~strategy:`Each_block () let dispatch_withdrawals_on_l1 state level = let open Lwt_result_syntax in @@ -791,6 +791,7 @@ let run configuration cctxt = in let* () = Injector.init + state.cctxt state ~signers: (List.filter_map @@ -798,15 +799,15 @@ let run configuration cctxt = | (None, _, _) -> None | (Some x, strategy, tags) -> Some (x, strategy, tags)) [ - (operator, Injector.Each_block, [`Commitment]); + (operator, `Each_block, [Injector.Commitment]); (* Batches of L2 operations are submitted with a delay after each block, to allow for more operations to arrive and be included in the following block. *) - (signers.submit_batch, Delay_block, [`Submit_batch]); - (signers.finalize_commitment, Each_block, [`Finalize_commitment]); - (signers.remove_commitment, Each_block, [`Remove_commitment]); - (signers.rejection, Each_block, [`Rejection]); - (signers.dispatch_withdrawals, Each_block, [`Dispatch_withdrawals]); + (signers.submit_batch, `Delay_block, [Submit_batch]); + (signers.finalize_commitment, `Each_block, [Finalize_commitment]); + (signers.remove_commitment, `Each_block, [Remove_commitment]); + (signers.rejection, `Each_block, [Rejection]); + (signers.dispatch_withdrawals, `Each_block, [Dispatch_withdrawals]); ]) in let* () = diff --git a/src/proto_alpha/lib_tx_rollup/dispatcher.ml b/src/proto_alpha/lib_tx_rollup/dispatcher.ml index daef32bbd59b2de28ede3653fd5cec0ac7885032..354907d21f7910d88d28ec37ead5a255ce17ef48 100644 --- a/src/proto_alpha/lib_tx_rollup/dispatcher.ml +++ b/src/proto_alpha/lib_tx_rollup/dispatcher.ml @@ -116,10 +116,4 @@ let dispatch_operations_of_block (state : State.t) (block : L2block.t) = let dispatch_withdrawals ~source state block = let open Lwt_result_syntax in let* operations = dispatch_operations_of_block state block in - List.iter_es - (fun dispatch_op -> - let manager_operation = Manager dispatch_op in - let hash = L1_operation.hash_manager_operation manager_operation in - Injector.add_pending_operation - {L1_operation.hash; source; manager_operation}) - operations + List.iter_es (Injector.add_pending_operation ~source) operations diff --git a/src/proto_alpha/lib_tx_rollup/dune b/src/proto_alpha/lib_tx_rollup/dune index ccc0f8755ed1c1ba8e55d41742a1ef46b009a6ac..da23ecc44d5321bbbdc3eddbeed9a4526bac4969 100644 --- a/src/proto_alpha/lib_tx_rollup/dune +++ b/src/proto_alpha/lib_tx_rollup/dune @@ -25,7 +25,8 @@ tezos-client-base-unix tezos-shell tezos-store - tezos-workers) + tezos-workers + tezos-rollups-alpha) (inline_tests (flags -verbose) (modes native)) (preprocess (pps ppx_inline_test)) (library_flags (:standard -linkall)) @@ -48,4 +49,5 @@ -open Tezos_micheline -open Tezos_client_base -open Tezos_client_base_unix - -open Tezos_workers))) + -open Tezos_workers + -open Tezos_rollups_alpha))) diff --git a/src/proto_alpha/lib_tx_rollup/error.ml b/src/proto_alpha/lib_tx_rollup/error.ml index 07fcf66de704fa643bac9ec8e6ccda4465c94086..10a6c3fcd12bbc8d26d5f60b93825a5e85894723 100644 --- a/src/proto_alpha/lib_tx_rollup/error.ml +++ b/src/proto_alpha/lib_tx_rollup/error.ml @@ -358,25 +358,6 @@ let () = | Tx_rollup_invalid_message_position_in_inbox i -> Some i | _ -> None) (fun i -> Tx_rollup_invalid_message_position_in_inbox i) -type error += No_worker_for_source of Signature.Public_key_hash.t - -let () = - register_error_kind - ~id:"tx_rollup.node.no_worker_for_source" - ~title:"No injecting queue for source" - ~description: - "An L1 operation could not be queued because its source has no worker." - ~pp:(fun ppf s -> - Format.fprintf - ppf - "No worker for source %a" - Signature.Public_key_hash.pp - s) - `Permanent - Data_encoding.(obj1 (req "source" Signature.Public_key_hash.encoding)) - (function No_worker_for_source s -> Some s | _ -> None) - (fun s -> No_worker_for_source s) - type error += No_batcher let () = diff --git a/src/proto_alpha/lib_tx_rollup/error.mli b/src/proto_alpha/lib_tx_rollup/error.mli index 81097dcb006add26fc81833b4a43f3de3107a869..7959486a25a296887136900667381732794f7485 100644 --- a/src/proto_alpha/lib_tx_rollup/error.mli +++ b/src/proto_alpha/lib_tx_rollup/error.mli @@ -95,10 +95,6 @@ type error += Tx_rollup_tree_kinded_key_not_found (** Error when a message position does not exist in the inbox for the proof RPC *) type error += Tx_rollup_invalid_message_position_in_inbox of int -(** Error when the injector has no worker for the source which must inject an - operation. *) -type error += No_worker_for_source of Signature.Public_key_hash.t - (** Error when we want to interact with the batcher but it was not started. *) type error += No_batcher diff --git a/src/proto_alpha/lib_tx_rollup/event.ml b/src/proto_alpha/lib_tx_rollup/event.ml index 0e319edd580d0c6e5f17c7796cae0fe8ea4f32af..ae25cc3696891a1ac2e95bda8c5c776ba135538b 100644 --- a/src/proto_alpha/lib_tx_rollup/event.ml +++ b/src/proto_alpha/lib_tx_rollup/event.ml @@ -195,6 +195,14 @@ let new_tezos_head = ~level:Notice ("tezos_head", Block_hash.encoding) +let inject_wait = + declare_1 + ~section + ~name:"inject_wait" + ~msg:"Waiting {delay} seconds to trigger injection" + ~level:Notice + ("delay", Data_encoding.float) + module Batcher = struct let section = section @ ["batcher"] @@ -280,201 +288,6 @@ module Batcher = struct end end -module Injector = struct - open Injector_worker_types - - let section = section @ ["injector"] - - let wait = - declare_1 - ~section - ~name:"wait" - ~msg:"Waiting {delay} seconds to trigger injection" - ~level:Notice - ("delay", Data_encoding.float) - - let declare_1 ~name ~msg ~level ?pp1 enc1 = - declare_3 - ~section - ~name - ~msg:("[{signer}: {tags}] " ^ msg) - ~level - ("signer", Signature.Public_key_hash.encoding) - ("tags", Injector_worker_types.tags_encoding) - enc1 - ~pp1:Signature.Public_key_hash.pp_short - ~pp2:Injector_worker_types.pp_tags - ?pp3:pp1 - - let declare_2 ~name ~msg ~level ?pp1 ?pp2 enc1 enc2 = - declare_4 - ~section - ~name - ~msg:("[{signer}: {tags}] " ^ msg) - ~level - ("signer", Signature.Public_key_hash.encoding) - ("tags", Injector_worker_types.tags_encoding) - enc1 - enc2 - ~pp1:Signature.Public_key_hash.pp_short - ~pp2:Injector_worker_types.pp_tags - ?pp3:pp1 - ?pp4:pp2 - - let declare_3 ~name ~msg ~level ?pp1 ?pp2 ?pp3 enc1 enc2 enc3 = - declare_5 - ~section - ~name - ~msg:("[{signer}: {tags}] " ^ msg) - ~level - ("signer", Signature.Public_key_hash.encoding) - ("tags", Injector_worker_types.tags_encoding) - enc1 - enc2 - enc3 - ~pp1:Signature.Public_key_hash.pp_short - ~pp2:Injector_worker_types.pp_tags - ?pp3:pp1 - ?pp4:pp2 - ?pp5:pp3 - - let request_failed = - declare_3 - ~name:"request_failed" - ~msg:"request {view} failed ({worker_status}): {errors}" - ~level:Warning - ("view", Request.encoding) - ~pp1:Request.pp - ("worker_status", Worker_types.request_status_encoding) - ~pp2:Worker_types.pp_status - ("errors", Error_monad.trace_encoding) - ~pp3:Error_monad.pp_print_trace - - let request_completed_notice = - declare_2 - ~name:"request_completed_notice" - ~msg:"{view} {worker_status}" - ~level:Notice - ("view", Request.encoding) - ("worker_status", Worker_types.request_status_encoding) - ~pp1:Request.pp - ~pp2:Worker_types.pp_status - - let request_completed_debug = - declare_2 - ~name:"request_completed_debug" - ~msg:"{view} {worker_status}" - ~level:Debug - ("view", Request.encoding) - ("worker_status", Worker_types.request_status_encoding) - ~pp1:Request.pp - ~pp2:Worker_types.pp_status - - let new_tezos_head = - declare_1 - ~name:"new_tezos_head" - ~msg:"processing new Tezos head {head}" - ~level:Debug - ("head", Block_hash.encoding) - - let injecting_pending = - declare_1 - ~name:"injecting_pending" - ~msg:"Injecting {count} pending operations" - ~level:Notice - ("count", Data_encoding.int31) - - let pp_operations_list ppf operations = - Format.fprintf - ppf - "@[%a@]" - (Format.pp_print_list L1_operation.pp) - operations - - let pp_operations_hash_list ppf operations = - Format.fprintf - ppf - "@[%a@]" - (Format.pp_print_list L1_operation.Hash.pp) - operations - - let injecting_operations = - declare_1 - ~name:"injecting_operations" - ~msg:"Injecting operations: {operations}" - ~level:Notice - ("operations", Data_encoding.list L1_operation.encoding) - ~pp1:pp_operations_list - - let simulating_operations = - declare_2 - ~name:"simulating_operations" - ~msg:"Simulating operations (force = {force}): {operations}" - ~level:Debug - ("operations", Data_encoding.list L1_operation.encoding) - ("force", Data_encoding.bool) - ~pp1:pp_operations_list - - let dropping_operation = - declare_2 - ~name:"dropping_operation" - ~msg:"Dropping operation {operation} failing with {error}" - ~level:Notice - ("operation", L1_operation.encoding) - ~pp1:L1_operation.pp - ("error", Environment.Error_monad.trace_encoding) - ~pp2:Environment.Error_monad.pp_trace - - let injected = - declare_1 - ~name:"injected" - ~msg:"Injected in {oph}" - ~level:Notice - ("oph", Operation_hash.encoding) - - let add_pending = - declare_1 - ~name:"add_pending" - ~msg:"Add {operation} to pending" - ~level:Notice - ("operation", L1_operation.encoding) - ~pp1:L1_operation.pp - - let included = - declare_3 - ~name:"included" - ~msg:"Included operations of {block} at level {level}: {operations}" - ~level:Notice - ("block", Block_hash.encoding) - ("level", Data_encoding.int32) - ("operations", Data_encoding.list L1_operation.Hash.encoding) - ~pp3:pp_operations_hash_list - - let revert_operations = - declare_1 - ~name:"revert_operations" - ~msg:"Reverting operations: {operations}" - ~level:Notice - ("operations", Data_encoding.list L1_operation.Hash.encoding) - ~pp1:pp_operations_hash_list - - let confirmed_level = - declare_1 - ~name:"confirmed_level" - ~msg:"Confirmed Tezos level {level}" - ~level:Notice - ("level", Data_encoding.int32) - - let confirmed_operations = - declare_2 - ~name:"confirmed_operations" - ~msg:"Confirmed operations of level {level}: {operations}" - ~level:Notice - ("level", Data_encoding.int32) - ("operations", Data_encoding.list L1_operation.Hash.encoding) - ~pp2:pp_operations_hash_list -end - module Accuser = struct let section = section @ ["accuser"] diff --git a/src/proto_alpha/lib_tx_rollup/injector.ml b/src/proto_alpha/lib_tx_rollup/injector.ml index 7ba248252e70e4689622267a37ddac135410cc77..c69747ae3d0aa466a76ec16daf4a1822fa0d7254 100644 --- a/src/proto_alpha/lib_tx_rollup/injector.ml +++ b/src/proto_alpha/lib_tx_rollup/injector.ml @@ -23,804 +23,129 @@ (* *) (*****************************************************************************) -open Protocol_client_context -open Protocol -open Alpha_context -open Common -open Injector_worker_types - -(* This is the Tenderbake finality for blocks. *) -(* TODO: https://gitlab.com/tezos/tezos/-/issues/2815 - Centralize this and maybe make it configurable. *) -let confirmations = 2 - -module Op_queue = Hash_queue.Make (L1_operation.Hash) (L1_operation) - -type injection_strategy = Each_block | Delay_block - -(** Information stored about an L1 operation that was injected on a Tezos - node. *) -type injected_info = { - op : L1_operation.t; (** The L1 manager operation. *) - oph : Operation_hash.t; - (** The hash of the operation which contains [op] (this can be an L1 batch of - several manager operations). *) -} - -(** The part of the state which gathers information about injected - operations (but not included). *) -type injected_state = { - injected_operations : injected_info L1_operation.Hash.Table.t; - (** A table mapping L1 manager operation hashes to the injection info for that - operation. *) - injected_ophs : L1_operation.Hash.t list Operation_hash.Table.t; - (** A mapping of all L1 manager operations contained in a L1 batch (i.e. an L1 - operation). *) -} - -(** Information stored about an L1 operation that was included in a Tezos - block. *) -type included_info = { - op : L1_operation.t; (** The L1 manager operation. *) - oph : Operation_hash.t; - (** The hash of the operation which contains [op] (this can be an L1 batch of - several manager operations). *) - l1_block : Block_hash.t; - (** The hash of the L1 block in which the operation was included. *) - l1_level : int32; (** The level of [l1_block]. *) -} - -(** The part of the state which gathers information about - operations which are included in the L1 chain (but not confirmed). *) -type included_state = { - included_operations : included_info L1_operation.Hash.Table.t; - included_in_blocks : (int32 * L1_operation.Hash.t list) Block_hash.Table.t; -} - -(* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2755 - Persist injector data on disk *) - -(** The internal state of each injector worker. *) -type state = { - cctxt : Protocol_client_context.full; - (** The client context which is used to perform the injections. *) - signer : signer; (** The signer for this worker. *) - tags : tags; - (** The tags of this worker, for both informative and identification - purposes. *) - strategy : injection_strategy; - (** The strategy of this worker for injecting the pending operations. *) - queue : Op_queue.t; (** The queue of pending operations for this injector. *) - injected : injected_state; (** The information about injected operations. *) - included : included_state; - (** The information about included operations. {b Note}: Operations which - are confirmed are simply removed from the state and do not appear - anymore. *) - rollup_node_state : State.t; - (** The state of the rollup node (essentially to access the stores). *) -} - -(** Builds a client context from another client context but uses logging instead - of printing on stdout directly. This client context cannot make the injector - exit. *) -let injector_context (cctxt : #Client_context.full) = - let log _channel msg = Logs_lwt.info (fun m -> m "%s" msg) in - object - inherit - Protocol_client_context.wrap_full - (new Client_context.proxy_context (cctxt :> Client_context.full)) - - inherit! Client_context.simple_printer log - - method! exit code = - Format.ksprintf Stdlib.failwith "Injector client wants to exit %d" code +open Protocol.Alpha_context +open Injector_sigs + +type tag = + | Commitment + | Submit_batch + | Finalize_commitment + | Remove_commitment + | Rejection + | Dispatch_withdrawals + +module Parameters = struct + type rollup_node_state = State.t + + let events_section = ["tx_rollup_node"; "injector"] + + module Tag = struct + type t = tag + + let compare = Stdlib.compare + + let string_of_tag : t -> string = function + | Submit_batch -> "submit_batch" + | Commitment -> "commitment" + | Finalize_commitment -> "finalize_commitment" + | Remove_commitment -> "remove_commitment" + | Rejection -> "rejection" + | Dispatch_withdrawals -> "dispatch_withdrawals" + + let pp ppf t = Format.pp_print_string ppf (string_of_tag t) + + let encoding : t Data_encoding.t = + let open Data_encoding in + string_enum + (List.map + (fun t -> (string_of_tag t, t)) + [ + Submit_batch; + Commitment; + Finalize_commitment; + Remove_commitment; + Rejection; + Dispatch_withdrawals; + ]) end -let init_injector rollup_node_state ~signer strategy tags = - let open Lwt_result_syntax in - let+ signer = get_signer rollup_node_state.State.cctxt signer in - let queue = Op_queue.create 50_000 in (* Very coarse approximation for the number of operation we expect for each block *) - let n = - Tags.fold - (fun t acc -> - let n = - match t with - | `Commitment -> 3 - | `Submit_batch -> 509 - | `Finalize_commitment -> 3 - | `Remove_commitment -> 3 - | `Rejection -> 3 - | `Dispatch_withdrawals -> 89 - in - acc + n) - tags - 0 - in - { - cctxt = injector_context rollup_node_state.State.cctxt; - signer; - tags; - strategy; - queue; - injected = - { - injected_operations = L1_operation.Hash.Table.create n; - injected_ophs = Operation_hash.Table.create n; - }; - included = + let table_estimated_size = function + | Commitment -> 3 + | Submit_batch -> 509 + | Finalize_commitment -> 3 + | Remove_commitment -> 3 + | Rejection -> 3 + | Dispatch_withdrawals -> 89 + + let fee_parameter _ _ = + Injection. { - included_operations = L1_operation.Hash.Table.create (confirmations * n); - included_in_blocks = Block_hash.Table.create (confirmations * n); - }; - rollup_node_state; - } - -module Event = struct - include Event - - let emit1 e state x = Event.emit e (state.signer.pkh, state.tags, x) - - let emit2 e state x y = Event.emit e (state.signer.pkh, state.tags, x, y) - - let emit3 e state x y z = Event.emit e (state.signer.pkh, state.tags, x, y, z) -end - -(** Add an operation to the pending queue corresponding to the signer for this - operation. *) -let add_pending_operation state op = - let open Lwt_syntax in - let+ () = Event.(emit1 Injector.add_pending) state op in - Op_queue.replace state.queue op.L1_operation.hash op - -(** Mark operations as injected (in [oph]). *) -let add_injected_operations state oph operations = - let infos = - List.map (fun op -> (op.L1_operation.hash, {op; oph})) operations - in - L1_operation.Hash.Table.replace_seq - state.injected.injected_operations - (List.to_seq infos) ; - Operation_hash.Table.replace - state.injected.injected_ophs - oph - (List.map fst infos) - -(** [add_included_operations state oph l1_block l1_level operations] marks the - [operations] as included (in the L1 batch [oph]) in the Tezos block - [l1_block] of level [l1_level]. *) -let add_included_operations state oph l1_block l1_level operations = - let open Lwt_syntax in - let+ () = - Event.(emit3 Injector.included) - state - l1_block - l1_level - (List.map (fun o -> o.L1_operation.hash) operations) - in - let infos = - List.map - (fun op -> (op.L1_operation.hash, {op; oph; l1_block; l1_level})) - operations - in - L1_operation.Hash.Table.replace_seq - state.included.included_operations - (List.to_seq infos) ; - Block_hash.Table.replace - state.included.included_in_blocks - l1_block - (l1_level, List.map fst infos) - -(** [remove state oph] removes the operations that correspond to the L1 batch - [oph] from the injected operations in the injector state. This function is - used to move operations from injected to included. *) -let remove_injected_operation state oph = - match Operation_hash.Table.find state.injected.injected_ophs oph with - | None -> - (* Nothing removed *) - [] - | Some mophs -> - Operation_hash.Table.remove state.injected.injected_ophs oph ; - List.fold_left - (fun removed moph -> - match - L1_operation.Hash.Table.find state.injected.injected_operations moph - with - | None -> removed - | Some info -> - L1_operation.Hash.Table.remove - state.injected.injected_operations - moph ; - info :: removed) - [] - mophs - -(** [remove state block] removes the included operations that correspond to all - the L1 batches included in [block]. This function is used when [block] is on - an alternative chain in the case of a reorganization. *) -let remove_included_operation state block = - match Block_hash.Table.find state.included.included_in_blocks block with - | None -> - (* Nothing removed *) - [] - | Some (_level, mophs) -> - Block_hash.Table.remove state.included.included_in_blocks block ; - List.fold_left - (fun removed moph -> - match - L1_operation.Hash.Table.find state.included.included_operations moph - with - | None -> removed - | Some info -> - L1_operation.Hash.Table.remove - state.included.included_operations - moph ; - info :: removed) - [] - mophs - -(** Simulate the injection of [operations]. See {!inject_operations} for the - specification of [must_succeed]. *) -let simulate_operations ~must_succeed state signer - (operations : L1_operation.t list) = - let open Lwt_result_syntax in - let open Annotated_manager_operation in - let force = - match operations with - | [] -> assert false - | [_] -> - (* If there is only one operation, fail when simulation fails *) - false - | _ -> ( - (* We want to see which operation failed in the batch if not all must - succeed *) - match must_succeed with `All -> false | `At_least_one -> true) - in - let*! () = - Event.(emit2 Injector.simulating_operations) state operations force - in - let operations = - List.map - (fun {L1_operation.manager_operation = Manager operation; _} -> - Annotated_manager_operation - (Injection.prepare_manager_operation - ~fee:Limit.unknown - ~gas_limit:Limit.unknown - ~storage_limit:Limit.unknown - operation)) - operations - in - let (Manager_list annot_op) = - Annotated_manager_operation.manager_of_list operations - in - let* (oph, op, result) = - Injection.inject_manager_operation - state.cctxt - ~simulation:true (* Only simulation here *) - ~force - ~chain:state.cctxt#chain - ~block:(`Head 0) - ~source:signer.pkh - ~src_pk:signer.pk - ~src_sk:signer.sk - ~successor_level: - true (* Needed to simulate tx_rollup operations in the next block *) - ~fee:Limit.unknown - ~gas_limit:Limit.unknown - ~storage_limit:Limit.unknown - ~fee_parameter: - { - minimal_fees = Tez.of_mutez_exn 100L; - minimal_nanotez_per_byte = Q.of_int 1000; - minimal_nanotez_per_gas_unit = Q.of_int 100; - force_low_fee = false; - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2811 - Use acceptable values wrt operations to inject *) - fee_cap = Tez.one; - burn_cap = Tez.one; - } - annot_op - in - return (oph, Contents_list op, Apply_results.Contents_result_list result) - -let inject_on_node state packed_contents = - let open Lwt_result_syntax in - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2815 *) - (* Branch to head - 2 for tenderbake *) - let* branch = - Tezos_shell_services.Shell_services.Blocks.hash - state.cctxt - ~chain:state.cctxt#chain - ~block:(`Head 2) - () - in - let unsigned_op_bytes = - Data_encoding.Binary.to_bytes_exn - Operation.unsigned_encoding - ({branch}, packed_contents) - in - let* signature = - Client_keys.sign - state.cctxt - ~watermark:Signature.Generic_operation - state.signer.sk - unsigned_op_bytes - in - let (Contents_list contents) = packed_contents in - let op : _ Operation.t = - {shell = {branch}; protocol_data = {contents; signature = Some signature}} - in - let op_bytes = - Data_encoding.Binary.to_bytes_exn Operation.encoding (Operation.pack op) - in - Tezos_shell_services.Shell_services.Injection.operation - state.cctxt - ~chain:state.cctxt#chain - op_bytes - >>=? fun oph -> - let*! () = Event.(emit1 Injector.injected) state oph in - return oph - -(** Inject the given [operations] in an L1 batch. If [must_succeed] is [`All] - then all the operations must succeed in the simulation of injection. If - [must_succeed] is [`At_least_one] at least one operation in the list - [operations] must be successful in the simulation. In any case, only - operations which are known as successful will be included in the injected L1 - batch. {b Note}: [must_succeed = `At_least_one] allows to incrementally build - "or-batches" by iteratively removing operations that fail from the desired - batch. *) -let rec inject_operations ~must_succeed state (operations : L1_operation.t list) - = - let open Lwt_result_syntax in - let* (_oph, packed_contents, result) = - simulate_operations ~must_succeed state state.signer operations - in - let results = Apply_results.to_list result in - let failure = ref false in - let* rev_non_failing_operations = - List.fold_left2_s - ~when_different_lengths: - [ - Exn - (Failure - "Unexpected error: length of operations and result differ in \ - simulation"); - ] - (fun acc op (Apply_results.Contents_result result) -> - match result with - | Apply_results.Manager_operation_result - {operation_result = Failed (_, error); _} -> - let*! () = - Event.(emit2 Injector.dropping_operation) state op error - in - failure := true ; - Lwt.return acc - | Apply_results.Manager_operation_result - {operation_result = Applied _ | Backtracked _ | Skipped _; _} -> - (* Not known to be failing *) - Lwt.return (op :: acc) - | _ -> - (* Only manager operations *) - assert false) - [] - operations - results - in - if !failure then - (* Invariant: must_succeed = `At_least_one, otherwise the simulation would have - returned an error. We try to inject without the failing operation. *) - let operations = List.rev rev_non_failing_operations in - inject_operations ~must_succeed state operations - else - (* Inject on node for real *) - let+ oph = inject_on_node state packed_contents in - (oph, operations) - -(** Returns the (upper bound on) the size of an L1 batch of operations composed - of the manager operations [rev_ops]. *) -let size_l1_batch signer rev_ops = - let contents_list = - List.map - (fun (op : L1_operation.t) -> - let (Manager operation) = op.manager_operation in - let contents = - Manager_operation - { - source = op.source; - operation; - (* Below are dummy values that are only used to approximate the - size. It is thus important that they remain above the real - values if we want the computed size to be an over_approximation - (without having to do a simulation first). *) - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2812 - check the size, or compute them wrt operation kind *) - fee = Tez.of_mutez_exn 3_000_000L; - counter = Z.of_int 500_000; - gas_limit = Gas.Arith.integral_of_int_exn 500_000; - storage_limit = Z.of_int 500_000; - } - in - Contents contents) - rev_ops - in - let (Contents_list contents) = - match Operation.of_list contents_list with - | Error _ -> - (* Cannot happen: rev_ops is non empty and contains only manager - operations *) - assert false - | Ok packed_contents_list -> packed_contents_list - in - let signature = - match signer.pkh with - | Signature.Ed25519 _ -> Signature.of_ed25519 Ed25519.zero - | Secp256k1 _ -> Signature.of_secp256k1 Secp256k1.zero - | P256 _ -> Signature.of_p256 P256.zero - in - let branch = Block_hash.zero in - let operation = + minimal_fees = Tez.of_mutez_exn 100L; + minimal_nanotez_per_byte = Q.of_int 1000; + minimal_nanotez_per_gas_unit = Q.of_int 100; + force_low_fee = false; + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2811 + Use acceptable values wrt operations to inject *) + fee_cap = Tez.one; + burn_cap = Tez.one; + } + + (* Below are dummy values that are only used to approximate the + size. It is thus important that they remain above the real + values if we want the computed size to be an over_approximation + (without having to do a simulation first). *) + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2812 + check the size, or compute them wrt operation kind *) + let approximate_fee_bound _ _ = { - shell = {branch}; - protocol_data = Operation_data {contents; signature = Some signature}; + fee = Tez.of_mutez_exn 3_000_000L; + counter = Z.of_int 500_000; + gas_limit = Gas.Arith.integral_of_int_exn 500_000; + storage_limit = Z.of_int 500_000; } - in - Data_encoding.Binary.length Operation.encoding operation - -(** Retrieve as many operations from the queue while remaining below the size - limit. *) -let get_operations_from_queue ~size_limit state = - let exception Reached_limit of L1_operation.t list in - let rev_ops = - try - Op_queue.fold - (fun _oph op ops -> - let new_ops = op :: ops in - let new_size = size_l1_batch state.signer new_ops in - if new_size > size_limit then raise (Reached_limit ops) ; - new_ops) - state.queue - [] - with Reached_limit ops -> ops - in - List.rev rev_ops -(* Ignore the failures of finalize and remove commitment operations. These - operations fail when there are either no commitment to finalize or to remove - (which can happen when there are no inbox for instance). *) -let ignore_failing_gc_operations operations = function - | Ok res -> Ok (`Injected res) - | Error _ as res -> - let only_gc_operations = - List.for_all - (fun op -> - let (Manager op) = op.L1_operation.manager_operation in - match op with - | Tx_rollup_finalize_commitment _ | Tx_rollup_remove_commitment _ -> - true - | _ -> false) - operations - in - if only_gc_operations then Ok `Ignored else res + (* TODO: https://gitlab.com/tezos/tezos/-/issues/2813 + Decide if some operations must all succeed *) + let batch_must_succeed _ = `At_least_one -(** [inject_pending_operations_for ~size_limit state pending] injects - operations from the pending queue [pending], whose total size does - not exceed [size_limit]. Upon successful injection, the - operations are removed from the queue and marked as injected. *) -let inject_pending_operations - ?(size_limit = Constants.max_operation_data_length) state = - let open Lwt_result_syntax in - (* Retrieve and remove operations from pending *) - let operations_to_inject = get_operations_from_queue ~size_limit state in - match operations_to_inject with - | [] -> return_unit - | _ -> ( - let*! () = - Event.(emit1 Injector.injecting_pending) - state - (List.length operations_to_inject) - in - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2813 - Decide if some operations must all succeed *) - let*! res = - inject_operations ~must_succeed:`At_least_one state operations_to_inject - in - let*? res = ignore_failing_gc_operations operations_to_inject res in - match res with - | `Injected (oph, injected_operations) -> - (* Injection succeeded, remove from pending and add to injected *) - List.iter - (fun op -> Op_queue.remove state.queue op.L1_operation.hash) - injected_operations ; - add_injected_operations state oph operations_to_inject ; - return_unit - | `Ignored -> - (* Injection failed but we ignore the failure. We can leave the GC - operations in the queue as their can be only one unique. *) - return_unit) + let ignore_failing_operation : type kind. kind manager_operation -> _ = + function + | Tx_rollup_remove_commitment _ | Tx_rollup_finalize_commitment _ -> + (* We can keep these operations as there will be at most one of them in + the queue at any given time. *) + `Ignore_keep + | _ -> `Don't_ignore -(** [register_included_operation state block level oph] marks the manager - operations contained in the L1 batch [oph] as being included in the [block] - of level [level], by moving them from the "injected" state to the "included" - state. *) -let register_included_operation state block level oph = - match remove_injected_operation state oph with - | [] -> Lwt.return_unit - | injected_infos -> - let included_mops = - List.map (fun (i : injected_info) -> i.op) injected_infos - in - add_included_operations state oph block level included_mops - -(** [register_included_operations state block level oph] marks the known (by - this injector) manager operations contained in [block] as being included. *) -let register_included_operations state (block : Alpha_block_services.block_info) - = - List.iter_s - (List.iter_s (fun (op : Alpha_block_services.operation) -> - register_included_operation - state - block.hash - block.header.shell.level - op.hash - (* TODO/TORU: Handle operations for rollup_id here with - callback *))) - block.Alpha_block_services.operations - -(** Returns [true] if an included operation should be re-queued for injection + (** Returns [true] if an included operation should be re-queued for injection when the block in which it is included is reverted (due to a reorganization). *) -let requeue_reverted_operation state op = - let open Lwt_syntax in - let (Manager operation) = op.L1_operation.manager_operation in - match operation with - | Tx_rollup_rejection _ -> - (* TODO: check if rejected commitment in still in main chain *) - return_true - | Tx_rollup_commit {commitment; _} -> ( - let level = L2block.Rollup_level commitment.level in - let* l2_block = State.get_level_l2_block state.rollup_node_state level in - match l2_block with - | None -> - (* We don't know this L2 block, should not happen *) - let+ () = Debug_events.(emit should_not_happen) __LOC__ in - false - | Some l2_block -> ( - match l2_block.L2block.header.commitment with - | None -> return_false - | Some c -> - let commit_hash = - Tx_rollup_commitment.(Compact.hash (Full.compact commitment)) - in - (* Do not re-queue if commitment for this level has changed *) - return Tx_rollup_commitment_hash.(c = commit_hash))) - | _ -> return_true - -(** [revert_included_operations state block] marks the known (by this injector) - manager operations contained in [block] as not being included any more, - typically in the case of a reorganization where [block] is on an alternative - chain. The operations are put back in the pending queue. *) -let revert_included_operations state block = - let open Lwt_syntax in - let included_infos = remove_included_operation state block in - let* () = - Event.(emit1 Injector.revert_operations) - state - (List.map (fun o -> o.op.hash) included_infos) - in - (* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2814 - maybe put at the front of the queue for re-injection. *) - List.iter_s - (fun {op; _} -> - let* requeue = requeue_reverted_operation state op in - if requeue then add_pending_operation state op else return_unit) - included_infos - -(** [register_confirmed_level state confirmed_level] is called when the level - [confirmed_level] is known as confirmed. In this case, the operations of - block which are below this level are also considered as confirmed and are - removed from the "included" state. These operations cannot be part of a - reorganization so there will be no need to re-inject them anymore. *) -let register_confirmed_level state confirmed_level = - let open Lwt_syntax in - let* () = - Event.(emit Injector.confirmed_level) - (state.signer.pkh, state.tags, confirmed_level) - in - Block_hash.Table.iter_s - (fun block (level, _operations) -> - if level <= confirmed_level then - let confirmed_ops = remove_included_operation state block in - Event.(emit2 Injector.confirmed_operations) - state - level - (List.map (fun o -> o.op.hash) confirmed_ops) - else Lwt.return_unit) - state.included.included_in_blocks - -(** [on_new_tezos_head state head reorg] is called when there is a new Tezos - head (with a potential reorganization [reorg]). It first reverts any blocks - that are in the alternative branch of the reorganization and then registers - the effect of the new branch (the newly included operation and confirmed - operations). *) -let on_new_tezos_head state (head : Alpha_block_services.block_info) - (reorg : Alpha_block_services.block_info reorg) = - let open Lwt_result_syntax in - let*! () = Event.(emit1 Injector.new_tezos_head) state head.hash in - let*! () = - List.iter_s - (fun removed_block -> - revert_included_operations state removed_block.Alpha_block_services.hash) - (List.rev reorg.old_chain) - in - let*! () = - List.iter_s - (fun added_block -> register_included_operations state added_block) - reorg.new_chain - in - (* Head is already included in the reorganization, so no need to process it - separately. *) - let confirmed_level = - Int32.sub - head.Alpha_block_services.header.shell.level - (Int32.of_int confirmations) - in - let*! () = - if confirmed_level >= 0l then register_confirmed_level state confirmed_level - else Lwt.return_unit - in - return_unit - -(* The request {Request.Inject} triggers an injection of the operations - the pending queue. *) -let on_inject state = inject_pending_operations state - -module Types = struct - type nonrec state = state - - type parameters = { - rollup_node_state : State.t; - strategy : injection_strategy; - tags : Tags.t; - } + let requeue_reverted_operation (type kind) state + (operation : kind manager_operation) = + let open Lwt_syntax in + match operation with + | Tx_rollup_rejection _ -> + (* TODO: check if rejected commitment in still in main chain *) + return_true + | Tx_rollup_commit {commitment; _} -> ( + let level = L2block.Rollup_level commitment.level in + let* l2_block = State.get_level_l2_block state level in + match l2_block with + | None -> + (* We don't know this L2 block, should not happen *) + let+ () = Debug_events.(emit should_not_happen) __LOC__ in + false + | Some l2_block -> ( + match l2_block.L2block.header.commitment with + | None -> return_false + | Some c -> + let commit_hash = + Tx_rollup_commitment.(Compact.hash (Full.compact commitment)) + in + (* Do not re-queue if commitment for this level has changed *) + return Tx_rollup_commitment_hash.(c = commit_hash))) + | _ -> return_true end -(* The worker for the injector. *) -module Worker = Worker.Make (Name) (Dummy_event) (Request) (Types) (Logger) - -(* The queue for the requests to the injector worker is infinite. *) -type worker = Worker.infinite Worker.queue Worker.t - -module Handlers = struct - type self = worker - - let on_request : type r. worker -> r Request.t -> r tzresult Lwt.t = - fun w request -> - let open Lwt_result_syntax in - let state = Worker.state w in - match request with - | Request.Add_pending op -> - let*! () = add_pending_operation state op in - return_unit - | Request.New_tezos_head (head, reorg) -> on_new_tezos_head state head reorg - | Request.Inject -> on_inject state - - let on_request w r = - (* The execution of the request handler is protected to avoid stopping the - worker in case of an exception. *) - protect @@ fun () -> on_request w r - - let on_launch _w signer Types.{rollup_node_state; strategy; tags} = - init_injector rollup_node_state ~signer strategy tags - - let on_error w r st errs = - let open Lwt_result_syntax in - let state = Worker.state w in - (* Errors do not stop the worker but emit an entry in the log. *) - let*! () = Event.(emit3 Injector.request_failed) state r st errs in - return_unit - - let on_completion w r _ st = - let state = Worker.state w in - match Request.view r with - | Request.View (Add_pending _ | New_tezos_head _) -> - Event.(emit2 Injector.request_completed_debug) state (Request.view r) st - | View Inject -> - Event.(emit2 Injector.request_completed_notice) - state - (Request.view r) - st - - let on_no_request _ = return_unit - - let on_close _w = Lwt.return_unit -end - -let table = Worker.create_table Queue - -(* TODO/TORU: https://gitlab.com/tezos/tezos/-/issues/2754 - Injector worker in a separate process *) -let init rollup_node_state ~signers = - let open Lwt_result_syntax in - let signers_map = - List.fold_left - (fun acc (signer, strategy, tags) -> - let tags = Tags.of_list tags in - let (strategy, tags) = - match Signature.Public_key_hash.Map.find_opt signer acc with - | None -> (strategy, tags) - | Some (other_strategy, other_tags) -> - let strategy = - match (strategy, other_strategy) with - | (Each_block, Each_block) -> Each_block - | (Delay_block, _) | (_, Delay_block) -> - (* Delay_block strategy takes over because we can always wait a - little bit more to inject operation which are to be injected - "each block". *) - Delay_block - in - (strategy, Tags.union other_tags tags) - in - Signature.Public_key_hash.Map.add signer (strategy, tags) acc) - Signature.Public_key_hash.Map.empty - signers - in - Signature.Public_key_hash.Map.iter_es - (fun signer (strategy, tags) -> - let+ worker = - Worker.launch - table - signer - {rollup_node_state; strategy; tags} - (module Handlers) - in - ignore worker) - signers_map - -let worker_of_signer signer_pkh = - match Worker.find_opt table signer_pkh with - | None -> - (* TODO: https://gitlab.com/tezos/tezos/-/issues/2818 - maybe lazily start worker here *) - error (Error.No_worker_for_source signer_pkh) - | Some worker -> ok worker - -let add_pending_operation op = - let open Lwt_result_syntax in - let*? w = worker_of_signer op.L1_operation.source in - let*! () = Worker.Queue.push_request w (Request.Add_pending op) in - return_unit - -let add_pending_operations ops = List.iter_es add_pending_operation ops - -let new_tezos_head h reorg = - let workers = Worker.list table in - List.iter_p - (fun (_signer, w) -> - Worker.Queue.push_request w (Request.New_tezos_head (h, reorg))) - workers - -let has_tag_in ~tags state = - match tags with - | None -> - (* Not filtering on tags *) - true - | Some tags -> not (Tags.disjoint state.tags tags) - -let has_strategy ~strategy state = - match strategy with - | None -> - (* Not filtering on strategy *) - true - | Some strategy -> state.strategy = strategy - -let inject ?tags ?strategy () = - let workers = Worker.list table in - let tags = Option.map Tags.of_list tags in - List.iter_p - (fun (_signer, w) -> - let worker_state = Worker.state w in - if has_tag_in ~tags worker_state && has_strategy ~strategy worker_state - then Worker.Queue.push_request w Request.Inject - else Lwt.return_unit) - workers +include Injector_functor.Make (Parameters) diff --git a/src/proto_alpha/lib_tx_rollup/injector.mli b/src/proto_alpha/lib_tx_rollup/injector.mli index 7b78bda92d2381494e6a69dfabb25a2dee2523f5..f23eda69a9a4d29fa898fc6f40a7de6778263ed9 100644 --- a/src/proto_alpha/lib_tx_rollup/injector.mli +++ b/src/proto_alpha/lib_tx_rollup/injector.mli @@ -23,48 +23,13 @@ (* *) (*****************************************************************************) -open Protocol_client_context -open Protocol -open Alpha_context -open Common +type tag = + | Commitment + | Submit_batch + | Finalize_commitment + | Remove_commitment + | Rejection + | Dispatch_withdrawals -type injection_strategy = - | Each_block (** Inject pending operations after each new L1 block *) - | Delay_block - (** Wait for some time after the L1 block is produced to inject pending - operations. This strategy allows for maximizing the number of the same - kind of operations to include in a block. *) - -(** Initializes the injector with the rollup node state, for a list of - signers. Each signer has its own worker with a queue of operations to - inject. *) -val init : - State.t -> - signers: - (public_key_hash * injection_strategy * Injector_worker_types.tag list) list -> - unit tzresult Lwt.t - -(** Add an operation as pending injection in the injector. *) -val add_pending_operation : L1_operation.t -> unit tzresult Lwt.t - -(** Add multiple operations as pending injection in the injector. *) -val add_pending_operations : L1_operation.t trace -> unit tzresult Lwt.t - -(** Notify the injector of a new Tezos head. The injector marks the operations - appropriately (for instance reverted operations that are part of a - reorganization are put back in the pending queue). When an operation is - considered as {e confirmed}, it disappears from the injector. *) -val new_tezos_head : - Alpha_block_services.block_info -> - Alpha_block_services.block_info reorg -> - unit Lwt.t - -(** Trigger an injection of the pending operations for all workers. If [tags] - is given, only the workers which have a tag in [tags] inject their pending - operations. If [strategy] is given, only workers which have this strategy - inject their pending operations. *) -val inject : - ?tags:Injector_worker_types.tag list -> - ?strategy:injection_strategy -> - unit -> - unit Lwt.t +include + Injector_sigs.S with type rollup_node_state := State.t and type tag := tag diff --git a/src/proto_alpha/lib_tx_rollup/state.ml b/src/proto_alpha/lib_tx_rollup/state.ml index eb715fc15b938206ea640131dacff3e43ef068c4..648a3ad33b04f9695f27714d2d6bfe720199b95f 100644 --- a/src/proto_alpha/lib_tx_rollup/state.ml +++ b/src/proto_alpha/lib_tx_rollup/state.ml @@ -62,61 +62,23 @@ let get_head state = state.head let fetch_tezos_block state hash = let open Lwt_syntax in - let fetch hash = + let errors = ref [] in + let find_in_cache hash fetch = + let fetch hash = + let* block = fetch hash in + match block with + | Error errs -> + errors := errs ; + return_none + | Ok block -> return_some block + in let+ block = - Alpha_block_services.info - state.cctxt - ~chain:state.cctxt#chain - ~block:(`Hash (hash, 0)) - () + Tezos_blocks_cache.find_or_replace state.tezos_blocks_cache hash fetch in - Result.to_option block - in - let+ block = - Tezos_blocks_cache.find_or_replace state.tezos_blocks_cache hash fetch + Result.of_option ~error:!errors block + |> record_trace (Error.Tx_rollup_cannot_fetch_tezos_block hash) in - Result.of_option ~error:[Error.Tx_rollup_cannot_fetch_tezos_block hash] block - -(* Compute the reorganization of L1 blocks from the chain whose head is - [old_head_hash] and the chain whose head [new_head_hash]. *) -let tezos_reorg state ~old_head_hash ~new_head_hash = - let open Lwt_result_syntax in - let rec loop old_chain new_chain old_head_hash new_head_hash = - if Block_hash.(old_head_hash = new_head_hash) then - let+ ancestor = fetch_tezos_block state old_head_hash in - { - ancestor = Some ancestor; - old_chain = List.rev old_chain; - new_chain = List.rev new_chain; - } - else - let* new_head = fetch_tezos_block state new_head_hash in - let* old_head = fetch_tezos_block state old_head_hash in - let old_level = old_head.header.shell.level in - let new_level = new_head.header.shell.level in - let diff = Int32.sub new_level old_level in - let (old_chain, new_chain, old, new_) = - if diff = 0l then - (* Heads at same level *) - let new_chain = new_head :: new_chain in - let old_chain = old_head :: old_chain in - let new_head_hash = new_head.header.shell.predecessor in - let old_head_hash = old_head.header.shell.predecessor in - (old_chain, new_chain, old_head_hash, new_head_hash) - else if diff > 0l then - (* New chain is longer *) - let new_chain = new_head :: new_chain in - let new_head_hash = new_head.header.shell.predecessor in - (old_chain, new_chain, old_head_hash, new_head_hash) - else - (* Old chain was longer *) - let old_chain = old_head :: old_chain in - let old_head_hash = old_head.header.shell.predecessor in - (old_chain, new_chain, old_head_hash, new_head_hash) - in - loop old_chain new_chain old new_ - in - loop [] [] old_head_hash new_head_hash + fetch_tezos_block ~find_in_cache state.cctxt hash let set_tezos_head state new_head_hash = let open Lwt_result_syntax in @@ -127,8 +89,9 @@ let set_tezos_head state new_head_hash = (* No known tezos head, consider the new head as being on top of a previous tezos block. *) let+ new_head = fetch_tezos_block state new_head_hash in - {ancestor = None; old_chain = []; new_chain = [new_head]} - | Some old_head_hash -> tezos_reorg state ~old_head_hash ~new_head_hash + {old_chain = []; new_chain = [new_head]} + | Some old_head_hash -> + tezos_reorg (fetch_tezos_block state) ~old_head_hash ~new_head_hash in let* () = Stores.Tezos_head_store.write state.stores.tezos_head new_head_hash @@ -208,20 +171,11 @@ let rollup_reorg state ~old_head ~new_head = let rec loop old_chain new_chain old_head new_head = match (old_head, new_head) with | (None, _) | (_, None) -> - return - { - ancestor = None; - old_chain = List.rev old_chain; - new_chain = List.rev new_chain; - } + return {old_chain = List.rev old_chain; new_chain = List.rev new_chain} | (Some old_head, Some new_head) -> if L2block.Hash.(old_head.L2block.hash = new_head.L2block.hash) then return - { - ancestor = Some old_head; - old_chain = List.rev old_chain; - new_chain = List.rev new_chain; - } + {old_chain = List.rev old_chain; new_chain = List.rev new_chain} else let diff = distance_l2_levels diff --git a/src/proto_alpha/lib_tx_rollup/tezos-tx-rollup-alpha.opam b/src/proto_alpha/lib_tx_rollup/tezos-tx-rollup-alpha.opam index 2e34c83ae9635130e680b440cb0d564c786b5f6a..d37a20087bf2345fb9b1cce9228c333729cebddc 100644 --- a/src/proto_alpha/lib_tx_rollup/tezos-tx-rollup-alpha.opam +++ b/src/proto_alpha/lib_tx_rollup/tezos-tx-rollup-alpha.opam @@ -29,6 +29,7 @@ depends: [ "tezos-shell" "tezos-store" "tezos-workers" + "tezos-rollups-alpha" ] build: [ ["dune" "build" "-p" name "-j" jobs] diff --git a/tezt/_regressions/sc_rollup_client_messages.out b/tezt/_regressions/sc_rollup_client_messages.out new file mode 100644 index 0000000000000000000000000000000000000000..bdf6a0bfd45f1af4d40bb8387e7dbb31b5398853 --- /dev/null +++ b/tezt/_regressions/sc_rollup_client_messages.out @@ -0,0 +1,31 @@ +sc_rollup_client_messages.out + +./tezos-client --wait none originate sc rollup from '[PUBLIC_KEY_HASH]' of kind arith booting with --burn-cap 9999999 +Node is bootstrapped. +Estimated gas: 1600.648 units (will add 100 for safety) +Estimated storage: 6522 bytes added (will add 20 for safety) +Operation successfully injected in the node. +Operation hash is '[OPERATION_HASH]' +NOT waiting for the operation to be included. +Use command + tezos-client wait for [OPERATION_HASH] to be included --confirmations 1 --branch [BLOCK_HASH] +and/or an external block explorer to make sure that it has been included. +This sequence of operations was run: + Manager signed operations: + From: [PUBLIC_KEY_HASH] + Fee to the baker: ꜩ0.000402 + Expected counter: 1 + Gas limit: 1701 + Storage limit: 6542 bytes + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ0.000402 + payload fees(the block proposer) ....... +ꜩ0.000402 + Originate smart contract rollup of kind arith with boot sector '' + This smart contract rollup origination was successfully applied + Consumed gas: 1600.648 + Storage size: 6522 bytes + Address: [SC_ROLLUP_HASH] + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ1.6305 + storage fees ........................... +ꜩ1.6305 + diff --git a/tezt/_regressions/sc_rollup_node_publish_messages.out b/tezt/_regressions/sc_rollup_node_publish_messages.out new file mode 100644 index 0000000000000000000000000000000000000000..32d36c59d60f8b1f4f480e0bae47e6c550a9bbbe --- /dev/null +++ b/tezt/_regressions/sc_rollup_node_publish_messages.out @@ -0,0 +1,31 @@ +sc_rollup_node_publish_messages.out + +./tezos-client --wait none originate sc rollup from '[PUBLIC_KEY_HASH]' of kind arith booting with --burn-cap 9999999 +Node is bootstrapped. +Estimated gas: 1600.648 units (will add 100 for safety) +Estimated storage: 6522 bytes added (will add 20 for safety) +Operation successfully injected in the node. +Operation hash is '[OPERATION_HASH]' +NOT waiting for the operation to be included. +Use command + tezos-client wait for [OPERATION_HASH] to be included --confirmations 1 --branch [BLOCK_HASH] +and/or an external block explorer to make sure that it has been included. +This sequence of operations was run: + Manager signed operations: + From: [PUBLIC_KEY_HASH] + Fee to the baker: ꜩ0.000402 + Expected counter: 1 + Gas limit: 1701 + Storage limit: 6542 bytes + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ0.000402 + payload fees(the block proposer) ....... +ꜩ0.000402 + Originate smart contract rollup of kind arith with boot sector '' + This smart contract rollup origination was successfully applied + Consumed gas: 1600.648 + Storage size: 6522 bytes + Address: [SC_ROLLUP_HASH] + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ1.6305 + storage fees ........................... +ꜩ1.6305 + diff --git a/tezt/_regressions/sc_rollup_node_publish_messages_above_batch_limit.out b/tezt/_regressions/sc_rollup_node_publish_messages_above_batch_limit.out new file mode 100644 index 0000000000000000000000000000000000000000..254400ef7c2d43b221e9352e14b3bc50d5ad6adc --- /dev/null +++ b/tezt/_regressions/sc_rollup_node_publish_messages_above_batch_limit.out @@ -0,0 +1,31 @@ +sc_rollup_node_publish_messages_above_batch_limit.out + +./tezos-client --wait none originate sc rollup from '[PUBLIC_KEY_HASH]' of kind arith booting with --burn-cap 9999999 +Node is bootstrapped. +Estimated gas: 1600.648 units (will add 100 for safety) +Estimated storage: 6522 bytes added (will add 20 for safety) +Operation successfully injected in the node. +Operation hash is '[OPERATION_HASH]' +NOT waiting for the operation to be included. +Use command + tezos-client wait for [OPERATION_HASH] to be included --confirmations 1 --branch [BLOCK_HASH] +and/or an external block explorer to make sure that it has been included. +This sequence of operations was run: + Manager signed operations: + From: [PUBLIC_KEY_HASH] + Fee to the baker: ꜩ0.000402 + Expected counter: 1 + Gas limit: 1701 + Storage limit: 6542 bytes + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ0.000402 + payload fees(the block proposer) ....... +ꜩ0.000402 + Originate smart contract rollup of kind arith with boot sector '' + This smart contract rollup origination was successfully applied + Consumed gas: 1600.648 + Storage size: 6522 bytes + Address: [SC_ROLLUP_HASH] + Balance updates: + [PUBLIC_KEY_HASH] ... -ꜩ1.6305 + storage fees ........................... +ꜩ1.6305 + diff --git a/tezt/lib_tezos/sc_rollup_client.ml b/tezt/lib_tezos/sc_rollup_client.ml index 2dac9bf609299b2407444b1f47b8fe5c41ed0bec..9e3ece0919d14f2daf7989c9a5b476df9d4274ad 100644 --- a/tezt/lib_tezos/sc_rollup_client.ml +++ b/tezt/lib_tezos/sc_rollup_client.ml @@ -200,3 +200,26 @@ let spawn_import_secret_key ?hooks ?(force = false) let import_secret_key ?hooks ?force key sc_client = spawn_import_secret_key ?hooks ?force key sc_client |> Process.check + +let publish_message ?hooks message sc_client = + let* out = + spawn_command ?hooks sc_client ["publish"; "message"; message] + |> Process.check_and_read_stdout + in + let op_hash = + out =~* rex "Operation hash: (\\w+)" |> mandatory "operation hash" + in + let state_hash = out =~* rex "State hash: (\\w+)" |> mandatory "state hash" in + let status = out =~* rex "Status: (\\w+)" |> mandatory "status" in + let ticks = out =~* rex "Ticks diff: ([0-9]+)" |> mandatory "ticks" in + return (op_hash, state_hash, status, ticks) + +let batcher_messages ?hooks sc_client = + let open Lwt.Syntax in + let+ res = rpc_get ?hooks sc_client ["batcher"; "messages"] in + res |> JSON.as_list + |> List.map (fun obj -> + match JSON.as_object obj with + | [("hash", hash); ("batch", batch)] -> + (JSON.as_string hash, JSON.encode batch) + | _ -> JSON.error obj "Illformed messages output") diff --git a/tezt/lib_tezos/sc_rollup_client.mli b/tezt/lib_tezos/sc_rollup_client.mli index 71a0a43cef40b7d218d5d33990d799dec03e48ab..307fb4961797c4df47a70f6cc603ed1314a4adf3 100644 --- a/tezt/lib_tezos/sc_rollup_client.mli +++ b/tezt/lib_tezos/sc_rollup_client.mli @@ -107,3 +107,15 @@ val import_secret_key : Account.aggregate_key -> t -> unit Lwt.t + +(** Run [sc_rollup_client publish message] and returns the corresponding L2 + hash, the resulting state hash, PVM status and tick number. *) +val publish_message : + ?hooks:Process.hooks -> + string -> + t -> + (string * string * string * string) Lwt.t + +(** [batcher_messages] gets the current messages waiting for publication in the + node. *) +val batcher_messages : ?hooks:Process.hooks -> t -> (string * string) list Lwt.t diff --git a/tezt/tests/sc_rollup.ml b/tezt/tests/sc_rollup.ml index 7b6098a271cf45fa29000ad01dcb5e7673490818..b5d85b1750740ea73a409578e086f0ed7b0693c9 100644 --- a/tezt/tests/sc_rollup.ml +++ b/tezt/tests/sc_rollup.ml @@ -1471,6 +1471,107 @@ let test_rollup_client_list_keys = pp maybe_keys) +let dummy_message n = if n <= 0 then "0" else Format.sprintf "%d +" n + +(* Messages are ordered from newer to older *) +let publish_dummy_messages sc_rollup_client n = + Lwt_list.fold_left_s + (fun messages n -> + let* hash = + Sc_rollup_client.publish_message (dummy_message n) sc_rollup_client + in + return (hash :: messages)) + [] + (range 0 (n - 1)) + +let test_rollup_client_messages = + let output_file _ = "sc_rollup_client_messages" in + let go sc_rollup_node = + let* () = Sc_rollup_node.run sc_rollup_node in + let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in + let message = "this is a test message" in + let* (hash, _, _, _) = + Sc_rollup_client.publish_message message sc_rollup_client + in + let* messages = Sc_rollup_client.batcher_messages sc_rollup_client in + let (hashes, _) = List.split messages in + Check.([hash] = hashes) + Check.(list string) + ~error_msg:"Injected message is not in the batcher queue" ; + Lwt.return_unit + in + test + ~__FILE__ + ~output_file + ~tags:["run"; "client"; "batcher"] + "Register messages in the batcher" + (fun protocol -> + setup ~protocol @@ fun node client -> + with_fresh_rollup + (fun _sc_rollup_address sc_rollup_node _filename -> go sc_rollup_node) + node + client) + +let test_rollup_node_publish_messages = + let output_file _ = "sc_rollup_node_publish_messages" in + let go sc_rollup_node client = + let* () = Sc_rollup_node.run sc_rollup_node in + let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in + let* _ = publish_dummy_messages sc_rollup_client 3 in + let* _ = Client.bake_for client in + let* messages = Sc_rollup_client.batcher_messages sc_rollup_client in + Check.([] = messages) + Check.(list (tuple2 string string)) + ~error_msg:"Batcher queue is not empty" ; + Lwt.return_unit + in + test + ~__FILE__ + ~output_file + ~tags:["run"; "client"; "batcher"] + "Publish messages with the batcher" + (fun protocol -> + setup ~protocol @@ fun node client -> + with_fresh_rollup + (fun _sc_rollup_address sc_rollup_node _filename -> + go sc_rollup_node client) + node + client) + +let test_rollup_node_publish_messages_above_batch_limit = + let output_file _ = "sc_rollup_node_publish_messages_above_batch_limit" in + let go sc_rollup_node client = + let* () = Sc_rollup_node.run sc_rollup_node in + let sc_rollup_client = Sc_rollup_client.create sc_rollup_node in + (* assumes a batch maximum size is 10 *) + let* published = publish_dummy_messages sc_rollup_client 11 in + let last_published_hash = + match published with (h, _, _, _) :: _ -> [h] | _ -> [] + in + let* _ = Client.bake_for client in + let* messages = Sc_rollup_client.batcher_messages sc_rollup_client in + let (remaining_hashes, _) = List.split messages in + + Check.(remaining_hashes = last_published_hash) + Check.(list string) + ~error_msg: + "Batcher queue does not contain the last message that didn't fit in \ + the batch" ; + Lwt.return_unit + in + test + ~__FILE__ + ~output_file + ~tags:["run"; "client"; "batcher"] + "Send more messages than the batch limit and publish" + (fun protocol -> + setup ~protocol @@ fun node client -> + with_fresh_rollup + (fun _sc_rollup_address sc_rollup_node _filename -> + go sc_rollup_node client) + node + client) + let register ~protocols = test_origination protocols ; test_rollup_node_configuration protocols ; @@ -1503,4 +1604,7 @@ let register ~protocols = test_rollup_node_uses_boot_sector protocols ; test_rollup_client_show_address protocols ; test_rollup_client_generate_keys protocols ; - test_rollup_client_list_keys protocols + test_rollup_client_list_keys protocols ; + test_rollup_client_messages protocols ; + test_rollup_node_publish_messages protocols ; + test_rollup_node_publish_messages_above_batch_limit protocols diff --git a/tezt/tests/tx_rollup_node.ml b/tezt/tests/tx_rollup_node.ml index 383aebf798008c2e32799023953525cb3fc1409a..353172973da53a0dc75e8a7e254499851123e26f 100644 --- a/tezt/tests/tx_rollup_node.ml +++ b/tezt/tests/tx_rollup_node.ml @@ -335,7 +335,6 @@ let test_tx_node_store_inbox = let operator = Constant.bootstrap1.public_key_hash in let*! rollup = Client.Tx_rollup.originate ~src:operator client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 2 in let* block_hash = RPC.get_block_hash client in let tx_node = Rollup_node.create @@ -360,7 +359,6 @@ let test_tx_node_store_inbox = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 3 in let* _ = Rollup_node.wait_for_tezos_level tx_node 3 in let* tx_node_inbox_1 = tx_client_get_inbox ~tx_client ~tezos_client:client ~block:"0" @@ -390,7 +388,6 @@ let test_tx_node_store_inbox = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 4 in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in let* tx_node_inbox_2 = tx_client_get_inbox ~tx_client ~tezos_client:client ~block:"1" @@ -657,7 +654,6 @@ let test_ticket_deposit_from_l1_to_l2 = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 3 in Log.info "The tx_rollup_deposit %s contract was successfully originated" contract_id ; @@ -822,7 +818,6 @@ let test_l2_to_l2_transaction = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 3 in Log.info "The tx_rollup_deposit %s contract was successfully originated" contract_id ; @@ -851,7 +846,6 @@ let test_l2_to_l2_transaction = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 4 in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in let* inbox = tx_client_get_inbox_as_json ~tx_client ~block:"head" in let ticket_id = get_ticket_hash_from_deposit_json inbox in @@ -886,7 +880,6 @@ let test_l2_to_l2_transaction = client in let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 5 in let* _ = Rollup_node.wait_for_tezos_level tx_node 5 in let* () = check_tz4_balance @@ -915,7 +908,6 @@ let test_l2_to_l2_transaction = in Log.info "Baking the batch" ; let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 6 in let* _ = Rollup_node.wait_for_tezos_level tx_node 6 in (* The decoding fails because of the buggy JSON encoding. This line can be uncommented once it is fixed.*) @@ -1506,7 +1498,6 @@ let test_l2_proof_rpc_position = in Log.info "Baking the batches" ; let* () = Client.bake_for_and_wait client in - let* _ = Node.wait_for_level node 5 in let* _ = Rollup_node.wait_for_tezos_level tx_node 5 in Log.info "Commitment for rollup level: 1" ; let* ({ @@ -1668,7 +1659,7 @@ let test_reject_bad_commitment = ~src:Constant.bootstrap3.public_key_hash client in - let* () = Client.bake_for client in + let* () = Client.bake_for_and_wait client in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in let* { proof; @@ -1757,11 +1748,11 @@ let test_committer = Log.info "Sending some L2 transactions" ; let* _ = inject_tx ~from:bls_key_1 ~dest:bls_pkh_2 ~amount:1000L () in let* _ = inject_tx ~from:bls_key_2 ~dest:bls_pkh_1 ~amount:2L () in - let* () = Client.bake_for client in + let* () = Client.bake_for_and_wait client in let* tzlevel = Rollup_node.wait_for_tezos_level tx_node (tzlevel + 1) in let* () = check_commitments_inclusion ~tx_node [("0", true)] in let* () = - check_injection tx_node "commitment" @@ Client.bake_for client + check_injection tx_node "commitment" @@ Client.bake_for_and_wait client in let* tzlevel = Rollup_node.wait_for_tezos_level tx_node (tzlevel + 1) in let* block = Rollup_node.Client.get_block ~tx_node ~block:"head" in @@ -1772,7 +1763,7 @@ let test_committer = Log.info "Sending some more L2 transactions" ; let* _ = inject_tx ~from:bls_key_1 ~dest:bls_pkh_2 ~amount:3L () in let* _ = inject_tx ~from:bls_key_2 ~dest:bls_pkh_1 ~amount:4L () in - let* () = Client.bake_for client in + let* () = Client.bake_for_and_wait client in let* tzlevel = Rollup_node.wait_for_tezos_level tx_node (tzlevel + 1) in let* block = Rollup_node.Client.get_block ~tx_node ~block:"head" in check_l2_level block 1 ; @@ -1783,7 +1774,7 @@ let test_committer = let* _ = inject_tx ~from:bls_key_1 ~dest:bls_pkh_2 ~amount:5L () in let* _ = inject_tx ~from:bls_key_2 ~dest:bls_pkh_1 ~amount:6L () in let* () = - check_injection tx_node "commitment" @@ Client.bake_for client + check_injection tx_node "commitment" @@ Client.bake_for_and_wait client in let* tzlevel = Rollup_node.wait_for_tezos_level tx_node (tzlevel + 1) in let* block = Rollup_node.Client.get_block ~tx_node ~block:"head" in @@ -1794,7 +1785,7 @@ let test_committer = [("0", true); ("1", true); ("2", false)] in let* () = - check_injection tx_node "commitment" @@ Client.bake_for client + check_injection tx_node "commitment" @@ Client.bake_for_and_wait client in let* _tzlevel = Rollup_node.wait_for_tezos_level tx_node (tzlevel + 1) in let* block = Rollup_node.Client.get_block ~tx_node ~block:"head" in @@ -1891,8 +1882,8 @@ let test_tickets_context = ~qty:5L in Log.info "Waiting for new L2 block" ; - let* () = Client.bake_for client in - let* () = Client.bake_for client in + let* () = Client.bake_for_and_wait client in + let* () = Client.bake_for_and_wait client in let* _ = Rollup_node.wait_for_tezos_level tx_node 6 in let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in check_inbox_success inbox ;