From 1671ee736518fc108a0e5f0d9c5c3339aa579ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Tue, 15 Jul 2025 16:04:32 +0200 Subject: [PATCH] EVM Node: Verify sequencer blueprint signatures in the node * What This change introduces a signature verification step for sequencer blueprints within the EVM node. It also modifies the mechanism for passing blueprints to the PVM, shifting from direct kernel inputs to durable storage. * Why This is first and foremost a preparatory step to allow skipping the signature verification of the blueprint for the sequencer mode. * How Before applying a new blueprint, the `Evm_context` now retrieves the sequencer's public key from storage. It then uses the new `Sequencer_blueprint.check_signature` function to verify each chunk of the incoming blueprint. The `Evm_state.apply_blueprint` function has been refactored. It no longer passes the blueprint payload as a kernel input. Instead, it writes the verified, unsigned blueprint chunks to the dedicated paths in durable storage. The PVM is then executed with an empty input list, signaling to the kernel that it must read the current blueprint from durable storage itself. This essentially allows to skip the stage one of the EVM node. The replay logic has been adapted to this new flow, skipping the signature check for trusted, previously-validated blueprints. Co-authored-by: Thomas Letan --- .../bin_node/lib_dev/durable_storage_path.ml | 10 ++++ .../bin_node/lib_dev/durable_storage_path.mli | 6 ++ etherlink/bin_node/lib_dev/evm_context.ml | 14 ++++- etherlink/bin_node/lib_dev/evm_ro_context.ml | 13 ++++- etherlink/bin_node/lib_dev/evm_state.ml | 56 +++++++++++++++---- etherlink/bin_node/lib_dev/evm_state.mli | 8 +-- .../bin_node/lib_dev/sequencer_blueprint.ml | 10 ++++ .../bin_node/lib_dev/sequencer_blueprint.mli | 9 ++- 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.ml b/etherlink/bin_node/lib_dev/durable_storage_path.ml index 54b7b70a9081..eafb4d51f3d9 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.ml +++ b/etherlink/bin_node/lib_dev/durable_storage_path.ml @@ -152,6 +152,16 @@ module Code = struct let code code_hash = code_storage code_hash ^ code end +module Blueprint = struct + let blueprint blueprint_number = + EVM.make "/blueprints/" ^ Z.to_string blueprint_number + + let chunk ~blueprint_number ~chunk_index = + blueprint blueprint_number ^ "/" ^ string_of_int chunk_index + + let nb_chunks ~blueprint_number = blueprint blueprint_number ^ "/nb_chunks" +end + module Block = struct type number = Current | Nth of Z.t diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.mli b/etherlink/bin_node/lib_dev/durable_storage_path.mli index 14097e1eb516..4a64a3790e7a 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.mli +++ b/etherlink/bin_node/lib_dev/durable_storage_path.mli @@ -89,6 +89,12 @@ module Code : sig val code : hash -> path end +module Blueprint : sig + val chunk : blueprint_number:Z.t -> chunk_index:int -> path + + val nb_chunks : blueprint_number:Z.t -> path +end + (** Paths related to blocks. *) module Block : sig (** Block number is either the current head or a specific height. *) diff --git a/etherlink/bin_node/lib_dev/evm_context.ml b/etherlink/bin_node/lib_dev/evm_context.ml index 556e65748b77..678b380385dc 100644 --- a/etherlink/bin_node/lib_dev/evm_context.ml +++ b/etherlink/bin_node/lib_dev/evm_context.ml @@ -868,11 +868,21 @@ module State = struct ctxt.session.next_blueprint_number in + let* sequencer = Durable_storage.sequencer (read_from_state evm_state) in + let*? chunks = + List.map_e + (fun chunk -> + let open Result_syntax in + let* chunk = Sequencer_blueprint.chunk_of_external_message chunk in + Sequencer_blueprint.check_signature sequencer chunk) + payload + in + let* try_apply = Misc.with_timing (fun time -> Lwt.return (time_processed := time)) (fun () -> - Evm_state.apply_blueprint + Evm_state.apply_unsigned_chunks ~native_execution_policy: ctxt.configuration.kernel_execution.native_execution_policy ~wasm_pvm_fallback:(not @@ List.is_empty delayed_transactions) @@ -880,7 +890,7 @@ module State = struct ~chain_family ~config evm_state - payload) + chunks) in match try_apply with diff --git a/etherlink/bin_node/lib_dev/evm_ro_context.ml b/etherlink/bin_node/lib_dev/evm_ro_context.ml index ccc8da316244..87c092ca94da 100644 --- a/etherlink/bin_node/lib_dev/evm_ro_context.ml +++ b/etherlink/bin_node/lib_dev/evm_ro_context.ml @@ -509,7 +509,16 @@ let replay ctxt ?(log_file = "replay") ?profile process_time := dt ; Lwt.return_unit) @@ fun () -> - Evm_state.apply_blueprint + let*? chunks = + List.map_e + (fun chunk -> + let open Result_syntax in + let+ chunk = Sequencer_blueprint.chunk_of_external_message chunk in + (* We are replaying, so we can assume the signature is correct *) + Sequencer_blueprint.unsafe_drop_signature chunk) + blueprint.blueprint.payload + in + Evm_state.apply_unsigned_chunks ~log_file ?profile ~data_dir:ctxt.data_dir @@ -517,7 +526,7 @@ let replay ctxt ?(log_file = "replay") ?profile ~config:(pvm_config ctxt) ~native_execution_policy:ctxt.native_execution_policy evm_state - blueprint.blueprint.payload + chunks in match apply_result with | Apply_success {block; evm_state} -> diff --git a/etherlink/bin_node/lib_dev/evm_state.ml b/etherlink/bin_node/lib_dev/evm_state.ml index 25d6a5599bc8..0c5a99d77701 100644 --- a/etherlink/bin_node/lib_dev/evm_state.ml +++ b/etherlink/bin_node/lib_dev/evm_state.ml @@ -232,9 +232,9 @@ let current_block_height ~root evm_state = match current_block_number with | None -> (* No block has been created yet and we are waiting for genesis, - whose number will be [zero]. Since the semantics of [apply_blueprint] - is to verify the block height has been incremented once, we default to - [-1]. *) + whose number will be [zero]. Since the semantics of + [apply_unsigned_chunks] is to verify the block height has been + incremented once, we default to [-1]. *) return (Qty Z.(pred zero)) | Some current_block_number -> let (Qty current_block_number) = decode_number_le current_block_number in @@ -318,6 +318,39 @@ let execute_and_inspect ?wasm_pvm_fallback ~data_dir ?wasm_entrypoint ~config let*! values = List.map_p (fun key -> inspect evm_state key) keys in return values +let store_blueprint_chunk evm_state (chunk : Sequencer_blueprint.unsigned_chunk) + = + let open Lwt_result_syntax in + let (Qty number) = chunk.number in + let key = + Durable_storage_path.Blueprint.chunk + ~blueprint_number:number + ~chunk_index:chunk.chunk_index + in + let value = + (* We want to encode a [StoreBlueprint] (see blueprint_storage.rs in + kernel_latest/kernel). The [StoreBlueprint] has two variants, and we + want to store a [SequencerChunk] whose tag is 0. [Value ""] is the + RLP-encoded for 0. *) + Rlp.List [Rlp.Value (Bytes.of_string ""); Value chunk.value] + |> Rlp.encode |> Bytes.to_string + in + let*! evm_state = modify ~key ~value evm_state in + return evm_state + +let store_blueprint_chunks ~blueprint_number evm_state + (chunks : Sequencer_blueprint.unsigned_chunk list) = + let open Lwt_result_syntax in + let nb_chunks = List.length chunks in + let* evm_state = List.fold_left_es store_blueprint_chunk evm_state chunks in + let*! evm_state = + modify + ~key:(Durable_storage_path.Blueprint.nb_chunks ~blueprint_number) + ~value:(Z.to_bits (Z.of_int nb_chunks)) + evm_state + in + return evm_state + type apply_result = | Apply_success of { evm_state : t; @@ -325,17 +358,18 @@ type apply_result = } | Apply_failure -let apply_blueprint ?wasm_pvm_fallback ?log_file ?profile ~data_dir +let apply_unsigned_chunks ?wasm_pvm_fallback ?log_file ?profile ~data_dir ~chain_family ~config ~native_execution_policy evm_state - (blueprint : Blueprint_types.payload) = + (chunks : Sequencer_blueprint.unsigned_chunk list) = let open Lwt_result_syntax in let root = Durable_storage_path.root_of_chain_family chain_family in - let exec_inputs = - List.map - (function `External payload -> `Input ("\001" ^ payload)) - blueprint - in let*! (Qty before_height) = current_block_height ~root evm_state in + let* evm_state = + store_blueprint_chunks + ~blueprint_number:(Z.succ before_height) + evm_state + chunks + in let* evm_state = execute ~native_execution:(native_execution_policy = Configuration.Always) @@ -346,7 +380,7 @@ let apply_blueprint ?wasm_pvm_fallback ?log_file ?profile ~data_dir ~config ?log_file evm_state - exec_inputs + [] in let* block_hash = current_block_hash ~chain_family evm_state in let root = Durable_storage_path.root_of_chain_family chain_family in diff --git a/etherlink/bin_node/lib_dev/evm_state.mli b/etherlink/bin_node/lib_dev/evm_state.mli index 8ae6cc74b519..3bdea3d4ffb7 100644 --- a/etherlink/bin_node/lib_dev/evm_state.mli +++ b/etherlink/bin_node/lib_dev/evm_state.mli @@ -95,15 +95,15 @@ type apply_result = } | Apply_failure -(** [apply_blueprint ~data-dir ~config state payload] applies the - blueprint [payload] on top of [evm_state]. If the payload produces +(** [apply_unsigned_chunks ~data-dir ~config state chunks] applies the + blueprint [chunks] on top of [evm_state]. If the payload produces a block, the new updated EVM state is returned along with the new block’s height. The [data-dir] is used to store the kernel logs in the {!kernel_logs_directory}. *) -val apply_blueprint : +val apply_unsigned_chunks : ?wasm_pvm_fallback:bool -> ?log_file:string -> ?profile:Configuration.profile_mode -> @@ -112,7 +112,7 @@ val apply_blueprint : config:Wasm_debugger.config -> native_execution_policy:Configuration.native_execution_policy -> t -> - Blueprint_types.payload -> + Sequencer_blueprint.unsigned_chunk list -> apply_result tzresult Lwt.t (** [flag_local_exec evm_state] adds a flag telling the kernel it is executed diff --git a/etherlink/bin_node/lib_dev/sequencer_blueprint.ml b/etherlink/bin_node/lib_dev/sequencer_blueprint.ml index 203eebe02692..dde1fd767baf 100644 --- a/etherlink/bin_node/lib_dev/sequencer_blueprint.ml +++ b/etherlink/bin_node/lib_dev/sequencer_blueprint.ml @@ -239,6 +239,16 @@ let check_signature_opt sequencer chunk = in if correctly_signed then Some chunk else None +let check_signature sequencer chunk = + let open Result_syntax in + match check_signature_opt sequencer chunk with + | Some chunk -> return chunk.unsigned_chunk + | None -> + error_with + "Signature check failed for the provided blueprint with public key %a" + Signature.Public_key.pp + sequencer + let decode_inbox_payload sequencer (payload : Blueprint_types.payload) = List.filter_map (fun chunk -> diff --git a/etherlink/bin_node/lib_dev/sequencer_blueprint.mli b/etherlink/bin_node/lib_dev/sequencer_blueprint.mli index 3e5d33c89835..10f6400addb9 100644 --- a/etherlink/bin_node/lib_dev/sequencer_blueprint.mli +++ b/etherlink/bin_node/lib_dev/sequencer_blueprint.mli @@ -16,9 +16,16 @@ type unsigned_chunk = private { type t (** [unsafe_drop_signature chunk] gives back the content of [chunk] - {e without checking if its signature is valid}. *) + {e without checking if its signature is valid}. See {!check_signature} if + you want to get the unsigned content iff the signature is correct. *) val unsafe_drop_signature : t -> unsigned_chunk +(** [check_signature pubkey chunk] will return the (unsigned) chunk content in + the case that it was indeed signed for [pubkey]. Otherwise it returns an + error. See {!unsafe_drop_signature} if you want to skip the signature + verification and just get the unsigned content. *) +val check_signature : Signature.public_key -> t -> unsigned_chunk tzresult + val chunk_encoding : t Data_encoding.t (** [chunk_to_rlp chunk] encodes a chunk into its RLP format. *) -- GitLab