diff --git a/etherlink/bin_node/lib_dev/encodings/rlp.ml b/etherlink/bin_node/lib_dev/encodings/rlp.ml index 9daff251ba67e49ff602171381f4eaf3bae76f0b..38bfa9f21c6a1de62e9bab1a4ca419686f62ed30 100644 --- a/etherlink/bin_node/lib_dev/encodings/rlp.ml +++ b/etherlink/bin_node/lib_dev/encodings/rlp.ml @@ -25,6 +25,18 @@ type error += Rlp_decoding_error of string +let () = + register_error_kind + ~id:"evm-node.dev.rlp-decoding-error" + ~title:"Unable to decode an RLP value" + ~description:"Unable to decode an RLP value" + ~pp:(fun ppf msg -> + Format.fprintf ppf "Unable to decode an RLP value: `%s`" msg) + `Permanent + Data_encoding.(obj1 (req "msg" string)) + (function Rlp_decoding_error msg -> Some msg | _ -> None) + (fun msg -> Rlp_decoding_error msg) + type item = Value of bytes | List of item list let rec encode_int buffer i = @@ -214,3 +226,45 @@ let rec pp ppf = function ~pp_sep:(fun ppf () -> Format.fprintf ppf "; ") pp) items + +(* Implements optional types decoding as in the RLP library from the + kernel's library. *) +let decode_option decode_value = + let open Result_syntax in + function + | List [] -> return_none + | List [v] -> + let* value = decode_value v in + return_some value + | _ -> tzfail (Rlp_decoding_error "Inconsistent encoding for optional type") + +let decode_result decode_value decode_error = + let open Result_syntax in + function + | List [Value tag; payload] -> ( + let* () = + if Bytes.length tag <> 1 then + tzfail + (Rlp_decoding_error + (Format.sprintf "Inconsistent tag size: %d" (Bytes.length tag))) + else return_unit + in + let tag = Bytes.get_uint8 tag 0 in + match tag with + | 1 -> + let* ok = decode_value payload in + return @@ Ok ok + | 2 -> + let* err = decode_error payload in + return @@ Error err + | t -> + tzfail + (Rlp_decoding_error + (Format.sprintf "Inconsistent tag [%d] for the result type" t))) + | rlp -> + tzfail + (Rlp_decoding_error + (Format.asprintf + "Inconsistent encoding for the result type: %a" + pp + rlp)) diff --git a/etherlink/bin_node/lib_dev/encodings/rlp.mli b/etherlink/bin_node/lib_dev/encodings/rlp.mli index dfe0f1ec96a8a20b820e9f882d4bfc45bf424f5a..c93f7522b2546d1e51419d65e94778cf07d0246f 100644 --- a/etherlink/bin_node/lib_dev/encodings/rlp.mli +++ b/etherlink/bin_node/lib_dev/encodings/rlp.mli @@ -60,5 +60,16 @@ val decode : bytes -> item tzresult fails to decode. *) val decode_exn : bytes -> item +(** [decode_option decode_value optional_value] decodes the option following + Rust's RLP encoding. *) +val decode_option : (item -> 'a tzresult) -> item -> 'a option tzresult + +(** [decode_result decode_ok decode_error value] decodes an encoded result type. *) +val decode_result : + (item -> 'a tzresult) -> + (item -> 'b tzresult) -> + item -> + ('a, 'b) result tzresult + (** [pp ppf item] pretty-prints an item. *) val pp : Format.formatter -> item -> unit diff --git a/etherlink/bin_node/lib_dev/observer.ml b/etherlink/bin_node/lib_dev/observer.ml index f3814285e90fe855cf0a9a834c1592a943412faf..14822fcd6c9100af44d9558416b3fb132c42c001 100644 --- a/etherlink/bin_node/lib_dev/observer.ml +++ b/etherlink/bin_node/lib_dev/observer.ml @@ -98,9 +98,9 @@ end) : Services_backend_sig.Backend = struct let simulate_and_read ~input = let open Lwt_result_syntax in let* raw_insights = Evm_context.execute_and_inspect Ctxt.ctxt ~input in - match Simulation.Encodings.insights_from_list raw_insights with - | Some i -> return i - | None -> Error_monad.failwith "Invalid insights format" + match raw_insights with + | [Some bytes] -> return bytes + | _ -> Error_monad.failwith "Invalid insights format" end let inject_kernel_upgrade ~payload = diff --git a/etherlink/bin_node/lib_dev/rollup_node.ml b/etherlink/bin_node/lib_dev/rollup_node.ml index 94f3247d857914f5b662ace26b33230ce7d9831a..672f93ce4c42460651c52d432e412556c81751a2 100644 --- a/etherlink/bin_node/lib_dev/rollup_node.ml +++ b/etherlink/bin_node/lib_dev/rollup_node.ml @@ -88,7 +88,9 @@ end) : Services_backend_sig.Backend = struct let eval_result = Data_encoding.Json.destruct Simulation.Encodings.eval_result json in - return eval_result.insights + match eval_result.insights with + | [data] -> return data + | _ -> failwith "Inconsistent simulation results" end let inject_kernel_upgrade ~payload:_ = Lwt_result_syntax.return_unit diff --git a/etherlink/bin_node/lib_dev/sequencer.ml b/etherlink/bin_node/lib_dev/sequencer.ml index 742530028cfdd17b424f81d9d5f5aab7f5ae1a9a..4d9cee4dee94d88b88a1aaee850b5cb23205c4ed 100644 --- a/etherlink/bin_node/lib_dev/sequencer.ml +++ b/etherlink/bin_node/lib_dev/sequencer.ml @@ -73,9 +73,9 @@ end) : Services_backend_sig.Backend = struct let simulate_and_read ~input = let open Lwt_result_syntax in let* raw_insights = Evm_context.execute_and_inspect Ctxt.ctxt ~input in - match Simulation.Encodings.insights_from_list raw_insights with - | Some i -> return i - | None -> Error_monad.failwith "Invalid insights format" + match raw_insights with + | [Some bytes] -> return bytes + | _ -> Error_monad.failwith "Invalid insights format" end let inject_kernel_upgrade ~payload = diff --git a/etherlink/bin_node/lib_dev/services.ml b/etherlink/bin_node/lib_dev/services.ml index 2418999039c8524abee230c322fcdad8a08fd007..f91c272133f66dd7487a66e011c856996be4f406 100644 --- a/etherlink/bin_node/lib_dev/services.ml +++ b/etherlink/bin_node/lib_dev/services.ml @@ -409,7 +409,12 @@ let dispatch_request (config : 'a Configuration.t) | Some (call, _) -> ( let* call_result = Backend_rpc.simulate_call call in match call_result with - | Ok result -> return (Either.Left result) + | Ok (Ok {value = Some value; gas_used = _}) -> + return (Either.Left value) + | Ok (Ok {value = None; gas_used = _}) -> + return (Either.Left (hash_of_string "")) + | Ok (Error _reason) -> + return (Either.Right "execution reverted:") | Error reason -> (* TODO: https://gitlab.com/tezos/tezos/-/issues/6229 *) return (Either.Right reason)) @@ -421,9 +426,17 @@ let dispatch_request (config : 'a Configuration.t) match input with | None -> return missing_parameter | Some (call, _) -> ( - let* gas_result = Backend_rpc.estimate_gas call in - match gas_result with - | Ok gas -> return (Either.Left gas) + let* result = Backend_rpc.estimate_gas call in + match result with + | Ok (Ok {value = _; gas_used = Some gas}) -> + return (Either.Left gas) + | Ok (Ok {value = _; gas_used = None}) -> + return + (Either.Right + "Simulation failed before execution, cannot estimate \ + gas.") + | Ok (Error _reason) -> + return (Either.Right "execution reverted:") | Error reason -> (* TODO: https://gitlab.com/tezos/tezos/-/issues/6229 *) return (Either.Right reason)) diff --git a/etherlink/bin_node/lib_dev/services_backend_sig.ml b/etherlink/bin_node/lib_dev/services_backend_sig.ml index ef2e4f629b8aef32c231e6e5dbf81a2fcc52f594..856a4be470ae5ca6d47e22b4886410906035f938 100644 --- a/etherlink/bin_node/lib_dev/services_backend_sig.ml +++ b/etherlink/bin_node/lib_dev/services_backend_sig.ml @@ -83,19 +83,21 @@ module type S = sig (** [simulate_call call_info] asks the rollup to simulate a call, and returns the result. *) val simulate_call : - Ethereum_types.call -> (Ethereum_types.hash, string) result tzresult Lwt.t + Ethereum_types.call -> + Simulation.call_result Simulation.simulation_result tzresult Lwt.t (** [estimate_gas call_info] asks the rollup to simulate a call, and returns the gas used to execute the call. *) val estimate_gas : Ethereum_types.call -> - (Ethereum_types.quantity, string) result tzresult Lwt.t + Simulation.call_result Simulation.simulation_result tzresult Lwt.t (** [is_tx_valid tx_raw] checks if the transaction is valid. Checks if the nonce is correct and returns the associated public key of transaction. *) val is_tx_valid : - string -> (Ethereum_types.address, string) result tzresult Lwt.t + string -> + Simulation.validation_result Simulation.simulation_result tzresult Lwt.t (** [storage_at address pos] returns the value at index [pos] of the account [address]'s storage. *) diff --git a/etherlink/bin_node/lib_dev/simulation.ml b/etherlink/bin_node/lib_dev/simulation.ml index 655ef4f1a62aa9092cc5e36398636f5aaac1bb01..42cd04eb33a8e39baa0d79c0375e309d42b57786 100644 --- a/etherlink/bin_node/lib_dev/simulation.ml +++ b/etherlink/bin_node/lib_dev/simulation.ml @@ -119,22 +119,24 @@ let encode_tx tx = in return @@ List.map encode_message messages +type execution_result = {value : hash option; gas_used : quantity option} + +type call_result = (execution_result, hash) result + +type validation_result = {address : address} + +type 'a simulation_result = ('a, string) result + module Encodings = struct open Data_encoding - type insights = { - success : bool option; - result : bytes option; - gas : bytes option; - } - type eval_result = { state_hash : string; status : string; output : unit; inbox_level : unit; num_ticks : Z.t; - insights : insights; + insights : bytes list; (** The simulation can ask to look at values on the state after the simulation. *) } @@ -199,39 +201,64 @@ module Encodings = struct /simulation_kernel_logs/, where is the \ data directory of the rollup node.") - let bool_as_bytes = - let bytes_to_bool b = - b |> Data_encoding.Binary.of_bytes Data_encoding.bool |> Result.to_option + let decode_data = + let open Result_syntax in + function + | Rlp.Value v -> return (decode_hash v) + | Rlp.List _ -> error_with "The simulation returned an ill-encoded data" + + let decode_gas_used = + let open Result_syntax in + function + | Rlp.Value v -> return (decode_number v) + | Rlp.List _ -> error_with "The simulation returned an ill-encoded gas" + + let decode_execution_result = + let open Result_syntax in + function + | Rlp.List [value; gas_used] -> + let* value = Rlp.decode_option decode_data value in + let* gas_used = Rlp.decode_option decode_gas_used gas_used in + return {value; gas_used} + | _ -> + error_with + "The simulation for eth_call/eth_estimateGas returned an ill-encoded \ + format" + + let decode_call_result v = + let open Result_syntax in + let decode_revert = function + | Rlp.Value msg -> return (decode_hash msg) + | _ -> error_with "The revert message is ill-encoded" in - - let bool_to_bytes b = - b |> Data_encoding.Binary.to_bytes Data_encoding.bool |> Result.to_option + Rlp.decode_result decode_execution_result decode_revert v + + let decode_validation_result = + let open Result_syntax in + function + | Rlp.Value address -> return {address = decode_address address} + | _ -> error_with "The transaction pool returned an illformed value" + + let simulation_result_from_rlp decode_payload bytes = + let open Result_syntax in + let decode_error_msg = function + | Rlp.Value msg -> return @@ Bytes.to_string msg + | rlp -> + error_with + "The simulation returned an unexpected error message: %a" + Rlp.pp + rlp in - conv - (fun b -> Option.bind b bool_to_bytes) - (fun b -> Option.bind b bytes_to_bool) - (option bytes) - - let insights = - conv - (fun {success; result; gas} -> (gas, result, success)) - (fun (gas, result, success) -> {gas; result; success}) - (tup3 (option bytes) (option bytes) bool_as_bytes) - - let insights_from_list l = - match l with - | [gas; result; success] -> - Some - { - success = - Option.bind success (fun s -> - s - |> Data_encoding.Binary.of_bytes Data_encoding.bool - |> Result.to_option); - result; - gas; - } - | _ -> None + let* rlp = Rlp.decode bytes in + match Rlp.decode_result decode_payload decode_error_msg rlp with + | Ok v -> Ok v + | Error e -> + error_with + "The simulation returned an unexpected format: %a, with error %a" + Rlp.pp + rlp + pp_print_trace + e let eval_result = conv @@ -261,26 +288,20 @@ module Encodings = struct ~description:"Ticks taken by the PVM for evaluating the messages") (req "insights" - insights + (list bytes) ~description:"PVM state values requested after the simulation") end -let call_result Encodings.{success; result; gas = _} = - let open Lwt_result_syntax in - match (success, result) with - | Some true, Some result -> - let v = result |> Hex.of_bytes |> Hex.show in - return (Ok (Hash (Hex v))) - | Some false, Some result -> - let error_msg = Bytes.to_string result in - return (Error error_msg) - | _ -> failwith "Insights of 'call_result' cannot be parsed" - -let gas_estimation Encodings.{success; result; gas} = - let open Lwt_result_syntax in - match (success, result, gas) with - | Some true, _, Some simulated_amount -> - let simulated_amount = Bytes.to_string simulated_amount |> Z.of_bits in +let simulation_result bytes = + Encodings.simulation_result_from_rlp Encodings.decode_call_result bytes + +let gas_estimation bytes = + let open Result_syntax in + let* result = + Encodings.simulation_result_from_rlp Encodings.decode_call_result bytes + in + match result with + | Ok (Ok {gas_used = Some (Qty gas_used); value}) -> (* See EIP2200 for reference. But the tl;dr is: we cannot do the opcode SSTORE if we have less than 2300 gas available, even if we don't consume it. The simulated amount then gives an @@ -288,25 +309,14 @@ let gas_estimation Encodings.{success; result; gas} = The extra gas units, i.e. 2300, will be refunded. *) - let simulated_amount = Z.(add simulated_amount (of_int 2300)) in + let simulated_amount = Z.(add gas_used (of_int 2300)) in (* add a safety margin of 2%, sufficient to cover a 1/64th difference *) let simulated_amount = Z.(add simulated_amount (cdiv simulated_amount (of_int 50))) in - return (Ok (quantity_of_z simulated_amount)) - | Some false, Some result, _ -> - let error_msg = Bytes.to_string result in - return (Error error_msg) - | _ -> failwith "Insights of 'gas_estimation' cannot be parsed" - -let is_tx_valid Encodings.{success; result; gas = _} = - let open Lwt_result_syntax in - match (result, success) with - | Some payload, Some success -> - if success then - let address = Ethereum_types.decode_address payload in - return (Ok address) - else - let error_msg = Bytes.to_string payload in - return (Error error_msg) - | _ -> failwith "Insights of 'is_tx_valid' is not [Some _, Some _]" + return + @@ Ok (Ok {gas_used = Some (quantity_of_z @@ simulated_amount); value}) + | _ -> return result + +let is_tx_valid bytes = + Encodings.simulation_result_from_rlp Encodings.decode_validation_result bytes diff --git a/etherlink/bin_node/lib_dev/simulator.ml b/etherlink/bin_node/lib_dev/simulator.ml index adc4c34da78b27dddc8e3350e6ff21b604abe97a..419df6f88e0bfae9ea97d085f8e7f0f1aeadf522 100644 --- a/etherlink/bin_node/lib_dev/simulator.ml +++ b/etherlink/bin_node/lib_dev/simulator.ml @@ -7,57 +7,47 @@ module type SimulationBackend = sig val simulate_and_read : - input:Simulation.Encodings.simulate_input -> - Simulation.Encodings.insights tzresult Lwt.t + input:Simulation.Encodings.simulate_input -> bytes tzresult Lwt.t end (* This value is a hard maximum used by estimateGas. Set at Int64.max_int / 2 *) let max_gas_limit = Z.of_int64 0x3FFFFFFFFFFFFFFFL module Make (SimulationBackend : SimulationBackend) = struct - let simulate_call call = + let call_simulation ~log_file ~input_encoder ~input = let open Lwt_result_syntax in - let*? messages = Simulation.encode call in + let*? messages = input_encoder input in let insight_requests = - [ - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_gas"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_result"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_status"]; - ] + [Simulation.Encodings.Durable_storage_key ["evm"; "simulation_result"]] in - let* results = - SimulationBackend.simulate_and_read - ~input: - { - messages; - reveal_pages = None; - insight_requests; - log_kernel_debug_file = Some "simulate_call"; - } + SimulationBackend.simulate_and_read + ~input: + { + messages; + reveal_pages = None; + insight_requests; + log_kernel_debug_file = Some log_file; + } + + let simulate_call call = + let open Lwt_result_syntax in + let* bytes = + call_simulation + ~log_file:"simulate_call" + ~input_encoder:Simulation.encode + ~input:call in - Simulation.call_result results + Lwt.return (Simulation.simulation_result bytes) let call_estimate_gas call = let open Lwt_result_syntax in - let*? messages = Simulation.encode call in - let insight_requests = - [ - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_gas"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_result"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_status"]; - ] - in - let* results = - SimulationBackend.simulate_and_read - ~input: - { - messages; - reveal_pages = None; - insight_requests; - log_kernel_debug_file = Some "estimate_gas"; - } + let* bytes = + call_simulation + ~log_file:"estimate_gas" + ~input_encoder:Simulation.encode + ~input:call in - Simulation.gas_estimation results + Lwt.return (Simulation.gas_estimation bytes) let rec confirm_gas (call : Ethereum_types.call) gas = let open Ethereum_types in @@ -67,41 +57,31 @@ module Make (SimulationBackend : SimulationBackend) = struct let new_call = {call with gas = Some gas} in let* result = call_estimate_gas new_call in match result with - | Error _ -> + | Error _ | Ok (Error _) -> (* TODO: https://gitlab.com/tezos/tezos/-/issues/6984 All errors should not be treated the same *) let new_gas = double gas in if reached_max new_gas then failwith "Gas estimate reached max gas limit." else confirm_gas call new_gas - | Ok _ -> return (Ok gas) + | Ok (Ok _) -> return gas let estimate_gas call = let open Lwt_result_syntax in let* res = call_estimate_gas call in match res with - | Ok gas -> confirm_gas call gas - | Error e -> failwith "Couldn't estimate gas: %s" e + | Ok (Ok {gas_used = Some gas; value}) -> + let+ gas_used = confirm_gas call gas in + Ok (Ok {Simulation.gas_used = Some gas_used; value}) + | _ -> return res let is_tx_valid tx_raw = let open Lwt_result_syntax in - let*? messages = Simulation.encode_tx tx_raw in - let insight_requests = - [ - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_gas"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_result"]; - Simulation.Encodings.Durable_storage_key ["evm"; "simulation_status"]; - ] - in - let* results = - SimulationBackend.simulate_and_read - ~input: - { - messages; - reveal_pages = None; - insight_requests; - log_kernel_debug_file = Some "tx_validity"; - } + let* bytes = + call_simulation + ~log_file:"tx_validity" + ~input_encoder:Simulation.encode_tx + ~input:tx_raw in - Simulation.is_tx_valid results + Lwt.return (Simulation.is_tx_valid bytes) end diff --git a/etherlink/bin_node/lib_dev/tx_pool.ml b/etherlink/bin_node/lib_dev/tx_pool.ml index 1c99945603058143475193f1feb8756e6ea91ad3..3be18cb1e23aa789a0b7a598e0aa1ac8cb1026f6 100644 --- a/etherlink/bin_node/lib_dev/tx_pool.ml +++ b/etherlink/bin_node/lib_dev/tx_pool.ml @@ -257,9 +257,9 @@ let on_normal_transaction state tx_raw = ~transaction:(Hex.of_string tx_raw |> Hex.show) in return (Error err) - | Ok pkey -> + | Ok {address} -> (* Add the tx to the pool*) - let*? pool = Pool.add pool pkey base_fee tx_raw in + let*? pool = Pool.add pool address base_fee tx_raw in (* compute the hash *) let tx_hash = Ethereum_types.hash_raw_tx tx_raw in let hash = diff --git a/etherlink/kernel_evm/ethereum/src/rlp_helpers.rs b/etherlink/kernel_evm/ethereum/src/rlp_helpers.rs index ea33bb6f784a3e74afb3c562213e32a1c8d10ce2..615abe9bfa0c9ef9e8f6a8b2fbfc59d2a1083e8a 100644 --- a/etherlink/kernel_evm/ethereum/src/rlp_helpers.rs +++ b/etherlink/kernel_evm/ethereum/src/rlp_helpers.rs @@ -16,6 +16,16 @@ pub fn next<'a, 'v>(decoder: &mut RlpIterator<'a, 'v>) -> Result, Decode decoder.next().ok_or(DecoderError::RlpIncorrectListLen) } +pub fn check_list(decoder: &Rlp<'_>, length: usize) -> Result<(), DecoderError> { + if !decoder.is_list() { + Err(DecoderError::RlpExpectedToBeList) + } else if decoder.item_count() != Ok(length) { + Err(DecoderError::RlpIncorrectListLen) + } else { + Ok(()) + } +} + pub fn decode_field( decoder: &Rlp<'_>, field_name: &'static str, @@ -268,3 +278,33 @@ pub fn decode_timestamp(decoder: &Rlp<'_>) -> Result { .into(); Ok(timestamp) } + +/// Hardcoding the option RLP encoding for u64 in little endian. This is +/// unfortunately necessary as we cannot redefine the u64 encoding. +pub fn append_option_u64_le(v: &Option, stream: &mut rlp::RlpStream) { + match v { + None => { + stream.begin_list(0); + } + Some(value) => { + stream.begin_list(1); + append_u64_le(stream, value); + } + } +} + +/// See [append_option_u64_le] +pub fn decode_option_u64_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result, DecoderError> { + let items = decoder.item_count()?; + match items { + 1 => { + let mut it = decoder.iter(); + Ok(Some(decode_field_u64_le(&next(&mut it)?, field_name)?)) + } + 0 => Ok(None), + _ => Err(DecoderError::RlpIncorrectListLen), + } +} diff --git a/etherlink/kernel_evm/kernel/src/simulation.rs b/etherlink/kernel_evm/kernel/src/simulation.rs index 894e44738bb2de932a724c2a7a39763560c31381..0ceeb66359aa0d04137a89ebbbb0fbe3f82f4633 100644 --- a/etherlink/kernel_evm/kernel/src/simulation.rs +++ b/etherlink/kernel_evm/kernel/src/simulation.rs @@ -15,13 +15,17 @@ use crate::{ tick_model, CONFIG, }; +use evm::ExitReason; use evm_execution::handler::ExtendedExitReason; use evm_execution::{account_storage, handler::ExecutionOutcome, precompiles}; use evm_execution::{run_transaction, EthereumError}; use primitive_types::{H160, U256}; -use rlp::{Decodable, DecoderError, Rlp}; +use rlp::{Decodable, DecoderError, Encodable, Rlp}; use tezos_ethereum::block::BlockConstants; -use tezos_ethereum::rlp_helpers::{decode_field, decode_option, next}; +use tezos_ethereum::rlp_helpers::{ + append_option_u64_le, check_list, decode_field, decode_option, decode_option_u64_le, + next, +}; use tezos_ethereum::tx_common::EthereumTransactionCommon; use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_host::runtime::Runtime; @@ -37,6 +41,99 @@ pub const EVALUATION_TAG: u8 = 0x00; /// Tag indicating simulation is a validation. pub const VALIDATION_TAG: u8 = 0x01; +pub const OK_TAG: u8 = 0x1; +pub const ERR_TAG: u8 = 0x2; + +const INCORRECT_SIGNATURE: &str = "Incorrect signature."; +const INVALID_CHAIN_ID: &str = "Invalid chain id."; +const NONCE_TOO_LOW: &str = "Nonce too low."; +const MAX_GAS_FEE_TOO_LOW: &str = "Max gas fee too low."; +const OUT_OF_TICKS_MSG: &str = "The transaction would exhaust all the ticks it + is allocated. Try reducing its gas consumption or splitting the call in + multiple steps, if possible."; + +// Redefined Result as we cannot implement Decodable and Encodable traits on Result +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SimulationResult { + Ok(T), + Err(E), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ExecutionResult { + value: Option>, + gas_used: Option, +} + +type CallResult = SimulationResult>; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ValidationResult { + address: H160, +} + +impl Encodable for SimulationResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match self { + Self::Ok(value) => { + stream.append(&OK_TAG); + stream.append(value) + } + Self::Err(e) => { + stream.append(&ERR_TAG); + stream.append(e) + } + }; + } +} + +impl Decodable for SimulationResult { + fn decode(decoder: &Rlp<'_>) -> Result { + check_list(decoder, 2)?; + + let mut it = decoder.iter(); + match decode_field(&next(&mut it)?, "tag")? { + OK_TAG => Ok(Self::Ok(decode_field(&next(&mut it)?, "ok")?)), + ERR_TAG => Ok(Self::Err(decode_field(&next(&mut it)?, "error")?)), + _ => Err(DecoderError::Custom("Invalid execution tag")), + } + } +} + +impl Encodable for ExecutionResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append(&self.value); + append_option_u64_le(&self.gas_used, stream); + } +} + +impl Decodable for ExecutionResult { + fn decode(decoder: &Rlp<'_>) -> Result { + check_list(decoder, 2)?; + + let mut it = decoder.iter(); + let value = decode_field(&next(&mut it)?, "value")?; + let gas_used = decode_option_u64_le(&next(&mut it)?, "gas_used")?; + Ok(ExecutionResult { value, gas_used }) + } +} + +impl Encodable for ValidationResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.append(&self.address); + } +} + +impl Decodable for ValidationResult { + fn decode(decoder: &Rlp) -> Result { + Ok(ValidationResult { + address: decode_field(decoder, "caller")?, + }) + } +} + /// Container for eth_call data, used in messages sent by the rollup node /// simulation. /// @@ -76,11 +173,46 @@ pub struct Evaluation { pub data: Vec, } -#[derive(Debug, PartialEq)] -pub enum EvaluationOutcome { - EvaluationError(EthereumError), - Outcome(Option), - OutOfTicks, +impl From for SimulationResult { + fn from(err: EthereumError) -> Self { + let msg = format!("The transaction failed: {:?}.", err); + Self::Err(msg) + } +} + +impl From, EthereumError>> + for SimulationResult +{ + fn from(result: Result, EthereumError>) -> Self { + match result { + Ok(Some(ExecutionOutcome { + gas_used, + reason: ExtendedExitReason::Exit(ExitReason::Succeed(_)), + result, + .. + })) => Self::Ok(SimulationResult::Ok(ExecutionResult { + value: result, + gas_used: Some(gas_used), + })), + Ok(Some(ExecutionOutcome { + reason: ExtendedExitReason::Exit(ExitReason::Revert(_)), + result, + .. + })) => Self::Ok(SimulationResult::Err(result.unwrap_or_default())), + Ok(Some(ExecutionOutcome { + reason: ExtendedExitReason::OutOfTicks, + .. + })) => Self::Err(String::from(OUT_OF_TICKS_MSG)), + Ok(Some(ExecutionOutcome { reason, .. })) => { + let msg = format!("The transaction failed: {:?}.", reason); + Self::Err(msg) + } + Ok(None) => Self::Err(String::from( + "No outcome was produced when the transaction was ran", + )), + Err(err) => err.into(), + } + } } impl Evaluation { @@ -94,7 +226,7 @@ impl Evaluation { pub fn run( &self, host: &mut Host, - ) -> Result { + ) -> Result, Error> { let chain_id = retrieve_chain_id(host)?; let block_fees = retrieve_block_fees(host)?; @@ -141,15 +273,6 @@ impl Evaluation { false, false, ) { - Ok(Some(ExecutionOutcome { - reason: ExtendedExitReason::OutOfTicks, - .. - })) - | Err(evm_execution::EthereumError::OutOfTicks) => { - Ok(EvaluationOutcome::OutOfTicks) - } - Err(err) => Ok(EvaluationOutcome::EvaluationError(err)), - Ok(None) => Ok(EvaluationOutcome::Outcome(None)), Ok(Some(outcome)) => { let outcome = simulation_add_gas_for_fees( outcome, @@ -159,8 +282,12 @@ impl Evaluation { ) .map_err(Error::Simulation)?; - Ok(EvaluationOutcome::Outcome(Some(outcome))) + let result: SimulationResult = + Result::Ok(Some(outcome)).into(); + + Ok(result) } + result => Ok(result.into()), } } } @@ -212,16 +339,6 @@ struct TxValidation { transaction: EthereumTransactionCommon, } -#[derive(Debug, PartialEq)] -enum TxValidationOutcome { - Valid(H160), - NonceTooLow, - NotCorrectSignature, - InvalidChainId, - MaxGasFeeTooLow, - OutOfTicks, -} - impl TxValidation { // Run the transaction and ensure // - it won't fail with out-of-ticks @@ -230,7 +347,7 @@ impl TxValidation { host: &mut Host, transaction: &EthereumTransactionCommon, caller: &H160, - ) -> Result { + ) -> Result, anyhow::Error> { let chain_id = retrieve_chain_id(host)?; let block_fees = retrieve_block_fees(host)?; @@ -254,7 +371,7 @@ impl TxValidation { ); let Ok(gas_limit) = tx_execution_gas_limit(transaction, &block_fees) else { - return Ok(TxValidationOutcome::MaxGasFeeTooLow); + return Self::to_error(MAX_GAS_FEE_TOO_LOW); }; match run_transaction( @@ -277,20 +394,28 @@ impl TxValidation { Ok(Some(ExecutionOutcome { reason: ExtendedExitReason::OutOfTicks, .. - })) => Ok(TxValidationOutcome::OutOfTicks), - _ => Ok(TxValidationOutcome::Valid(*caller)), + })) => Self::to_error(OUT_OF_TICKS_MSG), + // TODO: #6498, 6813 + // Check for PREPAY + _ => Ok(SimulationResult::Ok(ValidationResult { address: *caller })), } } + pub fn to_error( + msg: &str, + ) -> Result, anyhow::Error> { + Ok(SimulationResult::Err(String::from(msg))) + } + /// Execute the simulation pub fn run( &self, host: &mut Host, - ) -> Result { + ) -> Result, anyhow::Error> { let tx = &self.transaction; let evm_account_storage = account_storage::init_account_storage()?; // Get the caller - let Ok(caller) = tx.caller() else {return Ok(TxValidationOutcome::NotCorrectSignature)}; + let Ok(caller) = tx.caller() else {return Self::to_error(INCORRECT_SIGNATURE)}; // Get the caller account let caller_account_path = evm_execution::account_storage::account_path(&caller)?; let caller_account = evm_account_storage.get(host, &caller_account_path)?; @@ -304,15 +429,15 @@ impl TxValidation { let chain_id = storage::read_chain_id(host)?; // Check if nonce is too low if tx.nonce < caller_nonce { - return Ok(TxValidationOutcome::NonceTooLow); + return Self::to_error(NONCE_TOO_LOW); } // Check if the chain id is correct if tx.chain_id.is_some() && tx.chain_id != Some(chain_id) { - return Ok(TxValidationOutcome::InvalidChainId); + return Self::to_error(INVALID_CHAIN_ID); } // Check if the gas price is high enough if tx.max_fee_per_gas < block_fees.base_fee_per_gas() { - return Ok(TxValidationOutcome::MaxGasFeeTooLow); + return Self::to_error(MAX_GAS_FEE_TOO_LOW); } // Check if running the transaction (assuming it is valid) would run out // of ticks, or fail validation for another reason. @@ -443,84 +568,6 @@ fn parse_inbox(host: &mut Host) -> Result { } } -fn store_simulation_outcome( - host: &mut Host, - outcome: EvaluationOutcome, -) -> Result<(), anyhow::Error> { - log!(host, Debug, "outcome={:?} ", outcome); - match outcome { - EvaluationOutcome::Outcome(Some(outcome)) => { - storage::store_simulation_status(host, outcome.is_success)?; - storage::store_evaluation_gas(host, outcome.gas_used)?; - storage::store_simulation_result(host, outcome.result) - } - EvaluationOutcome::Outcome(None) => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result( - host, - Some(b"No outcome was produced when the transaction was ran".to_vec()), - ) - } - EvaluationOutcome::OutOfTicks => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result( - host, - Some( - b"The transaction would exhaust all the ticks it is allocated. \ - Try reducing its gas consumption or splitting the call in \ - multiple steps, if possible." - .to_vec(), - ), - ) - } - EvaluationOutcome::EvaluationError(err) => { - storage::store_simulation_status(host, false)?; - let msg = format!("The transaction failed: {:?}.", err); - storage::store_simulation_result(host, Some(msg.as_bytes().to_vec())) - } - } -} - -fn store_tx_validation_outcome( - host: &mut Host, - outcome: TxValidationOutcome, -) -> Result<(), anyhow::Error> { - match outcome { - TxValidationOutcome::Valid(caller) => { - storage::store_simulation_status(host, true)?; - storage::store_simulation_result(host, Some(caller.to_fixed_bytes().to_vec())) - } - TxValidationOutcome::NonceTooLow => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result(host, Some(b"Nonce too low.".to_vec())) - } - TxValidationOutcome::NotCorrectSignature => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result(host, Some(b"Incorrect signature.".to_vec())) - } - TxValidationOutcome::InvalidChainId => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result(host, Some(b"Invalid chain id.".to_vec())) - } - TxValidationOutcome::MaxGasFeeTooLow => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result(host, Some(b"Max gas fee too low.".to_vec())) - } - TxValidationOutcome::OutOfTicks => { - storage::store_simulation_status(host, false)?; - storage::store_simulation_result( - host, - Some( - b"The transaction would exhaust all the ticks it is allocated. \ - Try reducing its gas consumption or splitting the call in \ - multiple steps, if possible." - .to_vec(), - ), - ) - } - } -} - pub fn start_simulation_mode( host: &mut Host, ) -> Result<(), anyhow::Error> { @@ -529,11 +576,11 @@ pub fn start_simulation_mode( match simulation { Message::Evaluation(simulation) => { let outcome = simulation.run(host)?; - store_simulation_outcome(host, outcome) + storage::store_simulation_result(host, outcome) } Message::TxValidation(tx_validation) => { let outcome = tx_validation.run(host)?; - store_tx_validation_outcome(host, outcome) + storage::store_simulation_result(host, outcome) } } } @@ -702,17 +749,12 @@ mod tests { assert!(outcome.is_ok(), "evaluation should have succeeded"); let outcome = outcome.unwrap(); - if let EvaluationOutcome::Outcome(outcome) = outcome { - assert!( - outcome.is_some(), - "simulation should have produced some outcome" - ); - let outcome = outcome.unwrap(); - assert_eq!( - Some(vec![0u8; 32]), - outcome.result, - "simulation result should be 0" - ); + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "simulation result should be 0"); } else { panic!("evaluation should have reached outcome"); } @@ -730,17 +772,12 @@ mod tests { assert!(outcome.is_ok(), "simulation should have succeeded"); let outcome = outcome.unwrap(); - if let EvaluationOutcome::Outcome(outcome) = outcome { - assert!( - outcome.is_some(), - "simulation should have produced some outcome" - ); - let outcome = outcome.unwrap(); - assert_eq!( - Some(vec![0u8; 32]), - outcome.result, - "evaluation result should be 0" - ); + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "evaluation result should be 0"); } else { panic!("evaluation should have reached outcome"); } @@ -765,17 +802,12 @@ mod tests { assert!(outcome.is_ok(), "evaluation should have succeeded"); let outcome = outcome.unwrap(); - if let EvaluationOutcome::Outcome(outcome) = outcome { - assert!( - outcome.is_some(), - "simulation should have produced some outcome" - ); - let outcome = outcome.unwrap(); - assert_eq!( - Some(vec![0u8; 32]), - outcome.result, - "evaluation result should be 0" - ); + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "evaluation result should be 0"); } else { panic!("evaluation should have reached outcome"); } @@ -995,7 +1027,10 @@ mod tests { let result = simulation.run(&mut host); println!("{result:?}"); assert!(result.is_ok()); - assert_eq!(TxValidationOutcome::MaxGasFeeTooLow, result.unwrap()); + assert_eq!( + TxValidation::to_error(MAX_GAS_FEE_TOO_LOW).unwrap(), + result.unwrap() + ); } #[test] @@ -1041,6 +1076,42 @@ mod tests { let result = simulation.run(&mut host); assert!(result.is_ok()); - assert_eq!(TxValidationOutcome::MaxGasFeeTooLow, result.unwrap()); + assert_eq!( + SimulationResult::Err(String::from(super::MAX_GAS_FEE_TOO_LOW)), + result.unwrap() + ); + } + + pub fn check_roundtrip( + v: R, + ) { + let bytes = v.rlp_bytes(); + let decoder = Rlp::new(&bytes); + println!("{:?}", bytes); + let decoded = R::decode(&decoder).expect("Value should be decodable"); + assert_eq!(v, decoded, "Roundtrip failed on {:?}", v) + } + + #[test] + fn test_simulation_result_encoding_roundtrip() { + let valid: SimulationResult = + SimulationResult::Ok(ValidationResult { + address: address_from_str("f95abdf6ede4c3703e0e9453771fbee8592d31e9") + .unwrap(), + }); + let call: SimulationResult = + SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value: Some(vec![0, 1, 2, 3]), + gas_used: Some(123), + })); + let revert: SimulationResult = + SimulationResult::Ok(SimulationResult::Err(vec![3, 2, 1, 0])); + let error: SimulationResult = + SimulationResult::Err(String::from("Un festival de GADTs")); + + check_roundtrip(valid); + check_roundtrip(call); + check_roundtrip(revert); + check_roundtrip(error) } } diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index c5e04312075218010725d98134ea8f3b14b97f63..7707864c9a1feb46be9fc2a6c7485564f9a4b0d3 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -8,6 +8,7 @@ use crate::block_in_progress::BlockInProgress; use crate::event::Event; use crate::indexable_storage::IndexableStorage; +use crate::simulation::SimulationResult; use anyhow::Context; use evm_execution::account_storage::EthereumAccount; use evm_execution::storage::blocks::add_new_block_hash; @@ -80,8 +81,6 @@ const EVM_INFO_PER_LEVEL_STATS_TOTAL: RefPath = RefPath::assert_from(b"/info_per_level/stats/total"); pub const SIMULATION_RESULT: RefPath = RefPath::assert_from(b"/simulation_result"); -pub const SIMULATION_STATUS: RefPath = RefPath::assert_from(b"/simulation_status"); -pub const SIMULATION_GAS: RefPath = RefPath::assert_from(b"/simulation_gas"); pub const DEPOSIT_NONCE: RefPath = RefPath::assert_from(b"/deposit_nonce"); @@ -297,30 +296,13 @@ pub fn store_current_block( } } } - -pub fn store_simulation_result( - host: &mut Host, - result: Option>, -) -> Result<(), anyhow::Error> { - if let Some(result) = result { - host.store_write(&SIMULATION_RESULT, &result, 0)? - } - Ok(()) -} - -pub fn store_evaluation_gas( - host: &mut Host, - result: u64, -) -> Result<(), Error> { - write_u256(host, &SIMULATION_GAS.into(), U256::from(result)) -} - -pub fn store_simulation_status( +pub fn store_simulation_result( host: &mut Host, - result: bool, + result: SimulationResult, ) -> Result<(), anyhow::Error> { - host.store_write(&SIMULATION_STATUS, &[result.into()], 0) - .context("Failed to write the simulation status.") + let encoded = result.rlp_bytes(); + host.store_write(&SIMULATION_RESULT, &encoded, 0) + .context("Failed to write the simulation result.") } pub fn store_transaction_receipt( diff --git a/etherlink/tezt/tests/evm_rollup.ml b/etherlink/tezt/tests/evm_rollup.ml index 50a0b37c6c3a764270e044c4f8f07ff6df79f856..9282b95b4d73c36391a847d2b1e84a26ae8776dd 100644 --- a/etherlink/tezt/tests/evm_rollup.ml +++ b/etherlink/tezt/tests/evm_rollup.ml @@ -2608,48 +2608,6 @@ let test_rpc_getTransactionByBlockNumberAndIndex = ~config @@ fun ~protocol:_ -> test_rpc_getTransactionByBlockArgAndIndex ~by:`Number -let test_validation_result = - register_both - ~tags:["evm"; "simulate"] - ~title: - "Ensure validation returns appropriate address for a given transaction." - ~minimum_base_fee_per_gas:base_fee_for_hardcoded_tx - @@ fun ~protocol:_ ~evm_setup:{sc_rollup_node; _} -> - (* tx is a signed legacy transaction obtained with the following data, using - the following private key: - data = { - "nonce": "0x00", - "gasPrice": "0x5208", - "gasLimit": "0x00", - "to": "0x0000000000000000000000000000000000000000", - "value": "0x00", - "data": "0x", - "chainId": 1337 - } - private key=0x84e147b8bc36d99cc6b1676318a0635d8febc9f02897b0563ad27358589ee502 - *) - let tx = - "f86180825208809400000000000000000000000000000000000000008080820a95a0f47140763cf73d6d9b342727e5a0809f7997bb62375060932af9bbc2e74b6212a03a018079a2fd7fefb625451ce2fafcdf873b892ff9d4e3e1f2ada5650012f072" - in - let simulation_msg = "ff0101" ^ tx in - let* simulation_result = - Sc_rollup_node.RPC.call sc_rollup_node - @@ Sc_rollup_rpc.post_global_block_simulate - ~insight_requests: - [ - `Durable_storage_key ["evm"; "simulation_status"]; - `Durable_storage_key ["evm"; "simulation_result"]; - ] - [Hex.to_string @@ `Hex "ff"; Hex.to_string @@ `Hex simulation_msg] - in - let expected_insights = - [Some "01"; Some "f0affc80a5f69f4a9a3ee01a640873b6ba53e539"] - in - Check.( - (simulation_result.insights = expected_insights) (list @@ option string)) - ~error_msg:"Expected result %R, but got %L" ; - unit - type storage_migration_results = { transfer_result : transfer_result; block_result : Block.t; @@ -4842,7 +4800,6 @@ let register_evm_node ~protocols = test_rpc_gasPrice protocols ; test_rpc_getStorageAt protocols ; test_accounts_double_indexing protocols ; - test_validation_result protocols ; test_rpc_sendRawTransaction_with_consecutive_nonce protocols ; test_rpc_sendRawTransaction_not_included protocols ; test_originate_evm_kernel_and_dump_pvm_state protocols ;