diff --git a/src/bin_evm_proxy/lib/ethereum_types.ml b/src/bin_evm_proxy/lib/ethereum_types.ml index 268bb415b6c424d23e11e4a7c544b2eb81a84fb7..5ca11d389af0b67cb4be2e34d379de7e41737fd3 100644 --- a/src/bin_evm_proxy/lib/ethereum_types.ml +++ b/src/bin_evm_proxy/lib/ethereum_types.ml @@ -26,6 +26,13 @@ (** Transaction hash size is 32 bytes. *) let transaction_hash_size = 32 +(** Translate an int in a binary string of two bytes (little endian). + Ints greater than 2 bytes are truncated. *) +let u16_to_bytes n = + let bytes = Bytes.make 2 'a' in + Bytes.set_uint16_le bytes 0 n ; + Bytes.to_string bytes + (** Append the [0x] prefix to a string. *) let append_0x s = "0x" ^ s @@ -116,7 +123,7 @@ let block_hash_encoding = Data_encoding.( conv (fun (Block_hash h) -> append_0x h) block_hash_of_string string) -(** Ethereum any hash. *) +(** Ethereum hash or data, that would encoded with a 0x prefix. *) type hash = Hash of string [@@ocaml.unboxed] (** [hash_of_string s] takes a string [s] representing a hash in diff --git a/src/bin_evm_proxy/lib/mockup.ml b/src/bin_evm_proxy/lib/mockup.ml index 2c0021e69b56eb7092ba2f5728c6b1e6af62252e..e7fc40c2b75e7b65a48d2ba80cba895b904aa56e 100644 --- a/src/bin_evm_proxy/lib/mockup.ml +++ b/src/bin_evm_proxy/lib/mockup.ml @@ -139,7 +139,7 @@ let transaction_object = s = hash_f @@ "00"; } -let call = empty_hash +let simulate_call _ = Lwt_result_syntax.return (hash_of_string "0x000102030405") let smart_rollup_address = return "foo" diff --git a/src/bin_evm_proxy/lib/rollup_node.ml b/src/bin_evm_proxy/lib/rollup_node.ml index 70be4a8b74cd2673e74bde906a18ba7a3ca418af..c3e1902339bae6b010337680215e37f4a3294ac5 100644 --- a/src/bin_evm_proxy/lib/rollup_node.ml +++ b/src/bin_evm_proxy/lib/rollup_node.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2023 Nomadic Labs *) +(* Copyright (c) 2023 Marigold *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -25,11 +26,6 @@ open Ethereum_types -let u16_to_bytes n = - let bytes = Bytes.make 2 'a' in - Bytes.set_uint16_le bytes 0 n ; - Bytes.to_string bytes - (* The hard limit is 4096 but it needs to add the external message tag. *) let max_input_size = 4095 @@ -45,7 +41,7 @@ let encode_transaction ~smart_rollup_address kind = match kind with | Simple data -> "\000" ^ data | NewChunked (hash, len) -> - let number_of_chunks_bytes = u16_to_bytes len in + let number_of_chunks_bytes = Ethereum_types.u16_to_bytes len in "\001" ^ hash ^ number_of_chunks_bytes | Chunk data -> "\002" ^ data in @@ -74,7 +70,9 @@ let make_evm_inbox_transactions tx_raw = let* chunks = String.chunk_bytes size_per_chunk (Bytes.of_string tx_raw) in let new_chunk_transaction = NewChunked (tx_hash, List.length chunks) in let chunks = - List.mapi (fun i chunk -> Chunk (tx_hash ^ u16_to_bytes i ^ chunk)) chunks + List.mapi + (fun i chunk -> Chunk (tx_hash ^ Ethereum_types.u16_to_bytes i ^ chunk)) + chunks in return (tx_hash, new_chunk_transaction :: chunks) @@ -212,6 +210,16 @@ module RPC = struct (list string)) (open_root / "local" / "batcher" / "injection") + let simulation = + Tezos_rpc.Service.post_service + ~description: + "Simulate messages evaluation by the PVM, and find result in durable \ + storage" + ~query:Tezos_rpc.Query.empty + ~input:Simulation.Encodings.simulate_input + ~output:Data_encoding.Json.encoding + (open_root / "global" / "block" / "head" / "simulate") + let call_service ~base ?(media_types = Media_type.all_media_types) = Tezos_rpc_http_client_unix.RPC_client_unix.call_service media_types ~base @@ -454,6 +462,27 @@ module RPC = struct let chain_id base () = inspect_durable_and_decode base Durable_storage_path.chain_id decode_number + + let simulate_call base call = + let open Lwt_result_syntax in + let*? messages = Simulation.encode call in + let insight_requests = + [ + Simulation.Encodings.Durable_storage_key ["evm"; "simulation_result"]; + (* TODO: https://gitlab.com/tezos/tezos/-/issues/5900 + for now the status is not used but it should be for error handling *) + Simulation.Encodings.Durable_storage_key ["evm"; "simulation_status"]; + ] + in + let* r = + call_service + ~base + simulation + () + () + {messages; reveal_pages = None; insight_requests} + in + Simulation.parse_insights r end module type S = sig @@ -487,6 +516,8 @@ module type S = sig val txpool : unit -> Ethereum_types.txpool tzresult Lwt.t val chain_id : unit -> Ethereum_types.quantity tzresult Lwt.t + + val simulate_call : Ethereum_types.call -> Ethereum_types.hash tzresult Lwt.t end module Make (Base : sig @@ -515,4 +546,6 @@ end) : S = struct let txpool = RPC.txpool Base.base let chain_id = RPC.chain_id Base.base + + let simulate_call = RPC.simulate_call Base.base end diff --git a/src/bin_evm_proxy/lib/rollup_node.mli b/src/bin_evm_proxy/lib/rollup_node.mli index 5328b4fbdaf66fc02e29085808a62d0fa4628587..473edf2ea400f38c4aae29ec4a116371decc7a82 100644 --- a/src/bin_evm_proxy/lib/rollup_node.mli +++ b/src/bin_evm_proxy/lib/rollup_node.mli @@ -97,6 +97,10 @@ module type S = sig (** [chain_id ()] returns chain id defined by the rollup. *) val chain_id : unit -> Ethereum_types.quantity tzresult Lwt.t + + (** [simulate_call call_info] asks the rollup to simulate a call, and returns the + result. *) + val simulate_call : Ethereum_types.call -> Ethereum_types.hash tzresult Lwt.t end (** Instantiate a module of type {!S} that communicates with a rollup diff --git a/src/bin_evm_proxy/lib/services.ml b/src/bin_evm_proxy/lib/services.ml index 609e66f3648623ef90089554a095291a8d5b6772..7b455a97d0e2c976b91cbd6a8993eddb28b7fec6 100644 --- a/src/bin_evm_proxy/lib/services.ml +++ b/src/bin_evm_proxy/lib/services.ml @@ -140,7 +140,9 @@ let dispatch_input return (Send_raw_transaction.Output (Ok tx_hash)) | Send_transaction.Input _ -> return (Send_transaction.Output (Ok Mockup.transaction_hash)) - | Eth_call.Input _ -> return (Eth_call.Output (Ok Mockup.call)) + | Eth_call.Input (Some (call, _)) -> + let* call_result = Rollup_node_rpc.simulate_call call in + return (Eth_call.Output (Ok call_result)) | Get_estimate_gas.Input _ -> return (Get_estimate_gas.Output (Ok Mockup.gas_price)) | Txpool_content.Input _ -> diff --git a/src/bin_evm_proxy/lib/simulation.ml b/src/bin_evm_proxy/lib/simulation.ml new file mode 100644 index 0000000000000000000000000000000000000000..a1c024b186e7e4f6738500c95f4c1fac5ad97d44 --- /dev/null +++ b/src/bin_evm_proxy/lib/simulation.ml @@ -0,0 +1,220 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2023 Marigold *) +(* *) +(* 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 Ethereum_types + +(** [rope_of_hex_string s] transforms a hex string [s] into a byte string + represented by a [Rope.t] *) +let rope_of_hex_string s = + `Hex s |> Hex.to_bytes_exn |> Bytes.to_string |> Rope.of_string + +(** Encoding used to forward the call to the kernel, to be used in simulation + mode only. *) +let rlp_encode call = + let of_opt of_val = function + | None -> Rlp.RlpData Rope.empty + | Some v -> of_val v + in + let of_addr (Address s) = Rlp.RlpData (rope_of_hex_string s) in + let of_qty (Qty z) = Rlp.RlpData (Rope.of_string @@ Z.to_bits z) in + let of_hash (Hash h) = Rlp.RlpData (rope_of_hex_string h) in + let rlp_form = + Rlp.RlpList + [ + of_opt of_addr call.from; + of_opt of_addr call.to_; + (* TODO: https://gitlab.com/tezos/tezos/-/issues/5863 + evm_execution will fail a simulation without gas *) + of_qty (Option.value ~default:(Qty (Z.of_int 11111)) call.gas); + of_opt of_qty call.gasPrice; + of_opt of_qty call.value; + of_opt of_hash call.data; + ] + in + (* we aim to use [String.chunk_bytes] *) + Rlp.encode rlp_form |> Rope.to_string |> Bytes.of_string + +type simulation_message = + | Start + | Simple of string + | NewChunked of int + | Chunk of int * string + +(* Max input size : 4096B + - Simulation tag : 1B + - Chunk tag : 1B + - Number of chunks : 2B *) +let max_chunk_size = 4092 + +let split_in_messages call = + let open Result_syntax in + let* chunks = String.chunk_bytes max_chunk_size call in + match chunks with + | [s] -> return [Start; Simple s] + | l -> + let len = List.length l in + let chunks = List.mapi (fun i c -> Chunk (i, c)) l in + return (Start :: NewChunked len :: chunks) + +(** Tag signaling a simulation message *) +let simulation_tag = "\255" + +(** Tag signaling a simulation message containing a full simulation call *) +let simple_tag = "\001" + +(** Tag signaling a simulation message starting a serie of chunks *) +let new_chunked_tag = "\002" + +(** Tag signaling a simulation message containing a chunk *) +let chunk_tag = "\003" + +(** [hex_str_of_binary_string s] translate a binary string into an hax string *) +let hex_str_of_binary_string s = s |> Hex.of_string |> Hex.show + +let encode_message = function + | Start -> hex_str_of_binary_string @@ simulation_tag + | Simple s -> hex_str_of_binary_string @@ simulation_tag ^ simple_tag ^ s + | NewChunked n -> + let n_le_str = Ethereum_types.u16_to_bytes n in + hex_str_of_binary_string @@ simulation_tag ^ new_chunked_tag ^ n_le_str + | Chunk (i, c) -> + let i_le_str = Ethereum_types.u16_to_bytes i in + hex_str_of_binary_string @@ simulation_tag ^ chunk_tag ^ i_le_str ^ c + +let encode call = + let open Result_syntax in + let* messages = call |> rlp_encode |> split_in_messages in + return @@ List.map encode_message messages + +module Encodings = struct + open Data_encoding + + type eval_result = { + state_hash : string; + status : string; + output : unit; + inbox_level : unit; + num_ticks : Z.t; + insights : bytes option list; + (** The simulation can ask to look at values on the state after + the simulation. *) + } + + type insight_request = + | Pvm_state_key of string list + | Durable_storage_key of string list + + type simulate_input = { + messages : string list; + reveal_pages : string list option; + insight_requests : insight_request list; + } + + let hex_string = conv Bytes.of_string Bytes.to_string bytes + + let insight_request = + union + [ + case + (Tag 0) + ~title:"pvm_state" + ~description:"Path in the PVM state" + (obj2 (req "kind" (constant "pvm_state")) (req "key" (list string))) + (function Pvm_state_key key -> Some ((), key) | _ -> None) + (fun ((), key) -> Pvm_state_key key); + case + (Tag 1) + ~title:"durable_storage" + ~description:"Path in the PVM durable storage" + (obj2 + (req "kind" (constant "durable_storage")) + (req "key" (list string))) + (function Durable_storage_key key -> Some ((), key) | _ -> None) + (fun ((), key) -> Durable_storage_key key); + ] + + let simulate_input = + conv + (fun {messages; reveal_pages; insight_requests} -> + (messages, reveal_pages, insight_requests)) + (fun (messages, reveal_pages, insight_requests) -> + {messages; reveal_pages; insight_requests}) + @@ obj3 + (req + "messages" + (list string) + ~description:"Serialized messages for simulation.") + (opt + "reveal_pages" + (list hex_string) + ~description:"Pages (at most 4kB) to be used for revelation ticks") + (dft + "insight_requests" + (list insight_request) + [] + ~description:"Paths in the PVM to inspect after the simulation") + + let eval_result = + conv + (fun {state_hash; status; output; inbox_level; num_ticks; insights} -> + (state_hash, status, output, inbox_level, num_ticks, insights)) + (fun (state_hash, status, output, inbox_level, num_ticks, insights) -> + {state_hash; status; output; inbox_level; num_ticks; insights}) + @@ obj6 + (req + "state_hash" + string + ~description: + "Hash of the state after execution of the PVM on the input \ + messages") + (req "status" string ~description:"Status of the PVM after evaluation") + (req + "output" + unit + ~description:"Output produced by evaluation of the messages") + (req + "inbox_level" + unit + ~description:"Level of the inbox that would contain these messages") + (req + "num_ticks" + z + ~description:"Ticks taken by the PVM for evaluating the messages") + (req + "insights" + (list (option bytes)) + ~description:"PVM state values requested after the simulation") +end + +let parse_insights (r : Data_encoding.json) = + let s = Data_encoding.Json.destruct Encodings.eval_result r in + match s.insights with + | Some b :: _ -> + let v = b |> Hex.of_bytes |> Hex.show in + Lwt.return_ok (Hash v) + | _ -> + Error_monad.failwith + "Couldn't parse insights: %s" + (Data_encoding.Json.to_string r) diff --git a/src/kernel_evm/kernel/src/simulation.rs b/src/kernel_evm/kernel/src/simulation.rs index d081632ab2cdde967c5bf56647e23dfbc96dda2f..bc1eba6d1eb32b63198b8f66c742f7628c2a7603 100644 --- a/src/kernel_evm/kernel/src/simulation.rs +++ b/src/kernel_evm/kernel/src/simulation.rs @@ -15,7 +15,7 @@ use evm_execution::{account_storage, handler::ExecutionOutcome, precompiles}; use primitive_types::{H160, U256}; use rlp::{Decodable, DecoderError, Rlp}; use tezos_ethereum::rlp_helpers::{decode_field, decode_option, next}; -use tezos_smart_rollup_debug::Runtime; +use tezos_smart_rollup_debug::{debug_msg, Runtime}; // SIMULATION/SIMPLE/RLP_ENCODED_SIMULATION pub const SIMULATION_SIMPLE_TAG: u8 = 1; @@ -44,19 +44,20 @@ pub struct Simulation { /// (optional) The address the transaction is sent from.\ /// Encoding: 20 bytes or empty (0x80) pub from: Option, - /// The address the transaction is directed to.\ + /// The address the transaction is directed to. + /// Some indexer seem to expect it to be optionnal\ /// Encoding: 20 bytes - pub to: H160, + pub to: Option, /// (optional) Integer of the gas provided for the transaction execution. /// eth_call consumes zero gas, but this parameter may be needed by some /// executions.\ - /// Encoding: big endian + /// Encoding: little endian pub gas: Option, /// (optional) Integer of the gasPrice used for each paid gas\ - /// Encoding: big endian + /// Encoding: little endian pub gas_price: Option, /// (optional) Integer of the value sent with this transaction (in Wei)\ - /// Encoding: big endian + /// Encoding: little endian pub value: Option, /// (optional) Hash of the method signature and encoded parameters. pub data: Vec, @@ -81,7 +82,7 @@ impl Simulation { ¤t_block.constants(), &mut evm_account_storage, &precompiles, - Some(self.to), + self.to, self.from.unwrap_or(default_caller), self.data.clone(), self.gas, @@ -94,14 +95,20 @@ impl Simulation { impl Decodable for Simulation { fn decode(decoder: &Rlp<'_>) -> Result { + // the proxynode works preferably with little endian + let u64_from_le = |v: Vec| u64::from_le_bytes(parsable!(v.try_into().ok())); + let u256_from_le = |v: Vec| U256::from_little_endian(&v); if decoder.is_list() { if Ok(6) == decoder.item_count() { let mut it = decoder.iter(); let from: Option = decode_option(&next(&mut it)?, "from")?; - let to: H160 = decode_field(&next(&mut it)?, "to")?; - let gas: Option = decode_option(&next(&mut it)?, "gas")?; - let gas_price: Option = decode_option(&next(&mut it)?, "gas_price")?; - let value: Option = decode_option(&next(&mut it)?, "value")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let gas: Option = + decode_option(&next(&mut it)?, "gas")?.map(u64_from_le); + let gas_price: Option = + decode_option(&next(&mut it)?, "gas_price")?.map(u64_from_le); + let value: Option = + decode_option(&next(&mut it)?, "value")?.map(u256_from_le); let data: Vec = decode_field(&next(&mut it)?, "data")?; Ok(Self { from, @@ -220,8 +227,11 @@ fn parse_inbox(host: &mut Host) -> Result { } pub fn start_simulation_mode(host: &mut Host) -> Result<(), Error> { + debug_msg!(host, "Starting simulation mode "); let simulation = parse_inbox(host)?; let outcome = simulation.run(host)?; + debug_msg!(host, "outcome={:?} ", outcome); + storage::store_simulation_status(host, outcome.is_success)?; storage::store_simulation_result(host, outcome.result) } @@ -244,9 +254,9 @@ mod tests { } } - fn address_of_str(s: &str) -> H160 { + fn address_of_str(s: &str) -> Option { let data = &hex::decode(s).unwrap(); - H160::from_slice(data) + Some(H160::from_slice(data)) } #[test] @@ -276,9 +286,9 @@ mod tests { #[test] fn test_decode_non_empty() { let input_string = - "f6942424242424242424242424242424242424242424943535353535353535353535353535353535353535822b678256ce828235821616".to_string(); + "f84894242424242424242424242424242424242424242494353535353535353535353535353535353535353588672b00000000000088ce56000000000000883582000000000000821616".to_string(); let to = address_of_str("3535353535353535353535353535353535353535"); - let from = Some(address_of_str("2424242424242424242424242424242424242424")); + let from = address_of_str("2424242424242424242424242424242424242424"); let data = hex::decode("1616").unwrap(); let expected = Simulation { from, @@ -349,7 +359,7 @@ mod tests { let simulation = Simulation { from: None, gas_price: None, - to: new_address, + to: Some(new_address), data: hex::decode(STORAGE_CONTRACT_CALL_NUM).unwrap(), gas: Some(10000), value: None, @@ -368,7 +378,7 @@ mod tests { let simulation = Simulation { from: None, gas_price: None, - to: new_address, + to: Some(new_address), data: hex::decode(STORAGE_CONTRACT_CALL_GET).unwrap(), gas: Some(10000), value: None, @@ -387,7 +397,7 @@ mod tests { #[test] fn parse_simulation() { let to = address_of_str("3535353535353535353535353535353535353535"); - let from = Some(address_of_str("2424242424242424242424242424242424242424")); + let from = address_of_str("2424242424242424242424242424242424242424"); let data = hex::decode("1616").unwrap(); let expected = Simulation { from, @@ -399,7 +409,7 @@ mod tests { }; let mut encoded = - hex::decode("f6942424242424242424242424242424242424242424943535353535353535353535353535353535353535822b678256ce828235821616").unwrap(); + hex::decode("f84894242424242424242424242424242424242424242494353535353535353535353535353535353535353588672b00000000000088ce56000000000000883582000000000000821616").unwrap(); let mut input = vec![parsing::SIMULATION_TAG, SIMULATION_SIMPLE_TAG]; input.append(&mut encoded); @@ -412,6 +422,44 @@ mod tests { ); } + #[test] + fn parse_simulation2() { + // setup + let mut host = MockHost::default(); + let new_address = create_contract(&mut host); + + let to = Some(new_address); + let data = hex::decode(STORAGE_CONTRACT_CALL_GET).unwrap(); + let gas = Some(11111); + let expected = Simulation { + from: None, + to, + gas, + gas_price: None, + value: None, + data, + }; + + let encoded = hex::decode( + "ff01e68094907823e0a92f94355968feb2cbf0fbb594fe321488672b0000000000008080846d4ce63c", + ) + .unwrap(); + + let parsed = Input::parse(&encoded); + assert_eq!( + Input::SimpleSimulation(expected), + parsed, + "should have been parsed as complete simulation" + ); + + if let Input::SimpleSimulation(s) = parsed { + let res = s.run(&mut host).expect("simulation should run"); + assert!(res.is_success, "simulation should have succeeded"); + } else { + panic!("Parsing failed") + } + } + #[test] fn parse_num_chunks() { let num: u16 = 42; diff --git a/src/kernel_evm/kernel/src/storage.rs b/src/kernel_evm/kernel/src/storage.rs index 5f29711c7c288bb78adc11e2daf03cff4526c380..3437799f4168e9e2f46e8efaaf221811aa2adabc 100644 --- a/src/kernel_evm/kernel/src/storage.rs +++ b/src/kernel_evm/kernel/src/storage.rs @@ -49,6 +49,7 @@ const EVM_INFO_PER_LEVEL_STATS_TOTAL: RefPath = RefPath::assert_from(b"/evm/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"); /// The size of an address. Size in bytes. const ADDRESS_SIZE: usize = 20; @@ -318,6 +319,14 @@ pub fn store_simulation_result( Ok(()) } +pub fn store_simulation_status( + host: &mut Host, + result: bool, +) -> Result<(), Error> { + host.store_write(&SIMULATION_STATUS, &[result.into()], 0) + .map_err(Error::from) +} + pub fn store_transaction_receipt( receipt_path: &OwnedPath, host: &mut Host, diff --git a/tezt/lib_ethereum/eth_cli.ml b/tezt/lib_ethereum/eth_cli.ml index 8e1a4e1802179379918e389d7adb1078141c3fc4..545d0fb09aab920d6e450aa8c230eae64e9df2de 100644 --- a/tezt/lib_ethereum/eth_cli.ml +++ b/tezt/lib_ethereum/eth_cli.ml @@ -108,8 +108,8 @@ let deploy ~source_private_key ~endpoint ~abi ~bin = ] decode -let call ?(expect_failure = false) ~source_private_key ~endpoint ~abi_label - ~address ~method_call () = +let contract_send ?(expect_failure = false) ~source_private_key ~endpoint + ~abi_label ~address ~method_call () = let command = [ "contract:send"; @@ -124,6 +124,19 @@ let call ?(expect_failure = false) ~source_private_key ~endpoint ~abi_label if expect_failure then spawn_command_and_read_string ~expect_failure command else spawn_command_and_read_json command JSON.as_string +let contract_call ?(expect_failure = false) ~endpoint ~abi_label ~address + ~method_call () = + let command = + [ + "contract:call"; + "--network"; + endpoint; + Format.sprintf "%s@%s" abi_label address; + method_call; + ] + in + spawn_command_and_read_string ~expect_failure command + let get_receipt ~endpoint ~tx = spawn_command_and_read_json_opt ["transaction:get"; tx; "--network"; endpoint] diff --git a/tezt/lib_ethereum/eth_cli.mli b/tezt/lib_ethereum/eth_cli.mli index 7f5bef62d40c6e2b1689843fef56cc4b52bc093a..f6b77357c91e548cac86b09f64349f649a23b6db 100644 --- a/tezt/lib_ethereum/eth_cli.mli +++ b/tezt/lib_ethereum/eth_cli.mli @@ -68,20 +68,22 @@ val deploy : bin:string -> (string * string) Lwt.t -(** [call ~source_private_key ~endpoint ~abi_label ~address ~method_call ()] - make a call to a contract found at [address], with interface registered as +(** [contract_send ~source_private_key ~endpoint ~abi_label ~address ~method_call ()] + makes a call to a contract found at [address], with interface registered as [abi_labbel] in the client, signed with a user's [source_private_key]. [method_call] is the call data, as a solidity expression. + This is a transaction to be included in a block. + example: - [call + [contract_send ~source_private_key ~endpoint ~abi_label:"storage" ~address:"0xaaaa....aaaa" ~method_call:"set(42)" ()]*) -val call : +val contract_send : ?expect_failure:bool -> source_private_key:string -> endpoint:string -> @@ -91,6 +93,33 @@ val call : unit -> string Lwt.t +(** [contract_call ~endpoint ~abi_label ~address ~method_call ()] + makes a call to a contract found at [address], with interface registered as + [abi_label] in the client, signed with a user's [source_private_key]. + [method_call] is the call data, as a solidity expression. + + This is a NOT transaction to be included in a block, but a simulation. It + can be a state modifying transaction, in which case the modification are not + included in a block. It can still be useful to test the result of a + transaction before executing it "for real", however the result might be + different as the state could change between testing and execution. + + example: + [contract_call + ~endpoint + ~abi_label:"storage" + ~address:"0xaaaa....aaaa" + ~method_call:"get()" + ()]*) +val contract_call : + ?expect_failure:bool -> + endpoint:string -> + abi_label:string -> + address:string -> + method_call:string -> + unit -> + string Lwt.t + (** [get_block ~block_id ~endpoint] asks the block [block_id] (it can be a hash or a number) to the JSON-RPC API server listening at [endpoint]. *) val get_block : block_id:string -> endpoint:string -> Block.t Lwt.t diff --git a/tezt/tests/evm_rollup.ml b/tezt/tests/evm_rollup.ml index 591ac99ac5e1b6d3d99e93cc1c2774fc45f4de16..a18767b8786de585e9c5eebd756e2d07ef4cfb1c 100644 --- a/tezt/tests/evm_rollup.ml +++ b/tezt/tests/evm_rollup.ml @@ -47,6 +47,7 @@ type full_evm_setup = { originator_key : string; rollup_operator_key : string; evm_proxy_server : Evm_proxy_server.t; + endpoint : string; } let hex_256_of n = Printf.sprintf "%064x" n @@ -213,7 +214,9 @@ let setup_evm_kernel ?(originator_key = Constant.bootstrap1.public_key_hash) ~src:originator_key client in - let* () = Sc_rollup_node.run sc_rollup_node sc_rollup_address [] in + let* () = + Sc_rollup_node.run sc_rollup_node sc_rollup_address ["--log-kernel-debug"] + in let sc_rollup_client = Sc_rollup_client.create ~protocol sc_rollup_node in (* EVM Kernel installation level. *) let* () = Client.bake_for_and_wait client in @@ -224,6 +227,7 @@ let setup_evm_kernel ?(originator_key = Constant.bootstrap1.public_key_hash) (Node.get_level node) in let* evm_proxy_server = Evm_proxy_server.init sc_rollup_node in + let endpoint = Evm_proxy_server.endpoint evm_proxy_server in return { node; @@ -234,6 +238,7 @@ let setup_evm_kernel ?(originator_key = Constant.bootstrap1.public_key_hash) originator_key; rollup_operator_key; evm_proxy_server; + endpoint; } let setup_past_genesis ?originator_key ?rollup_operator_key protocol = @@ -596,6 +601,18 @@ let test_l2_deploy_simple_storage = ~error_msg:"Expected %L account to be initialized by contract creation.") ; unit +let send_call_set_storage_simple contract_address sender n + {sc_rollup_node; node; client; endpoint; _} = + let call_set (sender : Eth_account.t) n = + Eth_cli.contract_send + ~source_private_key:sender.private_key + ~endpoint + ~abi_label:simple_storage.label + ~address:contract_address + ~method_call:(Printf.sprintf "set(%d)" n) + in + wait_for_application ~sc_rollup_node ~node ~client (call_set sender n) () + (** Test that a contract can be called, and that the call can modify the storage. *) let test_l2_call_simple_storage = @@ -605,8 +622,7 @@ let test_l2_call_simple_storage = ~title:"Check L2 contract call" @@ fun protocol -> (* setup *) - let* ({sc_rollup_node; node; client; evm_proxy_server; sc_rollup_client; _} as - evm_setup) = + let* ({evm_proxy_server; sc_rollup_client; _} as evm_setup) = setup_past_genesis protocol in let endpoint = Evm_proxy_server.endpoint evm_proxy_server in @@ -615,36 +631,23 @@ let test_l2_call_simple_storage = (* deploy contract *) let* address, _tx = deploy ~contract:simple_storage ~sender evm_setup in - (* calls the set method *) - let call_set (sender : Eth_account.t) n = - Eth_cli.call - ~source_private_key:sender.private_key - ~endpoint - ~abi_label:simple_storage.label - ~address - ~method_call:(Printf.sprintf "set(%d)" n) - in - (* set 42 *) - let* tx = - wait_for_application ~sc_rollup_node ~node ~client (call_set sender 42) () - in - let* () = check_tx_succeeded ~endpoint ~tx in + let* tx = send_call_set_storage_simple address sender 42 evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in let* () = check_storage_size sc_rollup_client ~address 1 in let* () = check_nb_in_storage ~evm_setup ~address ~nth:0 ~expected:42 in (* set 24 by another user *) let* tx = - wait_for_application - ~sc_rollup_node - ~node - ~client - (call_set Eth_account.bootstrap_accounts.(1) 24) - () + send_call_set_storage_simple + address + Eth_account.bootstrap_accounts.(1) + 24 + evm_setup in - let* () = check_tx_succeeded ~endpoint ~tx in + let* () = check_tx_succeeded ~endpoint ~tx in let* () = check_storage_size sc_rollup_client ~address 1 in (* value stored has changed *) let* () = check_nb_in_storage ~evm_setup ~address ~nth:0 ~expected:24 in @@ -652,11 +655,9 @@ let test_l2_call_simple_storage = (* set -1 *) (* some environments prevent sending a negative value, as the value is unsigned (eg remix) but it is actually the expected result *) - let* tx = - wait_for_application ~sc_rollup_node ~node ~client (call_set sender (-1)) () - in - let* () = check_tx_succeeded ~endpoint ~tx in + let* tx = send_call_set_storage_simple address sender (-1) evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in let* () = check_storage_size sc_rollup_client ~address 1 in (* value stored has changed *) let* () = @@ -713,7 +714,7 @@ let test_l2_deploy_erc20 = (* minting / burning *) let call_mint (sender : Eth_account.t) n = - Eth_cli.call + Eth_cli.contract_send ~source_private_key:sender.private_key ~endpoint ~abi_label:erc20.label @@ -721,7 +722,7 @@ let test_l2_deploy_erc20 = ~method_call:(Printf.sprintf "mint(%d)" n) in let call_burn ?(expect_failure = false) (sender : Eth_account.t) n = - Eth_cli.call + Eth_cli.contract_send ~expect_failure ~source_private_key:sender.private_key ~endpoint @@ -1064,6 +1065,211 @@ let test_inject_100_transactions = hasn't changed" ; unit +let test_eth_call_large = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "eth_call"; "simulate"; "large"; "try"] + ~title:"Try to call with a large amount of data" + (fun protocol -> + (* setup *) + let* {evm_proxy_server; _} = setup_past_genesis protocol in + let sender = Eth_account.bootstrap_accounts.(0) in + + (* large request *) + let eth_call = + [ + ("to", Ezjsonm.encode_string sender.address); + ("data", Ezjsonm.encode_string ("0x" ^ String.make 12_000 'a')); + ("gas", `String "111111"); + ] + in + + (* make call to proxy *) + let* call_result = + Evm_proxy_server.( + call_evm_rpc + evm_proxy_server + { + method_ = "eth_call"; + parameters = `A [`O eth_call; `String "latest"]; + }) + in + + (* Check the RPC returns a `result`. *) + let r = call_result |> Evm_proxy_server.extract_result in + Check.((JSON.as_string r = "0x") string) + ~error_msg:"Expected result %R, but got %L" ; + + unit) + +let test_eth_call_storage_contract_rollup_node = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "eth_call"; "simulate"] + ~title:"Try to call a view (directly through proxy)" + (fun protocol -> + (* setup *) + let* ({evm_proxy_server; endpoint; _} as evm_setup) = + setup_past_genesis protocol + in + + let sender = Eth_account.bootstrap_accounts.(0) in + + (* deploy contract *) + let* address, tx = deploy ~contract:simple_storage ~sender evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in + Check.( + (String.lowercase_ascii address + = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344") + string + ~error_msg:"Expected address to be %R but was %L.") ; + + (* craft request *) + let data = "0x4e70b1dc" in + let eth_call = + [ + ("to", Ezjsonm.encode_string address); + ("data", Ezjsonm.encode_string data); + ("gas", `String "111111"); + ] + in + + (* make call to proxy *) + let* call_result = + Evm_proxy_server.( + call_evm_rpc + evm_proxy_server + { + method_ = "eth_call"; + parameters = `A [`O eth_call; `String "latest"]; + }) + in + + let r = call_result |> Evm_proxy_server.extract_result in + Check.( + (JSON.as_string r + = "0x0000000000000000000000000000000000000000000000000000000000000000") + string) + ~error_msg:"Expected result %R, but got %L" ; + + let* tx = send_call_set_storage_simple address sender 42 evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in + + (* make call to proxy *) + let* call_result = + Evm_proxy_server.( + call_evm_rpc + evm_proxy_server + { + method_ = "eth_call"; + parameters = `A [`O eth_call; `String "latest"]; + }) + in + let r = call_result |> Evm_proxy_server.extract_result in + Check.( + (JSON.as_string r + = "0x000000000000000000000000000000000000000000000000000000000000002a") + string) + ~error_msg:"Expected result %R, but got %L" ; + unit) + +let test_eth_call_storage_contract_proxy = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "simulate"] + ~title:"Try to call a view (directly through rollup node)" + (fun protocol -> + let* ({sc_rollup_client; evm_proxy_server; _} as evm_setup) = + setup_past_genesis protocol + in + + let endpoint = Evm_proxy_server.endpoint evm_proxy_server in + let sender = Eth_account.bootstrap_accounts.(0) in + + (* deploy contract *) + let* address, tx = deploy ~contract:simple_storage ~sender evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in + + Check.( + (String.lowercase_ascii address + = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344") + string + ~error_msg:"Expected address to be %R but was %L.") ; + + let*! simulation_result = + Sc_rollup_client.simulate + ~insight_requests: + [ + `Durable_storage_key ["evm"; "simulation_result"]; + `Durable_storage_key ["evm"; "simulation_status"]; + ] + sc_rollup_client + [ + Hex.to_string @@ `Hex "ff"; + Hex.to_string + @@ `Hex + "ff01e68094d77420f73b4612a7a99dba8c2afd30a1886b03448857040000000000008080844e70b1dc"; + ] + in + let expected_insights = + [ + Some "0000000000000000000000000000000000000000000000000000000000000000"; + Some "01"; + ] + in + Check.( + (simulation_result.insights = expected_insights) (list @@ option string)) + ~error_msg:"Expected result %R, but got %L" ; + unit) + +let test_eth_call_storage_contract_eth_cli = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "eth_call"; "simulate"] + ~title:"Try to call a view through an ethereum client" + (fun protocol -> + (* setup *) + let* ({evm_proxy_server; endpoint; sc_rollup_node; client; node; _} as + evm_setup) = + setup_past_genesis protocol + in + + (* sanity *) + let* call_result = + Evm_proxy_server.( + call_evm_rpc + evm_proxy_server + { + method_ = "eth_call"; + parameters = `A [`O [("to", `Null)]; `String "latest"]; + }) + in + (* Check the RPC returns a `result`. *) + let _result = call_result |> Evm_proxy_server.extract_result in + + let sender = Eth_account.bootstrap_accounts.(0) in + + (* deploy contract send send 42 *) + let* address, _tx = deploy ~contract:simple_storage ~sender evm_setup in + let* tx = send_call_set_storage_simple address sender 42 evm_setup in + let* () = check_tx_succeeded ~endpoint ~tx in + + (* make a call to proxy through eth-cli *) + let call_num = + Eth_cli.contract_call + ~endpoint + ~abi_label:simple_storage.label + ~address + ~method_call:"num()" + in + let* res = + wait_for_application ~sc_rollup_node ~node ~client call_num () + in + + Check.((String.trim res = "42") string) + ~error_msg:"Expected result %R, but got %L" ; + unit) + let register_evm_proxy_server ~protocols = test_originate_evm_kernel protocols ; test_evm_proxy_server_connection protocols ; @@ -1084,6 +1290,10 @@ let register_evm_proxy_server ~protocols = test_l2_deploy_simple_storage protocols ; test_l2_call_simple_storage protocols ; test_l2_deploy_erc20 protocols ; - test_inject_100_transactions protocols + test_inject_100_transactions protocols ; + test_eth_call_storage_contract_rollup_node protocols ; + test_eth_call_storage_contract_proxy protocols ; + test_eth_call_storage_contract_eth_cli protocols ; + test_eth_call_large protocols let register ~protocols = register_evm_proxy_server ~protocols