diff --git a/etherlink/kernel_evm/kernel/src/delayed_inbox.rs b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs new file mode 100644 index 0000000000000000000000000000000000000000..dbb0c8952a78957b0529f4418eae7b256cb436f9 --- /dev/null +++ b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Marigold + +use crate::{ + inbox::{Transaction, TransactionContent}, + linked_list::LinkedList, +}; +use anyhow::Result; +use rlp::{Decodable, DecoderError, Encodable}; +use tezos_ethereum::{ + transaction::TRANSACTION_HASH_SIZE, tx_common::EthereumTransactionCommon, +}; +use tezos_smart_rollup_host::{path::RefPath, runtime::Runtime}; + +pub struct DelayedInbox(LinkedList); + +pub const DELAYED_INBOX_PATH: RefPath = RefPath::assert_from(b"/delayed-inbox"); + +// Tags that indicates the delayed transaction is a eth transaction. +pub const DELAYED_TRANSACTION_TAG: u8 = 0x00; + +/// Hash of a transaction +/// +/// It represents the key of the transaction in the delayed inbox. +#[derive(Clone, Copy)] +pub struct Hash([u8; TRANSACTION_HASH_SIZE]); + +impl Encodable for Hash { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + s.append(&self.0.to_vec()); + } +} + +impl Decodable for Hash { + fn decode(decoder: &rlp::Rlp) -> Result { + let hash: Vec = decoder.as_val()?; + let hash = hash + .try_into() + .map_err(|_| DecoderError::Custom("expected a vec of 32 elements"))?; + Ok(Hash(hash)) + } +} + +impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// Delayed transaction +/// Later it might be turn into a struct +/// And fields like the timestamp might be added +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +pub enum DelayedTransaction { + Ethereum(EthereumTransactionCommon), +} + +impl Encodable for DelayedTransaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match self { + DelayedTransaction::Ethereum(delayed_tx) => { + stream.append(&DELAYED_TRANSACTION_TAG); + stream.append(&delayed_tx.to_bytes()); + } + } + } +} + +impl Decodable for DelayedTransaction { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if !decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let tag: u8 = decoder.at(0)?.as_val()?; + let payload = decoder.at(1)?; + match tag { + DELAYED_TRANSACTION_TAG => { + let payload: Vec = payload.as_val()?; + let delayed_tx = EthereumTransactionCommon::from_bytes(&payload)?; + Ok(Self::Ethereum(delayed_tx)) + } + _ => Err(DecoderError::Custom("unknown tag")), + } + } +} + +impl DelayedInbox { + pub fn new(host: &mut Host) -> Result { + let linked_list = LinkedList::new(&DELAYED_INBOX_PATH, host)?; + Ok(Self(linked_list)) + } + + pub fn save_transaction( + &mut self, + host: &mut Host, + tx: Transaction, + ) -> Result<()> { + let Transaction { tx_hash, content } = tx; + let delayed_transaction = match content { + TransactionContent::Ethereum(tx) => DelayedTransaction::Ethereum(tx), + _ => { + // not yet supported + return Ok(()); + } + }; + self.0.push(host, &Hash(tx_hash), &delayed_transaction)?; + Ok(()) + } +} diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index c3323d67e89797443369a939c8eaf01ff00caa57..c5801765030481bdc4b0fbf5a9302c21bc9c9a93 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -181,6 +181,7 @@ pub fn read_input( ticketer: &Option, admin: &Option, inbox_is_empty: &mut bool, + delayed_bridge: &Option, ) -> Result { let input = host.read_input()?; @@ -193,6 +194,7 @@ pub fn read_input( smart_rollup_address, ticketer, admin, + delayed_bridge, )) } None => Ok(InputResult::NoInput), @@ -283,6 +285,7 @@ pub fn read_inbox( smart_rollup_address: [u8; 20], ticketer: Option, admin: Option, + delayed_bridge: Option, ) -> Result, anyhow::Error> { let mut res = InboxContent { kernel_upgrade: None, @@ -301,6 +304,7 @@ pub fn read_inbox( &ticketer, &admin, &mut inbox_is_empty, + &delayed_bridge, )? { InputResult::NoInput => { if inbox_is_empty { @@ -485,7 +489,7 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))); - let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None) + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None) .unwrap() .unwrap(); let expected_transactions = vec![Transaction { @@ -509,7 +513,7 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None) + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None) .unwrap() .unwrap(); let expected_transactions = vec![Transaction { @@ -549,7 +553,7 @@ mod tests { let transfer_metadata = TransferMetadata::new(sender.clone(), source); host.add_transfer(payload, &transfer_metadata); - let inbox_content = read_inbox(&mut host, [0; 20], None, Some(sender)) + let inbox_content = read_inbox(&mut host, [0; 20], None, Some(sender), None) .unwrap() .unwrap(); let expected_upgrade = Some(KernelUpgrade { preimage_hash }); @@ -585,7 +589,7 @@ mod tests { ))); let _inbox_content = - read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None).unwrap(); + read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None).unwrap(); let num_chunks = chunked_transaction_num_chunks(&mut host, &tx_hash) .expect("The number of chunks should exist"); @@ -628,7 +632,7 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); let _inbox_content = - read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None).unwrap(); + read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None).unwrap(); // The out of bounds chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -660,7 +664,7 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); let _inbox_content = - read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None).unwrap(); + read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None).unwrap(); // The unknown chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -707,7 +711,7 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk0))); - let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None) + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None) .unwrap() .unwrap(); assert_eq!( @@ -723,7 +727,7 @@ mod tests { for input in inputs { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None) + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None) .unwrap() .unwrap(); @@ -778,7 +782,7 @@ mod tests { host.add_external(framed); - let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None) + let inbox_content = read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None) .unwrap() .unwrap(); let expected_transactions = vec![Transaction { @@ -797,12 +801,12 @@ mod tests { // in the inbox, we mock it by adding a single input. host.add_external(Bytes::from(vec![])); let inbox_content = - read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None).unwrap(); + read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None).unwrap(); assert!(inbox_content.is_some()); // Reading again the inbox returns no inbox content at all. let inbox_content = - read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None).unwrap(); + read_inbox(&mut host, SMART_ROLLUP_ADDRESS, None, None, None).unwrap(); assert!(inbox_content.is_none()); } } diff --git a/etherlink/kernel_evm/kernel/src/lib.rs b/etherlink/kernel_evm/kernel/src/lib.rs index b1602f469e61d594c1b67387290ad36ef4e9a6b2..e068501d3e33d6767ba7a8f4ccbe0c653142872f 100644 --- a/etherlink/kernel_evm/kernel/src/lib.rs +++ b/etherlink/kernel_evm/kernel/src/lib.rs @@ -10,17 +10,19 @@ use crate::error::UpgradeProcessError::Fallback; use crate::inbox::KernelUpgrade; use crate::migration::storage_migration; use crate::safe_storage::{InternalStorage, KernelRuntime, SafeStorage, TMP_PATH}; -use crate::stage_one::fetch; +use crate::stage_one::{fetch, Configuration}; use crate::storage::{read_smart_rollup_address, store_smart_rollup_address}; use crate::upgrade::upgrade_kernel; use crate::Error::UpgradeError; use anyhow::Context; use block::ComputationResult; +use delayed_inbox::DelayedInbox; use evm_execution::Config; use migration::MigrationStatus; use primitive_types::U256; use storage::{ - is_sequencer, read_admin, read_base_fee_per_gas, read_chain_id, read_kernel_version, + is_sequencer, read_admin, read_base_fee_per_gas, read_chain_id, + read_delayed_transaction_bridge, read_kernel_version, read_last_info_per_level_timestamp, read_last_info_per_level_timestamp_stats, read_ticketer, store_base_fee_per_gas, store_chain_id, store_kernel_version, store_storage_version, STORAGE_VERSION, STORAGE_VERSION_PATH, @@ -37,6 +39,7 @@ mod block; mod block_in_progress; mod blueprint; mod blueprint_storage; +mod delayed_inbox; mod error; mod inbox; mod indexable_storage; @@ -91,7 +94,7 @@ pub fn stage_one( smart_rollup_address: [u8; 20], ticketer: Option, admin: Option, - is_sequencer: bool, + configuration: &mut Configuration, ) -> Result<(), anyhow::Error> { log!(host, Info, "Entering stage one."); log!( @@ -101,7 +104,8 @@ pub fn stage_one( ticketer, admin ); - fetch(host, smart_rollup_address, ticketer, admin, is_sequencer) + + fetch(host, smart_rollup_address, ticketer, admin, configuration) } fn produce_and_upgrade( @@ -206,6 +210,26 @@ fn retrieve_base_fee_per_gas(host: &mut Host) -> Result(host: &mut Host) -> anyhow::Result { + let is_sequencer = is_sequencer(host)?; + if is_sequencer { + let delayed_bridge = read_delayed_transaction_bridge(host) + // The sequencer must declare a delayed transaction bridge. This + // default value is only to facilitate the testing. + .unwrap_or_else(|| { + ContractKt1Hash::from_base58_check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap() + }); + let delayed_inbox = Box::new(DelayedInbox::new(host)?); + Ok(Configuration::Sequencer { + delayed_bridge, + delayed_inbox, + }) + } else { + Ok(Configuration::Proxy) + } +} + pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { let chain_id = retrieve_chain_id(host).context("Failed to retrieve chain id")?; @@ -239,15 +263,21 @@ pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { .context("Failed to retrieve smart rollup address")?; let ticketer = read_ticketer(host); let admin = read_admin(host); - let is_sequencer = is_sequencer(host)?; + let mut configuration = fetch_configuration(host)?; let base_fee_per_gas = retrieve_base_fee_per_gas(host)?; // Run the stage one, this is a no-op if the inbox was already consumed // by another kernel run. This ensures that if the migration does not // consume all reboots. At least one reboot will be used to consume the // inbox. - stage_one(host, smart_rollup_address, ticketer, admin, is_sequencer) - .context("Failed during stage 1")?; + stage_one( + host, + smart_rollup_address, + ticketer, + admin, + &mut configuration, + ) + .context("Failed during stage 1")?; // Start processing blueprints stage_two(host, chain_id, base_fee_per_gas).context("Failed during stage 2") diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index 0e32932f572b9a6b80a152a652f69f33e4e56f8b..88beeee96ade64aa3e4d454d029da0002af4c5cc 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -207,8 +207,31 @@ impl InputResult { InputResult::Input(Input::SequencerBlueprint(seq_blueprint)) } - // External message structure : - // EXTERNAL_TAG 1B / FRAMING_PROTOCOL_TARGETTED 21B / MESSAGE_TAG 1B / DATA + /// Parses transactions that come from the delayed inbox. + fn parse_transaction_from_delayed_inbox( + source: ContractKt1Hash, + delayed_bridge: &Option, + bytes: &[u8], + ) -> Self { + match delayed_bridge { + Some(delayed_bridge) if delayed_bridge.as_ref() == source.as_ref() => (), + _ => { + return InputResult::Unparsable; + } + }; + let tx = parsable!(EthereumTransactionCommon::from_bytes(bytes).ok()); + let tx_hash: TransactionHash = Keccak256::digest(bytes).into(); + + Self::Input(Input::SimpleTransaction(Box::new(Transaction { + tx_hash, + content: TransactionContent::Ethereum(tx), + }))) + } + + /// Parses an external message + /// + /// External message structure : + /// EXTERNAL_TAG 1B / FRAMING_PROTOCOL_TARGETTED 21B / MESSAGE_TAG 1B / DATA fn parse_external(input: &[u8], smart_rollup_address: &[u8]) -> Self { // Compatibility with framing protocol for external messages let remaining = match ExternalMessageFrame::parse(input) { @@ -284,6 +307,7 @@ impl InputResult { smart_rollup_address: &[u8], ticketer: &Option, admin: &Option, + delayed_bridge: &Option, ) -> Self { if transfer.destination.hash().0 != smart_rollup_address { log!( @@ -301,7 +325,13 @@ impl InputResult { MichelsonOr::Left(MichelsonPair(receiver, ticket)) => { Self::parse_deposit(host, ticket, receiver, ticketer) } - MichelsonOr::Right(_extra) => Self::Unparsable, + MichelsonOr::Right(MichelsonBytes(bytes)) => { + Self::parse_transaction_from_delayed_inbox( + source, + delayed_bridge, + &bytes, + ) + } }, MichelsonOr::Right(MichelsonBytes(upgrade)) => { Self::parse_kernel_upgrade(source, admin, &upgrade) @@ -315,6 +345,7 @@ impl InputResult { smart_rollup_address: &[u8], ticketer: &Option, admin: &Option, + delayed_bridge: &Option, ) -> Self { match message { InternalInboxMessage::InfoPerLevel(info) => { @@ -326,6 +357,7 @@ impl InputResult { smart_rollup_address, ticketer, admin, + delayed_bridge, ), _ => InputResult::Unparsable, } @@ -337,6 +369,7 @@ impl InputResult { smart_rollup_address: [u8; 20], ticketer: &Option, admin: &Option, + delayed_bridge: &Option, ) -> Self { let bytes = Message::as_ref(&input); let (input_tag, remaining) = parsable!(bytes.split_first()); @@ -355,6 +388,7 @@ impl InputResult { &smart_rollup_address, ticketer, admin, + delayed_bridge, ), }, Err(_) => InputResult::Unparsable, @@ -381,6 +415,7 @@ mod tests { message, ZERO_SMART_ROLLUP_ADDRESS, &None, + &None, &None ), InputResult::Unparsable diff --git a/etherlink/kernel_evm/kernel/src/stage_one.rs b/etherlink/kernel_evm/kernel/src/stage_one.rs index 07eb3a0d338f8c699b75336b1764a5201ac44ff4..bc5206e2f480f938c8e70b57d10205c91def181e 100644 --- a/etherlink/kernel_evm/kernel/src/stage_one.rs +++ b/etherlink/kernel_evm/kernel/src/stage_one.rs @@ -5,6 +5,7 @@ use crate::blueprint::Blueprint; use crate::blueprint_storage::{store_inbox_blueprint, store_sequencer_blueprint}; use crate::current_timestamp; +use crate::delayed_inbox::DelayedInbox; use crate::inbox::read_inbox; use crate::inbox::InboxContent; use crate::storage::store_kernel_upgrade; @@ -15,6 +16,14 @@ use tezos_smart_rollup_host::metadata::RAW_ROLLUP_ADDRESS_SIZE; use tezos_smart_rollup_host::runtime::Runtime; +pub enum Configuration { + Proxy, + Sequencer { + delayed_bridge: ContractKt1Hash, + delayed_inbox: Box, + }, +} + pub fn fetch_inbox_blueprints( host: &mut Host, smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], @@ -25,7 +34,7 @@ pub fn fetch_inbox_blueprints( kernel_upgrade, transactions, sequencer_blueprints: _, - }) = read_inbox(host, smart_rollup_address, ticketer, admin)? + }) = read_inbox(host, smart_rollup_address, ticketer, admin, None)? { let timestamp = current_timestamp(host); let blueprint = Blueprint { @@ -47,14 +56,25 @@ fn fetch_sequencer_blueprints( smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], ticketer: Option, admin: Option, + delayed_bridge: ContractKt1Hash, + delayed_inbox: &mut DelayedInbox, ) -> Result<(), anyhow::Error> { if let Some(InboxContent { kernel_upgrade, - transactions: _, + transactions, sequencer_blueprints, - }) = read_inbox(host, smart_rollup_address, ticketer, admin)? - { - // TODO: store delayed inbox messages (transactions). + }) = read_inbox( + host, + smart_rollup_address, + ticketer, + admin, + Some(delayed_bridge), + )? { + // Store the transactions in the delayed inbox. + for transaction in transactions { + delayed_inbox.save_transaction(host, transaction)?; + } + // Store the blueprints. for seq_blueprint in sequencer_blueprints { log!( @@ -79,11 +99,22 @@ pub fn fetch( smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], ticketer: Option, admin: Option, - is_sequencer: bool, + config: &mut Configuration, ) -> Result<(), anyhow::Error> { - if is_sequencer { - fetch_sequencer_blueprints(host, smart_rollup_address, ticketer, admin) - } else { - fetch_inbox_blueprints(host, smart_rollup_address, ticketer, admin) + match config { + Configuration::Sequencer { + delayed_bridge, + delayed_inbox, + } => fetch_sequencer_blueprints( + host, + smart_rollup_address, + ticketer, + admin, + delayed_bridge.clone(), + delayed_inbox, + ), + Configuration::Proxy => { + fetch_inbox_blueprints(host, smart_rollup_address, ticketer, admin) + } } } diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index 484396821c4dc9aa14b945c39f9f093cc1b6f427..b333904ea3c53381ebb937fd833ea13ee1a53d43 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -37,6 +37,7 @@ const KERNEL_VERSION_PATH: RefPath = RefPath::assert_from(b"/kernel_version"); const TICKETER: RefPath = RefPath::assert_from(b"/ticketer"); const ADMIN: RefPath = RefPath::assert_from(b"/admin"); +const DELAYED_BRIDGE: RefPath = RefPath::assert_from(b"/delayed_bridge"); // Path to the block in progress, used between reboots const EVM_BLOCK_IN_PROGRESS: RefPath = RefPath::assert_from(b"/blocks/in_progress"); @@ -622,7 +623,7 @@ pub fn store_last_info_per_level_timestamp( } pub fn read_timestamp_path( - host: &mut Host, + host: &Host, path: &OwnedPath, ) -> Result { let mut buffer = [0u8; 8]; @@ -632,7 +633,7 @@ pub fn read_timestamp_path( } pub fn read_last_info_per_level_timestamp( - host: &mut Host, + host: &Host, ) -> Result { read_timestamp_path(host, &EVM_INFO_PER_LEVEL_TIMESTAMP.into()) } @@ -672,10 +673,7 @@ pub fn index_account( } } -fn read_b58_kt1( - host: &mut Host, - path: &OwnedPath, -) -> Option { +fn read_b58_kt1(host: &Host, path: &OwnedPath) -> Option { let mut buffer = [0; 36]; store_read_slice(host, path, &mut buffer, 36).ok()?; let kt1_b58 = String::from_utf8(buffer.to_vec()).ok()?; @@ -867,3 +865,13 @@ mod internal_for_tests { #[cfg(test)] pub use internal_for_tests::*; + +/// Smart Contract of the delayed bridge +/// +/// This smart contract is used to submit transactions to the rollup +/// when in sequencer mode +pub fn read_delayed_transaction_bridge( + host: &Host, +) -> Option { + read_b58_kt1(host, &DELAYED_BRIDGE.into()) +} diff --git a/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.mligo b/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.mligo new file mode 100644 index 0000000000000000000000000000000000000000..7823338ac719dcb2ef67525896061a7d6c166152 --- /dev/null +++ b/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.mligo @@ -0,0 +1,37 @@ +(* SPDX-CopyrightText Marigold *) +#include "./ticket_type.mligo" +#include "./evm_type.mligo" + +type storage = unit + +// Ethereum RLP encoded transaction +type delayed_inbox_payload = { + transaction : bytes; + evm_rollup : address; +} + +type return = operation list * storage + +let main {transaction; evm_rollup} store : return = + // Check that one tez has been sent + let fees = Tezos.get_amount () in + if fees < 1tez then + failwith "Not enough tez to include the transaction in the delayed inbox" + else + // Craft an internal inbox message that respect the EVM rollup type + // and put the payload in the bytes field. + let evm_rollup : evm contract = + Option.unopt ((Tezos.get_contract_opt evm_rollup) : evm contract option) + in + let send_to_delayed = + Tezos.transaction (Other transaction) 0mutez evm_rollup + in + let burn_contract = + Tezos.get_contract_with_error + ("tz1burnburnburnburnburnburnburjAYjjX" : address) + "Invalid burn address" + in + let burn_1tez = + Tezos.transaction () 1tez burn_contract + in + [burn_1tez; send_to_delayed], store diff --git a/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.tz b/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.tz new file mode 100644 index 0000000000000000000000000000000000000000..e7745afc3e3fed5bc32d9c6a911d1219c7b948f6 --- /dev/null +++ b/etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.tz @@ -0,0 +1,33 @@ +{ parameter (pair (address %evm_rollup) (bytes %transaction)) ; + storage unit ; + code { UNPAIR ; + UNPAIR ; + AMOUNT ; + PUSH mutez 1000000 ; + SWAP ; + COMPARE ; + LT ; + IF { DROP 3 ; + PUSH string "Not enough tez to include the transaction in the delayed inbox" ; + FAILWITH } + { CONTRACT (or (or (pair bytes (ticket (pair nat (option bytes)))) bytes) bytes) ; + IF_NONE { PUSH string "option is None" ; FAILWITH } {} ; + PUSH mutez 0 ; + DIG 2 ; + RIGHT (pair bytes (ticket (pair nat (option bytes)))) ; + LEFT bytes ; + TRANSFER_TOKENS ; + PUSH address "tz1burnburnburnburnburnburnburjAYjjX" ; + CONTRACT unit ; + IF_NONE { PUSH string "Invalid burn address" ; FAILWITH } {} ; + PUSH mutez 1000000 ; + UNIT ; + TRANSFER_TOKENS ; + DIG 2 ; + NIL operation ; + DIG 3 ; + CONS ; + DIG 2 ; + CONS ; + PAIR } } } + diff --git a/tezt/lib_ethereum/configuration.ml b/tezt/lib_ethereum/configuration.ml index 396644d3f921ec576f1a4d4d0359369c189b7684..7d7cdef571affe87f8d51ad8c9d3f2ad909f9c73 100644 --- a/tezt/lib_ethereum/configuration.ml +++ b/tezt/lib_ethereum/configuration.ml @@ -8,7 +8,7 @@ let default_bootstrap_account_balance = Wei.of_eth_int 9999 let make_config ?bootstrap_accounts ?ticketer ?administrator - ?(sequencer = false) () = + ?(sequencer = false) ?delayed_bridge () = let open Sc_rollup_helpers.Installer_kernel_config in let ticketer = Option.fold @@ -47,6 +47,17 @@ let make_config ?bootstrap_accounts ?ticketer ?administrator if sequencer then [Set {value = "00"; to_ = Durable_storage_path.sequencer}] else [] in - match ticketer @ bootstrap_accounts @ administrator @ sequencer with + let delayed_bridge = + Option.fold + ~some:(fun delayed_bridge -> + let to_ = Durable_storage_path.delayed_bridge_path in + let value = Hex.(of_string delayed_bridge |> show) in + [Set {value; to_}]) + ~none:[] + delayed_bridge + in + match + ticketer @ bootstrap_accounts @ administrator @ sequencer @ delayed_bridge + with | [] -> None | res -> Some (`Config res) diff --git a/tezt/lib_ethereum/configuration.mli b/tezt/lib_ethereum/configuration.mli index 9f8608e769fa80d692f70f9357321eea6a9c6f47..70171437d303743950cec094372e4033864f50b1 100644 --- a/tezt/lib_ethereum/configuration.mli +++ b/tezt/lib_ethereum/configuration.mli @@ -16,5 +16,6 @@ val make_config : ?ticketer:string -> ?administrator:string -> ?sequencer:bool -> + ?delayed_bridge:string -> unit -> [> `Config of Sc_rollup_helpers.Installer_kernel_config.instr list] option diff --git a/tezt/lib_ethereum/durable_storage_path.ml b/tezt/lib_ethereum/durable_storage_path.ml index 5687e3cd83a94648c659fa0bd8533c0dd98815d0..cfb87a6f0ed0c2c4b98ed811a244592a6400eb55 100644 --- a/tezt/lib_ethereum/durable_storage_path.ml +++ b/tezt/lib_ethereum/durable_storage_path.ml @@ -51,3 +51,5 @@ let ticketer = evm "/ticketer" let sequencer = evm "/sequencer" let kernel_boot_wasm = kernel "/boot.wasm" + +let delayed_bridge_path = evm "/delayed_bridge" diff --git a/tezt/lib_ethereum/durable_storage_path.mli b/tezt/lib_ethereum/durable_storage_path.mli index 9a9c4a5322a0236f5a00940497f729dcf5f58efb..41217ce74586a37fba3a25661882e56c10ab22bc 100644 --- a/tezt/lib_ethereum/durable_storage_path.mli +++ b/tezt/lib_ethereum/durable_storage_path.mli @@ -55,3 +55,6 @@ val sequencer : path (** [kernel_boot_wasm] is the path to the kernel `boot.wasm`. *) val kernel_boot_wasm : path + +(** [delayed_bridge_path] is the path to the delayed transaction bridge contract. *) +val delayed_bridge_path : path diff --git a/tezt/lib_ethereum/helpers.ml b/tezt/lib_ethereum/helpers.ml index f6b449d75a5ffedf85aa156fcb8b146d94b2ebd8..e3fee52687fd6667484598c64e12211117c08866 100644 --- a/tezt/lib_ethereum/helpers.ml +++ b/tezt/lib_ethereum/helpers.ml @@ -24,6 +24,9 @@ (* *) (*****************************************************************************) +let evm_type = + "or (or (pair bytes (ticket (pair nat (option bytes)))) bytes) bytes" + let no_0x s = if String.starts_with ~prefix:"0x" s then String.sub s 2 (String.length s - 2) else s diff --git a/tezt/lib_ethereum/helpers.mli b/tezt/lib_ethereum/helpers.mli index 28e44e6e76f94f027fff0e87fef7e475eccdeb6f..e29fb09036810bab055e59faa39813e23af2ab62 100644 --- a/tezt/lib_ethereum/helpers.mli +++ b/tezt/lib_ethereum/helpers.mli @@ -24,6 +24,9 @@ (* *) (*****************************************************************************) +(** Michelson type to use when originating the EVM rollup. *) +val evm_type : string + (** [no_0x s] removes the prefix [0x] of [s] if it exists. *) val no_0x : string -> string diff --git a/tezt/tests/evm_rollup.ml b/tezt/tests/evm_rollup.ml index 81d8f59f7374b663a308d14df21d95687ce9d20f..d2b06277ce453f49e1b5cb55a5c29c8c61b74988 100644 --- a/tezt/tests/evm_rollup.ml +++ b/tezt/tests/evm_rollup.ml @@ -21,9 +21,6 @@ open Rpc.Syntax let pvm_kind = "wasm_2_0_0" -let evm_type = - "or (or (pair bytes (ticket (pair nat (option bytes)))) bytes) bytes" - let kernel_inputs_path = "tezt/tests/evm_kernel_inputs" let exchanger_path () = diff --git a/tezt/tests/evm_sequencer.ml b/tezt/tests/evm_sequencer.ml index ab2da0c785ebd03a975eda79ae5e844ae62942d4..f75e55ec03ec7aa4eb4874216ab6b5082c33362c 100644 --- a/tezt/tests/evm_sequencer.ml +++ b/tezt/tests/evm_sequencer.ml @@ -8,19 +8,51 @@ open Sc_rollup_helpers open Rpc.Syntax +let uses _protocol = + [ + Constant.octez_smart_rollup_node; + Constant.octez_evm_node; + Constant.smart_rollup_installer; + ] + (** Renaming the helper to avoid confusion on its behavior. *) let next_rollup_node_level = Helpers.next_evm_level +type l1_contracts = {delayed_transaction_bridge : string} + type setup = { node : Node.t; client : Client.t; + sc_rollup_address : string; sc_rollup_node : Sc_rollup_node.t; evm_node : Evm_node.t; + l1_contracts : l1_contracts; } +let delayed_path () = + Base.( + project_root + // "etherlink/kernel_evm/l1_bridge/delayed_transaction_bridge.tz") + +let setup_l1_contracts client = + (* Originates the delayed transaction bridge. *) + let* delayed_transaction_bridge = + Client.originate_contract + ~alias:"evm-seq-delayed-bridge" + ~amount:Tez.zero + ~src:Constant.bootstrap1.public_key_hash + ~prg:(delayed_path ()) + ~burn_cap:Tez.one + client + in + let* () = Client.bake_for_and_wait client in + + return {delayed_transaction_bridge} + let setup_sequencer ?(bootstrap_accounts = Eth_account.bootstrap_accounts) protocol = let* node, client = setup_l1 protocol in + let* l1_contracts = setup_l1_contracts client in let sc_rollup_node = Sc_rollup_node.create ~default_operator:Constant.bootstrap1.public_key_hash @@ -30,7 +62,11 @@ let setup_sequencer ?(bootstrap_accounts = Eth_account.bootstrap_accounts) in let preimages_dir = Sc_rollup_node.data_dir sc_rollup_node // "wasm_2_0_0" in let config = - Configuration.make_config ~bootstrap_accounts ~sequencer:true () + Configuration.make_config + ~bootstrap_accounts + ~sequencer:true + ~delayed_bridge:l1_contracts.delayed_transaction_bridge + () in let* {output; _} = prepare_installer_kernel @@ -43,7 +79,7 @@ let setup_sequencer ?(bootstrap_accounts = Eth_account.bootstrap_accounts) originate_sc_rollup ~kind:"wasm_2_0_0" ~boot_sector:("file:" ^ output) - ~parameters_ty:"unit" + ~parameters_ty:Helpers.evm_type client in let* () = @@ -55,19 +91,35 @@ let setup_sequencer ?(bootstrap_accounts = Eth_account.bootstrap_accounts) let* evm_node = Evm_node.init ~mode (Sc_rollup_node.endpoint sc_rollup_node) in - return {evm_node; node; client; sc_rollup_node} + return + {node; client; evm_node; l1_contracts; sc_rollup_address; sc_rollup_node} + +let send_raw_transaction_to_delayed_inbox ?(amount = Tez.one) ?expect_failure + ~sc_rollup_node ~node ~client ~l1_contracts ~sc_rollup_address raw_tx = + let expected_hash = + `Hex raw_tx |> Hex.to_bytes |> Tezos_crypto.Hacl.Hash.Keccak_256.digest + |> Hex.of_bytes |> Hex.show + in + let* () = + Client.transfer + ~arg:(sf "Pair %S 0x%s" sc_rollup_address raw_tx) + ~amount + ~giver:Constant.bootstrap2.public_key_hash + ~receiver:l1_contracts.delayed_transaction_bridge + ~burn_cap:Tez.one + ?expect_failure + client + in + let* () = Client.bake_for_and_wait client in + let* _ = next_rollup_node_level ~sc_rollup_node ~node ~client in + Lwt.return expected_hash let test_persistent_state = Protocol.register_test ~__FILE__ ~tags:["evm"; "sequencer"] ~title:"Sequencer state is persistent across runs" - ~uses:(fun _protocol -> - [ - Constant.octez_evm_node; - Constant.octez_smart_rollup_node; - Constant.smart_rollup_installer; - ]) + ~uses @@ fun protocol -> let* {evm_node; _} = setup_sequencer protocol in (* Sleep to let the sequencer produce some blocks. *) @@ -98,14 +150,9 @@ let test_publish_blueprints = ~__FILE__ ~tags:[Tag.flaky; "evm"; "sequencer"; "data"] ~title:"Sequencer publishes the blueprints to L1" - ~uses:(fun _protocol -> - [ - Constant.octez_smart_rollup_node; - Constant.smart_rollup_installer; - Constant.octez_evm_node; - ]) + ~uses @@ fun protocol -> - let* {evm_node; node; client; sc_rollup_node} = setup_sequencer protocol in + let* {evm_node; node; client; sc_rollup_node; _} = setup_sequencer protocol in (* Sleep to let the sequencer produce some blocks. *) let* () = Lwt_unix.sleep 20. in (* Ask for the current block. *) @@ -131,6 +178,56 @@ let test_publish_blueprints = ~error_msg:"Expected the same head on the rollup node and the sequencer" ; unit +let test_send_transaction_to_delayed_inbox = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "sequencer"; "delayed_inbox"] + ~title:"Send a transaction to the delayed inbox" + ~uses + @@ fun protocol -> + (* Start the evm node *) + let* {client; node; l1_contracts; sc_rollup_address; sc_rollup_node; _} = + setup_sequencer protocol + in + let raw_transfer = + "f86d80843b9aca00825b0494b53dc01974176e5dff2298c5a94343c2585e3c54880de0b6b3a764000080820a96a07a3109107c6bd1d555ce70d6253056bc18996d4aff4d4ea43ff175353f49b2e3a05f9ec9764dc4a3c3ab444debe2c3384070de9014d44732162bb33ee04da187ef" + in + let send ~amount ?expect_failure () = + send_raw_transaction_to_delayed_inbox + ~sc_rollup_node + ~client + ~l1_contracts + ~sc_rollup_address + ~node + ~amount + ?expect_failure + raw_transfer + in + (* Test that paying less than 1XTZ is not allowed. *) + let* _hash = + send ~amount:(Tez.parse_floating "0.9") ~expect_failure:true () + in + (* Test the correct case where the user burns 1XTZ to send the transaction. *) + let* hash = send ~amount:Tez.one ~expect_failure:false () in + (* Assert that the expected transaction hash is found in the delayed inbox + durable storage path. *) + let* delayed_transactions_hashes = + Sc_rollup_node.RPC.call sc_rollup_node + @@ Sc_rollup_rpc.get_global_block_durable_state_value + ~pvm_kind:"wasm_2_0_0" + ~operation:Sc_rollup_rpc.Subkeys + ~key:"/evm/delayed-inbox" + () + in + Check.(list_mem string hash delayed_transactions_hashes) + ~error_msg:"hash %L should be present in the delayed inbox %R" ; + (* Test that paying more than 1XTZ is allowed. *) + let* _hash = + send ~amount:(Tez.parse_floating "1.1") ~expect_failure:false () + in + unit + let register ~protocols = test_persistent_state protocols ; - test_publish_blueprints protocols + test_publish_blueprints protocols ; + test_send_transaction_to_delayed_inbox protocols