diff --git a/src/bin_evm_proxy/lib/next_rollup_node.ml b/src/bin_evm_proxy/lib/next_rollup_node.ml index 8e7ca23fe8913779b7329de8e4f9250ef48f8cc1..11f80f28baeaf84a712bc6590d66fef3024f8f12 100644 --- a/src/bin_evm_proxy/lib/next_rollup_node.ml +++ b/src/bin_evm_proxy/lib/next_rollup_node.ml @@ -4,6 +4,7 @@ (* Copyright (c) 2023 Nomadic Labs *) (* Copyright (c) 2023 Marigold *) (* Copyright (c) 2023 Functori *) +(* Copyright (c) 2023 Trilitech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -25,4 +26,576 @@ (* *) (*****************************************************************************) -include Current_rollup_node +open Ethereum_types + +(* The hard limit is 4096 but it needs to add the external message tag. *) +let max_input_size = 4095 + +let smart_rollup_address_size = 20 + +type transaction = + | Simple of string + | NewChunked of (string * int) + | Chunk of string + +let encode_transaction ~smart_rollup_address kind = + let data = + match kind with + | Simple data -> "\000" ^ data + | NewChunked (hash, len) -> + let number_of_chunks_bytes = Ethereum_types.u16_to_bytes len in + "\001" ^ hash ^ number_of_chunks_bytes + | Chunk data -> "\002" ^ data + in + "\000" ^ smart_rollup_address ^ data + +let make_evm_inbox_transactions tx_raw = + let open Result_syntax in + (* Maximum size describes the maximum size of [tx_raw] to fit + in a simple transaction. *) + let transaction_tag_size = 1 in + let framing_protocol_tag_size = 1 in + let maximum_size = + max_input_size - framing_protocol_tag_size - smart_rollup_address_size + - transaction_tag_size - Ethereum_types.transaction_hash_size + in + let tx_hash = Ethereum_types.hash_raw_tx tx_raw in + if String.length tx_raw <= maximum_size then + (* Simple transaction, fits in a single input. *) + let tx_hash = Ethereum_types.hash_raw_tx tx_raw in + let tx = Simple (tx_hash ^ tx_raw) in + return (tx_hash, [tx]) + else + let size_per_chunk = + max_input_size - framing_protocol_tag_size - smart_rollup_address_size + - transaction_tag_size - 2 (* Index as u16 *) + - Ethereum_types.transaction_hash_size + in + 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 ^ Ethereum_types.u16_to_bytes i ^ chunk)) + chunks + in + return (tx_hash, new_chunk_transaction :: chunks) + +let make_encoded_messages ~smart_rollup_address tx_raw = + let open Result_syntax in + let tx_raw = Ethereum_types.hash_to_bytes tx_raw in + let* tx_hash, messages = make_evm_inbox_transactions tx_raw in + let messages = + List.map + (fun x -> + x + |> encode_transaction ~smart_rollup_address + |> Hex.of_string |> Hex.show) + messages + in + return (tx_hash, messages) + +(** [chunks bytes size] returns [Bytes.length bytes / size] chunks of size + [size]. *) +let chunks bytes size = + let n = Bytes.length bytes in + assert (n mod size = 0) ; + let nb = n / size in + let rec collect i acc = + if i = nb then acc + else + let chunk = Bytes.sub_string bytes (i * size) size in + collect (i + 1) (chunk :: acc) + in + collect 0 [] |> List.rev + +module Durable_storage_path = struct + module EVM = struct + let root = "/evm" + + let make s = root ^ s + end + + let chain_id = EVM.make "/chain_id" + + let kernel_version = EVM.make "/kernel_version" + + module Accounts = struct + let accounts = EVM.make "/eth_accounts" + + let balance = "/balance" + + let nonce = "/nonce" + + let code = "/code" + + let account (Address s) = accounts ^ "/" ^ s + + let balance address = account address ^ balance + + let nonce address = account address ^ nonce + + let code address = account address ^ code + end + + module Block = struct + let blocks = EVM.make "/blocks" + + let number = "/number" + + let hash = "/hash" + + let transactions = "/transactions" + + let timestamp = "/timestamp" + + type number = Current | Nth of Z.t + + let number_to_string = function + | Current -> "current" + | Nth i -> Z.to_string i + + let block_field block_number field = + blocks ^ "/" ^ number_to_string block_number ^ field + + let hash block_number = block_field block_number hash + + let transactions block_number = block_field block_number transactions + + let timestamp block_number = block_field block_number timestamp + + let current_number = blocks ^ "/current" ^ number + end + + module Transaction_receipt = struct + let receipts = EVM.make "/transactions_receipts" + + let receipt tx_hash = receipts ^ "/" ^ tx_hash + end + + module Transaction_object = struct + let objects = EVM.make "/transactions_objects" + + let object_ tx_hash = objects ^ "/" ^ tx_hash + end +end + +module RPC = struct + open Tezos_rpc + open Path + + let smart_rollup_address = + Service.get_service + ~description:"Smart rollup address" + ~query:Query.empty + ~output:(Data_encoding.Fixed.bytes 20) + (open_root / "global" / "smart_rollup_address") + + type state_value_query = {key : string} + + let state_value_query : state_value_query Tezos_rpc.Query.t = + let open Tezos_rpc.Query in + query (fun key -> {key}) + |+ field "key" Tezos_rpc.Arg.string "" (fun t -> t.key) + |> seal + + let durable_state_value = + Tezos_rpc.Service.get_service + ~description: + "Retrieve value by key from PVM durable storage. PVM state is taken \ + with respect to the specified block level. Value returned in hex \ + format." + ~query:state_value_query + ~output:Data_encoding.(option bytes) + (open_root / "global" / "block" / "head" / "durable" / "wasm_2_0_0" + / "value") + + let batcher_injection = + Tezos_rpc.Service.post_service + ~description:"Inject messages in the batcher's queue" + ~query:Tezos_rpc.Query.empty + ~input: + Data_encoding.( + def "messages" ~description:"Messages to inject" (list string)) + ~output: + Data_encoding.( + def + "message_hashes" + ~description:"Hashes of injected L2 messages" + (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 + + let inspect_durable_and_decode_opt base key decode = + let open Lwt_result_syntax in + let* bytes = call_service ~base durable_state_value () {key} () in + match bytes with + | Some bytes -> return_some (decode bytes) + | None -> return_none + + let inspect_durable_and_decode base key decode = + let open Lwt_result_syntax in + let* res_opt = inspect_durable_and_decode_opt base key decode in + match res_opt with Some res -> return res | None -> failwith "null" + + let smart_rollup_address base = + let open Lwt_result_syntax in + let*! answer = + call_service + ~base + ~media_types:[Media_type.octet_stream] + smart_rollup_address + () + () + () + in + match answer with + | Ok address -> return (Bytes.to_string address) + | Error tztrace -> + failwith + "Failed to communicate with %a, because %a" + Uri.pp + base + pp_print_trace + tztrace + + let balance base address = + let open Lwt_result_syntax in + let key = Durable_storage_path.Accounts.balance address in + let+ answer = call_service ~base durable_state_value () {key} () in + match answer with + | Some bytes -> + Bytes.to_string bytes |> Z.of_bits |> Ethereum_types.quantity_of_z + | None -> Ethereum_types.Qty Z.zero + + let nonce base address = + let open Lwt_result_syntax in + let key = Durable_storage_path.Accounts.nonce address in + let+ answer = call_service ~base durable_state_value () {key} () in + match answer with + | Some bytes -> + Bytes.to_string bytes |> Z.of_bits |> Ethereum_types.quantity_of_z + | None -> Ethereum_types.Qty Z.zero + + let code base address = + let open Lwt_result_syntax in + let key = Durable_storage_path.Accounts.code address in + let+ answer = call_service ~base durable_state_value () {key} () in + match answer with + | Some bytes -> + bytes |> Hex.of_bytes |> Hex.show |> Ethereum_types.hash_of_string + | None -> Ethereum_types.Hash "" + + let inject_raw_transaction base tx = + let open Lwt_result_syntax in + (* The injection's service returns a notion of L2 message hash (defined + by the rollup node) used to track the message's injection in the batcher. + We do not wish to follow the message's inclusion, and thus, ignore + the resulted hash. *) + let* _answer = call_service ~base batcher_injection () () [tx] in + return_unit + + let inject_raw_transaction base ~smart_rollup_address tx_raw = + let open Lwt_result_syntax in + let*? tx_hash, messages = + make_encoded_messages ~smart_rollup_address tx_raw + in + let* () = List.iter_es (inject_raw_transaction base) messages in + return (Ethereum_types.Hash Hex.(of_string tx_hash |> show)) + + exception Invalid_block_structure of string + + let block_number base n = + let open Lwt_result_syntax in + match n with + (* This avoids an unecessary service call in case we ask a block's number + with an already expected/known block number [n]. *) + | Durable_storage_path.Block.Nth i -> + return @@ Ethereum_types.Block_height i + | Durable_storage_path.Block.Current -> ( + let key = Durable_storage_path.Block.current_number in + let+ answer = call_service ~base durable_state_value () {key} () in + match answer with + | Some bytes -> + Ethereum_types.Block_height (Bytes.to_string bytes |> Z.of_bits) + | None -> + raise + @@ Invalid_block_structure + "Unexpected [None] value for [current_number]'s [answer]") + + let block_hash base number = + let open Lwt_result_syntax in + let* (Block_height block_number) = + match number with + | Durable_storage_path.Block.Current -> block_number base number + | Nth n -> return (Block_height n) + in + let key = Durable_storage_path.Block.hash (Nth block_number) in + let+ hash_answer = call_service ~base durable_state_value () {key} () in + match hash_answer with + | Some bytes -> + Block_hash (Bytes.to_string bytes |> Hex.of_string |> Hex.show) + | None -> + raise + @@ Invalid_block_structure "Unexpected [None] value for [block.hash]" + + let block_timestamp base number = + let open Lwt_result_syntax in + let* (Block_height block_number) = + match number with + | Durable_storage_path.Block.Current -> block_number base number + | Nth n -> return (Block_height n) + in + let key = Durable_storage_path.Block.timestamp (Nth block_number) in + inspect_durable_and_decode base key decode_number + + let current_block_number base () = + block_number base Durable_storage_path.Block.Current + + let transaction_receipt base (Hash tx_hash) = + let open Lwt_result_syntax in + let+ bytes = + inspect_durable_and_decode_opt + base + (Durable_storage_path.Transaction_receipt.receipt tx_hash) + Fun.id + in + match bytes with + | Some bytes -> + Some + (Ethereum_types.transaction_receipt_from_rlp (Bytes.to_string bytes)) + | None -> None + + let transaction_object base (Hash tx_hash) = + let open Lwt_result_syntax in + let+ bytes = + inspect_durable_and_decode_opt + base + (Durable_storage_path.Transaction_object.object_ tx_hash) + Fun.id + in + match bytes with + | Some bytes -> + Some + (Ethereum_types.transaction_object_from_rlp (Bytes.to_string bytes)) + | None -> None + + let transactions ~full_transaction_object ~number base = + let open Lwt_result_syntax in + let* (Block_height block_number) = block_number base number in + let key_transactions = + Durable_storage_path.Block.transactions (Nth block_number) + in + let* transactions_answer = + call_service ~base durable_state_value () {key = key_transactions} () + in + match transactions_answer with + | Some bytes -> + let chunks = chunks bytes Ethereum_types.transaction_hash_size in + if full_transaction_object then + let+ objects = + List.filter_map_es + (fun bytes -> + transaction_object base (Hash Hex.(of_string bytes |> show))) + chunks + in + TxFull objects + else + let hashes = + List.map (fun bytes -> Hash Hex.(of_string bytes |> show)) chunks + in + return (TxHash hashes) + | None -> + raise + @@ Invalid_block_structure + "Unexpected [None] value for [block.transactions]" + + let block ~full_transaction_object ~number base = + let open Lwt_result_syntax in + let* transactions = transactions ~full_transaction_object ~number base in + let* (Ethereum_types.Block_height level_z as level) = + block_number base number + in + let* hash = block_hash base number in + let* timestamp = block_timestamp base number in + let* parent = + if Z.zero = level_z then + return (Ethereum_types.Block_hash (String.make 64 'a')) + else block_hash base (Nth (Z.pred level_z)) + in + return + { + number = Some level; + hash = Some hash; + parent; + nonce = Ethereum_types.Hash (String.make 16 'a'); + sha3Uncles = Ethereum_types.Hash (String.make 64 'a'); + logsBloom = Some (Ethereum_types.Hash (String.make 512 'a')); + transactionRoot = Ethereum_types.Hash (String.make 64 'a'); + stateRoot = Ethereum_types.Hash (String.make 64 'a'); + receiptRoot = Ethereum_types.Hash (String.make 64 'a'); + (* We need the following dummy value otherwise eth-cli will complain + that miner's address is not a valid Ethereum address. *) + miner = Ethereum_types.Hash "6471A723296395CF1Dcc568941AFFd7A390f94CE"; + difficulty = Ethereum_types.Qty Z.zero; + totalDifficulty = Ethereum_types.Qty Z.zero; + extraData = ""; + size = Ethereum_types.Qty Z.zero; + gasLimit = Ethereum_types.Qty Z.zero; + gasUsed = Ethereum_types.Qty Z.zero; + timestamp; + transactions; + uncles = []; + } + + let current_block base ~full_transaction_object = + block + ~full_transaction_object + ~number:Durable_storage_path.Block.Current + base + + let nth_block base ~full_transaction_object n = + block + ~full_transaction_object + ~number:Durable_storage_path.Block.(Nth n) + base + + let txpool _ () = + Lwt.return_ok {pending = AddressMap.empty; queued = AddressMap.empty} + + let chain_id base () = + inspect_durable_and_decode base Durable_storage_path.chain_id decode_number + + let kernel_version base () = + inspect_durable_and_decode + base + Durable_storage_path.kernel_version + Bytes.to_string + + 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.call_result r + + let estimate_gas base call = + let open Lwt_result_syntax in + let*? messages = Simulation.encode call in + let insight_requests = + [ + Simulation.Encodings.Durable_storage_key ["evm"; "simulation_gas"]; + (* 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.gas_estimation r +end + +module type S = sig + val smart_rollup_address : string tzresult Lwt.t + + val balance : Ethereum_types.address -> Ethereum_types.quantity tzresult Lwt.t + + val nonce : Ethereum_types.address -> Ethereum_types.quantity tzresult Lwt.t + + val code : Ethereum_types.address -> Ethereum_types.hash tzresult Lwt.t + + val inject_raw_transaction : + smart_rollup_address:string -> hash -> hash tzresult Lwt.t + + val current_block : + full_transaction_object:bool -> Ethereum_types.block tzresult Lwt.t + + val current_block_number : unit -> Ethereum_types.block_height tzresult Lwt.t + + val nth_block : + full_transaction_object:bool -> Z.t -> Ethereum_types.block tzresult Lwt.t + + val transaction_receipt : + Ethereum_types.hash -> + Ethereum_types.transaction_receipt option tzresult Lwt.t + + val transaction_object : + Ethereum_types.hash -> + Ethereum_types.transaction_object option tzresult Lwt.t + + val txpool : unit -> Ethereum_types.txpool tzresult Lwt.t + + val chain_id : unit -> Ethereum_types.quantity tzresult Lwt.t + + val kernel_version : unit -> string tzresult Lwt.t + + val simulate_call : Ethereum_types.call -> Ethereum_types.hash tzresult Lwt.t + + val estimate_gas : + Ethereum_types.call -> Ethereum_types.quantity tzresult Lwt.t +end + +module Make (Base : sig + val base : Uri.t +end) : S = struct + let smart_rollup_address = RPC.smart_rollup_address Base.base + + let balance = RPC.balance Base.base + + let nonce = RPC.nonce Base.base + + let code = RPC.code Base.base + + let inject_raw_transaction = RPC.inject_raw_transaction Base.base + + let current_block = RPC.current_block Base.base + + let current_block_number = RPC.current_block_number Base.base + + let nth_block = RPC.nth_block Base.base + + let transaction_receipt = RPC.transaction_receipt Base.base + + let transaction_object = RPC.transaction_object Base.base + + let txpool = RPC.txpool Base.base + + let chain_id = RPC.chain_id Base.base + + let kernel_version = RPC.kernel_version Base.base + + let simulate_call = RPC.simulate_call Base.base + + let estimate_gas = RPC.estimate_gas Base.base +end diff --git a/src/kernel_evm/CHANGES.md b/src/kernel_evm/CHANGES.md index e37ef177af8bcfa9bd64a873d35946cbd74774f7..ab9919405a95672970124bb57bccd2e0be59dc3f 100644 --- a/src/kernel_evm/CHANGES.md +++ b/src/kernel_evm/CHANGES.md @@ -5,13 +5,18 @@ ### EVM Kernel - Fallback mechanism if stage zero fails. (!9732) +- Switch to `ExternalMessageFrame` protocol for external messages. (!9687) ### EVM Node +- Switch to `ExternalMessageFrame` protocol for external messages. (!9687) + ### Bug fixes ### Breaking changes +- External Messages must now be framed using `ExternalMessageFrame` (adds an additional prefix byte). (!9687) + ### Internal - The kernel reboots before reaching maximum number of ticks (!9369) diff --git a/src/kernel_evm/kernel/src/inbox.rs b/src/kernel_evm/kernel/src/inbox.rs index acf31c5ba9ce2df082487e06abe3929b87db2d0a..13f53d538f18fefb54a215dddbc306244ee28f11 100644 --- a/src/kernel_evm/kernel/src/inbox.rs +++ b/src/kernel_evm/kernel/src/inbox.rs @@ -344,17 +344,27 @@ mod tests { use crate::inbox::TransactionContent::Ethereum; use crate::storage::*; use libsecp256k1::PublicKey; + use tezos_crypto_rs::hash::SmartRollupHash; use tezos_data_encoding::types::Bytes; use tezos_ethereum::transaction::TRANSACTION_HASH_SIZE; + use tezos_smart_rollup_encoding::inbox::ExternalMessageFrame; + use tezos_smart_rollup_encoding::smart_rollup::SmartRollupAddress; use tezos_smart_rollup_mock::MockHost; - const ZERO_SMART_ROLLUP_ADDRESS: [u8; 20] = [0; 20]; + const SMART_ROLLUP_ADDRESS: [u8; 20] = [ + 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, + ]; const ZERO_TX_HASH: TransactionHash = [0; TRANSACTION_HASH_SIZE]; + fn smart_rollup_address() -> SmartRollupAddress { + SmartRollupAddress::new(SmartRollupHash(SMART_ROLLUP_ADDRESS.into())) + } + fn input_to_bytes(smart_rollup_address: [u8; 20], input: Input) -> Vec { let mut buffer = Vec::new(); - // Smart rollup address. + // Targetted framing protocol + buffer.push(0); buffer.extend_from_slice(&smart_rollup_address); match input { Input::SimpleTransaction(tx) => { @@ -442,13 +452,9 @@ mod tests { content: Ethereum(tx.clone()), })); - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - input, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))); - let inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); let expected_transactions = vec![Transaction { tx_hash: ZERO_TX_HASH, content: Ethereum(tx), @@ -458,21 +464,18 @@ mod tests { #[test] fn parse_valid_chunked_transaction() { - let mut host = MockHost::default(); + let address = smart_rollup_address(); + let mut host = MockHost::with_address(&address); let (data, tx) = large_transaction(); let inputs = make_chunked_transactions(ZERO_TX_HASH, data); for input in inputs { - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - input, - ))) + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); let expected_transactions = vec![Transaction { tx_hash: ZERO_TX_HASH, content: Ethereum(tx), @@ -509,13 +512,14 @@ mod tests { }; let input = Input::Upgrade(kernel_upgrade.clone()); + let zero_smart_rollup_address = [0; 20]; host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, + zero_smart_rollup_address, input, ))); let inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + read_inbox(&mut host, zero_smart_rollup_address, None).unwrap(); let expected_upgrade = Some(kernel_upgrade); assert_eq!(inbox_content.kernel_upgrade, expected_upgrade); } @@ -537,16 +541,15 @@ mod tests { }; host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, + SMART_ROLLUP_ADDRESS, new_chunk1, ))); host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, + SMART_ROLLUP_ADDRESS, new_chunk2, ))); - let _inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let _inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); let num_chunks = chunked_transaction_num_chunks(&mut host, &tx_hash) .expect("The number of chunks should exist"); @@ -568,10 +571,7 @@ mod tests { let chunk = inputs.remove(0); // Announce a chunked transaction. - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - new_chunk, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, new_chunk))); // Give a chunk with an invalid `i`. let out_of_bound_i = 42; @@ -587,13 +587,9 @@ mod tests { }, _ => panic!("Expected a transaction chunk"), }; - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - chunk, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); - let _inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let _inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); // The out of bounds chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -622,13 +618,9 @@ mod tests { _ => panic!("Expected a transaction chunk"), }; - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - chunk, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); - let _inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let _inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); // The unknown chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -671,18 +663,11 @@ mod tests { let new_chunk = inputs[0].clone(); let chunk0 = inputs[1].clone(); - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - new_chunk, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, new_chunk))); - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - chunk0, - ))); + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk0))); - let inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); assert_eq!( inbox_content, InboxContent { @@ -693,13 +678,61 @@ mod tests { // On the next level, try to re-give the chunks, but this time in full: for input in inputs { - host.add_external(Bytes::from(input_to_bytes( - ZERO_SMART_ROLLUP_ADDRESS, - input, - ))) + host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = - read_inbox(&mut host, ZERO_SMART_ROLLUP_ADDRESS, None).unwrap(); + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); + + let expected_transactions = vec![Transaction { + tx_hash: ZERO_TX_HASH, + content: Ethereum(tx), + }]; + assert_eq!(inbox_content.transactions, expected_transactions); + } + + #[test] + fn parse_valid_simple_transaction_framed() { + // Don't use zero-hash for rollup here - as the long string of zeros is still valid under the previous + // parsing. This won't happen in practice, though + let address = smart_rollup_address(); + + let mut host = MockHost::with_address(&address); + + let tx = + EthereumTransactionCommon::from_rlp_bytes(&hex::decode("f86d80843b9aca00825208940b52d4d3be5d18a7ab5\ +e4476a2f5382bbf2b38d888016345785d8a000080820a95a0d9ef1298c18c88604e3f08e14907a17dfa81b1dc6b37948abe189d8db5cb8a43a06\ +fc7040a71d71d3cb74bd05ead7046b10668ad255da60391c017eea31555f156").unwrap()).unwrap(); + + let input = Input::SimpleTransaction(Box::new(Transaction { + tx_hash: ZERO_TX_HASH, + content: Ethereum(tx.clone()), + })); + + let mut buffer = Vec::new(); + match input { + Input::SimpleTransaction(tx) => { + // Simple transaction tag + buffer.push(0); + buffer.extend_from_slice(&tx.tx_hash); + let mut tx_bytes = match tx.content { + Ethereum(tx) => tx.into(), + _ => panic!( + "Simple transaction can contain only ethereum transactions" + ), + }; + + buffer.append(&mut tx_bytes) + } + _ => unreachable!("Not tested"), + }; + + let framed = ExternalMessageFrame::Targetted { + address, + contents: buffer, + }; + + host.add_external(framed); + + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None).unwrap(); let expected_transactions = vec![Transaction { tx_hash: ZERO_TX_HASH, content: Ethereum(tx), diff --git a/src/kernel_evm/kernel/src/parsing.rs b/src/kernel_evm/kernel/src/parsing.rs index cdad82ed605f3c0b17bcba43879a97721a977a72..d9c5e4eae70c4f4adc689f94fa81fe46e68ece99 100644 --- a/src/kernel_evm/kernel/src/parsing.rs +++ b/src/kernel_evm/kernel/src/parsing.rs @@ -16,7 +16,9 @@ use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; use tezos_smart_rollup_encoding::{ contract::Contract, - inbox::{InboxMessage, InfoPerLevel, InternalInboxMessage, Transfer}, + inbox::{ + ExternalMessageFrame, InboxMessage, InfoPerLevel, InternalInboxMessage, Transfer, + }, michelson::{ticket::UnitTicket, MichelsonBytes, MichelsonInt, MichelsonPair}, }; use tezos_smart_rollup_host::input::Message; @@ -58,7 +60,8 @@ const TRANSACTION_CHUNK_TAG: u8 = 2; const KERNEL_UPGRADE_TAG: u8 = 3; pub const MAX_SIZE_PER_CHUNK: usize = 4095 // Max input size minus external tag - - 20 // Smart rollup address size + - 1 // ExternalMessageFrame tag + - 20 // Smart rollup address size (ExternalMessageFrame::Targetted) - 1 // Transaction chunk tag - 2 // Number of chunks (u16) - 32; // Transaction hash size @@ -168,18 +171,18 @@ impl InputResult { } // External message structure : - // EXTERNAL_TAG 1B / ROLLUP_ADDRESS 20B / MESSAGE_TAG 1B / DATA + // EXTERNAL_TAG 1B / FRAMING_PROTOCOL_TARGETTED 21B / MESSAGE_TAG 1B / DATA fn parse_external(input: &[u8], smart_rollup_address: &[u8]) -> Self { - // Next 20 bytes is the targeted smart rollup address. - let remaining = { - let (target_smart_rollup_address, remaining) = parsable!(split_at(input, 20)); - - if target_smart_rollup_address == smart_rollup_address { - remaining - } else { - return InputResult::Unparsable; + // Compatibility with framing protocol for external messages + let remaining = match ExternalMessageFrame::parse(input) { + Ok(ExternalMessageFrame::Targetted { address, contents }) + if address.hash().as_ref() == smart_rollup_address => + { + contents } + _ => return InputResult::Unparsable, }; + let (transaction_tag, remaining) = parsable!(remaining.split_first()); match *transaction_tag { SIMPLE_TRANSACTION_TAG => Self::parse_simple_transaction(remaining), diff --git a/tezt/tests/evm_rollup.ml b/tezt/tests/evm_rollup.ml index a37d6cc45dba3d315a894147a7120da310b1ae4c..991461434cd8db18a897ac73e7b5cb33c69c627a 100644 --- a/tezt/tests/evm_rollup.ml +++ b/tezt/tests/evm_rollup.ml @@ -1697,8 +1697,9 @@ let gen_test_kernel_upgrade ?rollup_address ?(should_fail = false) ?(nonce = 2) in let upgrade_tag_bytes = "\003" in let full_external_message = - Hex.show @@ Hex.of_string @@ rollup_address_bytes ^ upgrade_tag_bytes - ^ upgrade_nonce_bytes ^ preimage_root_hash_bytes ^ signature + Hex.show @@ Hex.of_string @@ "\000" ^ rollup_address_bytes + ^ upgrade_tag_bytes ^ upgrade_nonce_bytes ^ preimage_root_hash_bytes + ^ signature in let* kernel_boot_wasm_before_upgrade = get_kernel_boot_wasm ~sc_rollup_client