From ef94ef6c10951e3fb866c76f4f59a7fb2d687f4e Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Thu, 11 Jul 2024 15:31:52 +0200 Subject: [PATCH] EVM Node: Initial support for the `finalized` block parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to the JSON RPC API (at the very least according to _some_ JSON RPC API specification), `finalized` is valid block parameter. For instance, on the [Ethereum official website], the full list of block parameters does mention `finalized`. The following options are possible for the defaultBlock parameter: - HEX String - an integer block number - String "earliest" for the earliest/genesis block - String "latest" - for the latest mined block - String "safe" - for the latest safe head block - String "finalized" - for the latest finalized block - String "pending" - for the pending state/transactions [Ethereum official website]: https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block It turns out that supporting this is actually quite straightforward. The proxy and sequencer modes already keeps track of the “confirmed” level (another name for finalized) for the metrics. We just have to store it in the session type of the Evm context to be able to retreived it when needs be. The current implementation has an obvious shortcoming, in that the value of the finalized level is not initialized at startup time. For the EVM node to return the expected value, it needs to see a new block being applied by a remote rollup node. Considering it is something that is happening very regularly on our networks (both Mainnet and Testnet), this is an acceptable compromise to make the patch simpler. Obviously, we will need to fix this in the future. Additionally, the Rollup node also supports the `finalized` block id, which we can use instead of `head`. This is basically the hidden perl that we needed to quickly implement this future for the proxy. --- .../lib_dev/encodings/ethereum_types.ml | 10 ++- .../lib_dev/encodings/ethereum_types.mli | 2 +- etherlink/bin_node/lib_dev/evm_context.ml | 36 +++++++- etherlink/bin_node/lib_dev/evm_context.mli | 1 + etherlink/bin_node/lib_dev/rollup_node.ml | 67 ++++++++------- etherlink/tezt/lib/helpers.ml | 5 +- etherlink/tezt/lib/helpers.mli | 2 +- etherlink/tezt/tests/evm_sequencer.ml | 82 ++++++++++++++++++- 8 files changed, 169 insertions(+), 36 deletions(-) diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml index 5157f9610ea1..79ac1a8a2753 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml @@ -105,13 +105,14 @@ let block_hash_to_bytes (Block_hash h) = hex_to_bytes h let genesis_parent_hash = Block_hash (Hex (String.make 64 'f')) module Block_parameter = struct - type t = Number of quantity | Earliest | Latest | Pending + type t = Number of quantity | Earliest | Latest | Pending | Finalized let pp fmt = function | Number quantity -> pp_quantity fmt quantity | Earliest -> Format.pp_print_string fmt "earliest" | Latest -> Format.pp_print_string fmt "latest" | Pending -> Format.pp_print_string fmt "pending" + | Finalized -> Format.pp_print_string fmt "finalized" let encoding = let open Data_encoding in @@ -145,6 +146,13 @@ module Block_parameter = struct (constant tag) (function Pending -> Some () | _ -> None) (fun () -> Pending)); + (let tag = "finalized" in + case + ~title:tag + (Tag 4) + (constant tag) + (function Finalized -> Some () | _ -> None) + (fun () -> Finalized)); ] type extended = diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli index eec747e95185..6e22548b167d 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli @@ -234,7 +234,7 @@ val block_from_rlp : bytes -> block module Block_parameter : sig (** Ethereum block params in RPCs. *) - type t = Number of quantity | Earliest | Latest | Pending + type t = Number of quantity | Earliest | Latest | Pending | Finalized val encoding : t Data_encoding.t diff --git a/etherlink/bin_node/lib_dev/evm_context.ml b/etherlink/bin_node/lib_dev/evm_context.ml index 5deb85137e11..de621bafdd85 100644 --- a/etherlink/bin_node/lib_dev/evm_context.ml +++ b/etherlink/bin_node/lib_dev/evm_context.ml @@ -9,6 +9,7 @@ type init_status = Loaded | Created type head = { current_block_hash : Ethereum_types.block_hash; + finalized_number : Ethereum_types.quantity; next_blueprint_number : Ethereum_types.quantity; evm_state : Evm_state.t; } @@ -25,6 +26,7 @@ type parameters = { type session_state = { mutable context : Irmin_context.rw; + mutable finalized_number : Ethereum_types.quantity; mutable next_blueprint_number : Ethereum_types.quantity; mutable current_block_hash : Ethereum_types.block_hash; mutable pending_upgrade : Ethereum_types.Upgrade.t option; @@ -48,6 +50,7 @@ let blueprint_watcher : Blueprint_types.with_events Lwt_watcher.input = let session_to_head_info session = { evm_state = session.evm_state; + finalized_number = session.finalized_number; next_blueprint_number = session.next_blueprint_number; current_block_hash = session.current_block_hash; } @@ -109,6 +112,7 @@ module Request = struct | New_last_known_L1_level : int32 -> (unit, tztrace) t | Delayed_inbox_hashes : (Ethereum_types.hash list, tztrace) t | Evm_state_after : block_request -> (Evm_state.t option, tztrace) t + | Finalized_state : (Evm_state.t option, tztrace) t | Earliest_state : (Evm_state.t option, tztrace) t | Earliest_number : (Ethereum_types.quantity option, tztrace) t | Reconstruct : { @@ -403,7 +407,16 @@ module State = struct in return (evm_state, on_success) | Blueprint_applied {number = Qty number; hash = expected_block_hash} -> ( - Metrics.set_confirmed_level ~level:number ; + let on_success session = + let (Qty finalized) = session.finalized_number in + (* We use [max] to not rely on the order of the EVM events (because + it is possible to see several blueprints applied during on L1 + level). *) + let new_finalized = Z.(max number finalized) in + session.finalized_number <- Qty new_finalized ; + Metrics.set_confirmed_level ~level:new_finalized ; + on_success session + in let* block_hash_opt = let*! bytes = Evm_state.inspect @@ -489,8 +502,10 @@ module State = struct let* ctxt = replace_current_commit ctxt conn evm_state in return (ctxt, evm_state, on_success) in - on_modified_head ctxt evm_state context ; on_success ctxt.session ; + on_modified_head ctxt evm_state context ; + let*! head_info in + head_info := session_to_head_info ctxt.session ; return_unit type error += Cannot_apply_blueprint of {local_state_level : Z.t} @@ -799,6 +814,7 @@ module State = struct session = { context; + finalized_number = Ethereum_types.quantity_of_z Z.zero; next_blueprint_number; current_block_hash; pending_upgrade; @@ -1155,6 +1171,18 @@ module Handlers = struct let*! evm_state = Irmin_context.PVMState.get context in return_some evm_state | None -> return_none) + | Finalized_state -> ( + let ctxt = Worker.state self in + Evm_store.use ctxt.store @@ fun conn -> + let* checkpoint = + Evm_store.Context_hashes.find conn ctxt.session.finalized_number + in + match checkpoint with + | Some checkpoint -> + let*! context = Irmin_context.checkout_exn ctxt.index checkpoint in + let*! evm_state = Irmin_context.PVMState.get context in + return_some evm_state + | None -> return_none) | Earliest_state -> ( let ctxt = Worker.state self in Evm_store.use ctxt.store @@ fun conn -> @@ -1517,6 +1545,7 @@ let find_evm_state block = let*! {evm_state; _} = head_info () in return_some evm_state | Block_parameter Earliest -> worker_wait_for_request Earliest_state + | Block_parameter Finalized -> worker_wait_for_request Finalized_state | Block_parameter (Number number) -> worker_wait_for_request (Evm_state_after (Number number)) | Block_hash {hash; require_canonical = _} -> @@ -1633,6 +1662,9 @@ let block_param_to_block_number | Block_parameter (Latest | Pending) -> let*! {next_blueprint_number = Qty next_number; _} = head_info () in return Ethereum_types.(Qty Z.(pred next_number)) + | Block_parameter Finalized -> + let*! {finalized_number; _} = head_info () in + return finalized_number | Block_parameter Earliest -> ( let* res = worker_wait_for_request Earliest_number in match res with diff --git a/etherlink/bin_node/lib_dev/evm_context.mli b/etherlink/bin_node/lib_dev/evm_context.mli index 6068da17fadf..1cfbe17befa7 100644 --- a/etherlink/bin_node/lib_dev/evm_context.mli +++ b/etherlink/bin_node/lib_dev/evm_context.mli @@ -9,6 +9,7 @@ type init_status = Loaded | Created type head = { current_block_hash : Ethereum_types.block_hash; + finalized_number : Ethereum_types.quantity; next_blueprint_number : Ethereum_types.quantity; evm_state : Evm_state.t; } diff --git a/etherlink/bin_node/lib_dev/rollup_node.ml b/etherlink/bin_node/lib_dev/rollup_node.ml index a00c1fbb53f3..57d7fcc107b2 100644 --- a/etherlink/bin_node/lib_dev/rollup_node.ml +++ b/etherlink/bin_node/lib_dev/rollup_node.ml @@ -42,23 +42,27 @@ module MakeBackend (Base : sig end) : Services_backend_sig.Backend = struct module Reader = struct let read ?block path = - match block with - | Some param - when param <> Ethereum_types.Block_parameter.(Block_parameter Latest) -> - failwith - "The EVM node in proxy mode support state requests only on latest \ - block." - | _ -> - let level : Rollup_services.Block_id.t = - if Base.finalized then Finalized else Head - in - call_service - ~keep_alive:Base.keep_alive - ~base:Base.base - durable_state_value - ((), level) - {key = path} - () + let open Lwt_result_syntax in + let* block_id = + match block with + | Some Ethereum_types.Block_parameter.(Block_parameter Latest) | None -> + let level : Rollup_services.Block_id.t = + if Base.finalized then Finalized else Head + in + return level + | Some (Block_parameter Finalized) -> return Block_id.Finalized + | Some _ -> + failwith + "The EVM node in proxy mode support state requests only on \ + latest or finalized block." + in + call_service + ~keep_alive:Base.keep_alive + ~base:Base.base + durable_state_value + ((), block_id) + {key = path} + () end module TxEncoder = struct @@ -135,20 +139,19 @@ end) : Services_backend_sig.Backend = struct let block_param_to_block_number (block_param : Ethereum_types.Block_parameter.extended) = let open Lwt_result_syntax in + let read_from_block_parameter param = + let* value = + Reader.read + ~block:(Block_parameter param) + Durable_storage_path.Block.current_number + in + match value with + | Some value -> + return (Ethereum_types.Qty (Bytes.to_string value |> Z.of_bits)) + | None -> failwith "Cannot fetch the requested block" + in match block_param with | Block_parameter (Number n) -> return n - | Block_parameter (Earliest | Latest) -> ( - let* value = - Reader.read - ~block:(Block_parameter Latest) - Durable_storage_path.Block.current_number - in - match value with - | Some value -> - return (Ethereum_types.Qty (Bytes.to_string value |> Z.of_bits)) - | None -> failwith "Cannot read current number") - | Block_parameter Pending -> - failwith "Pending block parameter is not supported" | Block_hash {hash; _} -> ( let* value = Reader.read @@ -164,6 +167,12 @@ end) : Services_backend_sig.Backend = struct "Missing state for block %a" Ethereum_types.pp_block_hash hash) + | Block_parameter Latest -> read_from_block_parameter Latest + | Block_parameter Finalized -> read_from_block_parameter Finalized + | Block_parameter Pending -> + failwith "Pending block parameter is not supported" + | Block_parameter Earliest -> + failwith "Earliest block parameter is not supported" module Tracer = struct let trace_transaction ~block_number:_ ~transaction_hash:_ ~config:_ = diff --git a/etherlink/tezt/lib/helpers.ml b/etherlink/tezt/lib/helpers.ml index fcccba862c77..782ceeb20dbb 100644 --- a/etherlink/tezt/lib/helpers.ml +++ b/etherlink/tezt/lib/helpers.ml @@ -142,7 +142,10 @@ let upgrade ~sc_rollup_node ~sc_rollup_address ~admin ~admin_contract ~client let check_block_consistency ~left ~right ?error_msg ~block () = let open Rpc.Syntax in let block = - match block with `Latest -> "latest" | `Level l -> Int32.to_string l + match block with + | `Latest -> "latest" + | `Level l -> Int32.to_string l + | `Finalized -> "finalized" in let error_msg = Option.value diff --git a/etherlink/tezt/lib/helpers.mli b/etherlink/tezt/lib/helpers.mli index a13d7814541e..b732b4927045 100644 --- a/etherlink/tezt/lib/helpers.mli +++ b/etherlink/tezt/lib/helpers.mli @@ -106,7 +106,7 @@ val check_block_consistency : left:Evm_node.t -> right:Evm_node.t -> ?error_msg:string -> - block:[< `Latest | `Level of int32] -> + block:[< `Latest | `Level of int32 | `Finalized] -> unit -> unit Lwt.t diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index f59314bb60c0..d8484fa6c194 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -4681,6 +4681,85 @@ let test_proxy_finalized_view = instead" ; unit +let test_finalized_block_param = + register_both + ~time_between_blocks:Nothing + ~kernels: + [ + Latest + (* This test focuses on a feature that purely relies on the node, it does not makes sense to register it for every protocols *); + ] + ~tags:["evm"; "finalized_block_param"] + ~title: + "The finalized block parameter is correctly interpreted by the EVM node" + ~da_fee:Wei.zero + @@ fun {sc_rollup_node; client; sequencer; proxy; _} _protocol -> + (* Produce a few EVM blocks *) + let* () = + repeat 4 @@ fun () -> + let* _ = produce_block sequencer in + unit + in + let* () = + bake_until_sync ~__LOC__ ~sc_rollup_node ~client ~sequencer ~proxy () + in + (* Check that the L2 blocks where indeed posted onchain. *) + let*@ sequencer_head = Rpc.get_block_by_number ~block:"latest" sequencer in + Check.((sequencer_head.number = 4l) int32) + ~error_msg:"Sequencer head should be %R, but is %L instead" ; + let* () = + check_head_consistency + ~left:proxy + ~right:sequencer + ~error_msg:"Sequencer and proxy should have the same head" + () + in + (* While the blocks were posted onchain, they are not final wrt. the + consensus algorithm, so the finalized proxy does not have a head yet. + + We produce both two more L2 blocks and two L1 blocks; the latter will + allow to finalized the first four blocks posted earlier. *) + let* () = + repeat 2 @@ fun () -> + let* _ = next_rollup_node_level ~sc_rollup_node ~client in + let* _ = produce_block sequencer in + unit + in + (* Produces two L1 blocks to ensure the L2 blocks are posted onchain by the sequencer *) + let* () = + bake_until_sync ~__LOC__ ~sc_rollup_node ~client ~sequencer ~proxy () + in + (* We can check the consistency of the various nodes. *) + let*@ sequencer_new_head = + Rpc.get_block_by_number ~block:"latest" sequencer + in + let*@ sequencer_finalized_head = + Rpc.get_block_by_number ~block:"finalized" sequencer + in + + Check.((sequencer_new_head.number = 6l) int32) + ~error_msg:"Sequencer head should be %R, but is %L instead" ; + Check.((sequencer_finalized_head.number = 4l) int32) + ~error_msg:"Sequencer finalized head should be %R, but is %L instead" ; + + let* () = + check_head_consistency + ~left:proxy + ~right:sequencer + ~error_msg:"Sequencer and proxy should have the same head" + () + in + let* () = + check_block_consistency + ~block:`Finalized + ~left:proxy + ~right:sequencer + ~error_msg:"Sequencer and proxy should have the same head" + () + in + + unit + let protocols = Protocol.all let () = @@ -4744,4 +4823,5 @@ let () = test_fa_bridge_feature_flag protocols ; test_trace_call protocols ; test_patch_kernel protocols ; - test_proxy_finalized_view protocols + test_proxy_finalized_view protocols ; + test_finalized_block_param protocols -- GitLab