From 3649cf615e5fea79cda78af3aa95338b83d6d973 Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Tue, 18 Jun 2024 17:30:03 +0100 Subject: [PATCH] EVM: integrate FA deposit application behind the feature flag Co-authored-by: Valentin Chaboche --- etherlink/CHANGES_KERNEL.md | 1 + .../evm_execution/src/fa_bridge/mod.rs | 3 + etherlink/kernel_evm/kernel/src/apply.rs | 52 +++- .../kernel_evm/kernel/src/delayed_inbox.rs | 18 ++ etherlink/kernel_evm/kernel/src/error.rs | 6 +- etherlink/kernel_evm/kernel/src/fees.rs | 11 + etherlink/kernel_evm/kernel/src/inbox.rs | 184 +++++++++++--- etherlink/kernel_evm/kernel/src/lib.rs | 110 +++++++- etherlink/kernel_evm/kernel/src/parsing.rs | 108 ++++++-- etherlink/kernel_evm/kernel/src/stage_one.rs | 32 ++- etherlink/kernel_evm/kernel/src/storage.rs | 2 +- etherlink/kernel_evm/kernel/src/tick_model.rs | 2 +- .../fa_bridge/ticket_router_tester.tz | 99 ++++++++ etherlink/tezt/lib/contract_path.ml | 5 + etherlink/tezt/lib/durable_storage_path.ml | 10 + etherlink/tezt/lib/durable_storage_path.mli | 6 + etherlink/tezt/lib/evm_node.ml | 3 +- etherlink/tezt/lib/evm_node.mli | 2 +- etherlink/tezt/tests/evm_sequencer.ml | 237 +++++++++++++++++- 19 files changed, 813 insertions(+), 78 deletions(-) create mode 100644 etherlink/tezos_contracts/fa_bridge/ticket_router_tester.tz diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index 5847607fb06a..c2d6c9ef2eee 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -11,6 +11,7 @@ if they are smaller than current view of the L1 timestamp plus an margin error of 5 minutes. 5 minutes is the default value but can be configured by the installer. (!13827) +- FA deposits are applied if FA bridge feature is enabled. (!13835) ## Bug fixes diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs index e4013a403a98..8ff5033b405e 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/mod.rs @@ -68,6 +68,9 @@ pub const FA_DEPOSIT_PROXY_GAS_LIMIT: u64 = 1_200_000; /// the global ticket table and emitting deposit event. pub const FA_DEPOSIT_INNER_TICKS: u64 = 2_000_000; +/// Number of ticks used to parse FA deposit +pub const TICKS_PER_FA_DEPOSIT_PARSING: u64 = 2_000_000; + /// TODO: Overapproximation of the amount of ticks required /// to execute a FA deposit. pub const FA_DEPOSIT_TOTAL_TICKS: u64 = 10_000_000; diff --git a/etherlink/kernel_evm/kernel/src/apply.rs b/etherlink/kernel_evm/kernel/src/apply.rs index 7d50bba365f5..7e184abdac07 100644 --- a/etherlink/kernel_evm/kernel/src/apply.rs +++ b/etherlink/kernel_evm/kernel/src/apply.rs @@ -11,6 +11,8 @@ use evm::{ExitError, ExitReason, ExitSucceed}; use evm_execution::account_storage::{ account_path, EthereumAccount, EthereumAccountStorage, }; +use evm_execution::fa_bridge::deposit::FaDeposit; +use evm_execution::fa_bridge::execute_fa_deposit; use evm_execution::handler::{ExecutionOutcome, ExtendedExitReason, RouterInterface}; use evm_execution::precompiles::PrecompileBTreeMap; use evm_execution::run_transaction; @@ -41,6 +43,7 @@ impl Transaction { fn to(&self) -> Option { match &self.content { TransactionContent::Deposit(Deposit { receiver, .. }) => Some(*receiver), + TransactionContent::FaDeposit(_) => Some(H160::zero()), TransactionContent::Ethereum(transaction) | TransactionContent::EthereumDelayed(transaction) => transaction.to, } @@ -48,7 +51,7 @@ impl Transaction { fn data(&self) -> Vec { match &self.content { - TransactionContent::Deposit(_) => vec![], + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => vec![], TransactionContent::Ethereum(transaction) | TransactionContent::EthereumDelayed(transaction) => { transaction.data.clone() @@ -59,6 +62,7 @@ impl Transaction { fn value(&self) -> U256 { match &self.content { TransactionContent::Deposit(Deposit { amount, .. }) => *amount, + &TransactionContent::FaDeposit(_) => U256::zero(), TransactionContent::Ethereum(transaction) | TransactionContent::EthereumDelayed(transaction) => transaction.value, } @@ -66,7 +70,7 @@ impl Transaction { fn nonce(&self) -> u64 { match &self.content { - TransactionContent::Deposit(_) => 0, + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => 0, TransactionContent::Ethereum(transaction) | TransactionContent::EthereumDelayed(transaction) => transaction.nonce, } @@ -74,7 +78,7 @@ impl Transaction { fn signature(&self) -> Option { match &self.content { - TransactionContent::Deposit(_) => None, + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => None, TransactionContent::Ethereum(transaction) | TransactionContent::EthereumDelayed(transaction) => { transaction.signature.clone() @@ -145,7 +149,7 @@ fn make_object_info( TransactionContent::Ethereum(e) | TransactionContent::EthereumDelayed(e) => { (e.gas_limit_with_fees().into(), e.max_fee_per_gas) } - TransactionContent::Deposit(_) => { + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => { (fee_updates.overall_gas_used, fee_updates.overall_gas_price) } }; @@ -462,6 +466,35 @@ fn apply_deposit( })) } +fn apply_fa_deposit( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + fa_deposit: &FaDeposit, + block_constants: &BlockConstants, + precompiles: &PrecompileBTreeMap, + allocated_ticks: u64, +) -> Result, Error> { + let caller = H160::zero(); + let outcome = execute_fa_deposit( + host, + block_constants, + evm_account_storage, + precompiles, + CONFIG, + caller, + fa_deposit, + allocated_ticks, + ) + .map_err(Error::InvalidRunTransaction)?; + + Ok(ExecutionResult::Valid(TransactionResult { + caller, + gas_used: outcome.gas_used.into(), + estimated_ticks_used: outcome.estimated_ticks_used, + execution_outcome: Some(outcome), + })) +} + pub const WITHDRAWAL_OUTBOX_QUEUE: RefPath = RefPath::assert_from(b"/evm/world_state/__outbox_queue"); @@ -614,6 +647,17 @@ pub fn apply_transaction( log!(host, Benchmarking, "Transaction type: DEPOSIT"); apply_deposit(host, evm_account_storage, deposit)? } + TransactionContent::FaDeposit(fa_deposit) => { + log!(host, Benchmarking, "Transaction type: FA_DEPOSIT"); + apply_fa_deposit( + host, + evm_account_storage, + fa_deposit, + block_constants, + precompiles, + allocated_ticks, + )? + } }; match apply_result { diff --git a/etherlink/kernel_evm/kernel/src/delayed_inbox.rs b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs index d870bff9c5ed..6f90f97bfb6b 100644 --- a/etherlink/kernel_evm/kernel/src/delayed_inbox.rs +++ b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs @@ -9,6 +9,7 @@ use crate::{ storage, }; use anyhow::Result; +use evm_execution::fa_bridge::deposit::FaDeposit; use rlp::{Decodable, DecoderError, Encodable}; use tezos_ethereum::{ rlp_helpers, transaction::TRANSACTION_HASH_SIZE, tx_common::EthereumTransactionCommon, @@ -31,6 +32,9 @@ pub const DELAYED_TRANSACTION_TAG: u8 = 0x01; // Tag that indicates the delayed transaction is a deposit. pub const DELAYED_DEPOSIT_TAG: u8 = 0x02; +// Tag that indicates the delayed transaction is a FA deposit. +pub const DELAYED_FA_DEPOSIT_TAG: u8 = 0x03; + /// Hash of a transaction /// /// It represents the key of the transaction in the delayed inbox. @@ -65,6 +69,7 @@ impl AsRef<[u8]> for Hash { pub enum DelayedTransaction { Ethereum(EthereumTransactionCommon), Deposit(Deposit), + FaDeposit(FaDeposit), } impl Encodable for DelayedTransaction { @@ -79,6 +84,10 @@ impl Encodable for DelayedTransaction { stream.append(&DELAYED_DEPOSIT_TAG); stream.append(delayed_deposit); } + DelayedTransaction::FaDeposit(delayed_fa_deposit) => { + stream.append(&DELAYED_FA_DEPOSIT_TAG); + stream.append(delayed_fa_deposit); + } } } } @@ -104,6 +113,10 @@ impl Decodable for DelayedTransaction { let deposit = Deposit::decode(&payload)?; Ok(DelayedTransaction::Deposit(deposit)) } + DELAYED_FA_DEPOSIT_TAG => { + let fa_deposit = FaDeposit::decode(&payload)?; + Ok(DelayedTransaction::FaDeposit(fa_deposit)) + } _ => Err(DecoderError::Custom("unknown tag")), } } @@ -172,6 +185,7 @@ impl DelayedInbox { TransactionContent::Ethereum(_) => anyhow::bail!("Non-delayed evm transaction should not be saved to the delayed inbox. {:?}", tx.tx_hash), TransactionContent::EthereumDelayed(tx) => DelayedTransaction::Ethereum(tx), TransactionContent::Deposit(deposit) => DelayedTransaction::Deposit(deposit), + TransactionContent::FaDeposit(fa_deposit) => DelayedTransaction::FaDeposit(fa_deposit) }; let item = DelayedInboxItem { transaction, @@ -204,6 +218,10 @@ impl DelayedInbox { tx_hash: tx_hash.0, content: TransactionContent::Deposit(deposit), }, + DelayedTransaction::FaDeposit(fa_deposit) => Transaction { + tx_hash: tx_hash.0, + content: TransactionContent::FaDeposit(fa_deposit), + }, } } diff --git a/etherlink/kernel_evm/kernel/src/error.rs b/etherlink/kernel_evm/kernel/src/error.rs index 7dcf1f1b72fa..bc8125c3c5b8 100644 --- a/etherlink/kernel_evm/kernel/src/error.rs +++ b/etherlink/kernel_evm/kernel/src/error.rs @@ -86,6 +86,8 @@ pub enum Error { Storage(StorageError), #[error("Invalid conversion")] InvalidConversion, + #[error("Failed to decode: {0}")] + RlpDecoderError(DecoderError), #[error("Invalid parsing")] InvalidParsing, #[error(transparent)] @@ -133,8 +135,8 @@ impl From for Error { } impl From for Error { - fn from(_: DecoderError) -> Self { - Self::InvalidConversion + fn from(e: DecoderError) -> Self { + Self::RlpDecoderError(e) } } diff --git a/etherlink/kernel_evm/kernel/src/fees.rs b/etherlink/kernel_evm/kernel/src/fees.rs index 6564d3bafc26..940c21152c2d 100644 --- a/etherlink/kernel_evm/kernel/src/fees.rs +++ b/etherlink/kernel_evm/kernel/src/fees.rs @@ -82,6 +82,7 @@ impl TransactionContent { Self::EthereumDelayed(_) => { FeeUpdates::for_delayed_tx(block_fees, execution_gas_used) } + Self::FaDeposit(_) => FeeUpdates::for_fa_deposit(execution_gas_used), } } } @@ -97,6 +98,16 @@ impl FeeUpdates { } } + fn for_fa_deposit(gas_used: U256) -> Self { + Self { + overall_gas_used: gas_used, + overall_gas_price: U256::zero(), + burn_amount: U256::zero(), + charge_user_amount: U256::zero(), + compensate_sequencer_amount: U256::zero(), + } + } + fn for_tx( tx: &EthereumTransactionCommon, block_fees: &BlockFees, diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index a7cbfbc99be8..017bf877aba1 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -25,6 +25,7 @@ use crate::tick_model::maximum_ticks_for_sequencer_chunk; use crate::upgrade::*; use crate::Error; use crate::{simulation, upgrade}; +use evm_execution::fa_bridge::deposit::FaDeposit; use primitive_types::{H160, U256}; use rlp::{Decodable, DecoderError, Encodable}; use sha3::{Digest, Keccak256}; @@ -74,11 +75,13 @@ pub enum TransactionContent { Ethereum(EthereumTransactionCommon), Deposit(Deposit), EthereumDelayed(EthereumTransactionCommon), + FaDeposit(FaDeposit), } const ETHEREUM_TX_TAG: u8 = 1; const DEPOSIT_TX_TAG: u8 = 2; const ETHEREUM_DELAYED_TX_TAG: u8 = 3; +const FA_DEPOSIT_TX_TAG: u8 = 4; impl Encodable for TransactionContent { fn rlp_append(&self, stream: &mut rlp::RlpStream) { @@ -96,6 +99,10 @@ impl Encodable for TransactionContent { stream.append(ÐEREUM_DELAYED_TX_TAG); eth.rlp_append(stream) } + TransactionContent::FaDeposit(fa_dep) => { + stream.append(&FA_DEPOSIT_TX_TAG); + fa_dep.rlp_append(stream) + } } } } @@ -123,6 +130,10 @@ impl Decodable for TransactionContent { let eth = EthereumTransactionCommon::decode(&tx)?; Ok(Self::EthereumDelayed(eth)) } + FA_DEPOSIT_TX_TAG => { + let fa_deposit = FaDeposit::decode(&tx)?; + Ok(Self::FaDeposit(fa_deposit)) + } _ => Err(DecoderError::Custom("Unknown transaction tag.")), } } @@ -142,14 +153,15 @@ impl Transaction { // FIXME: probably need to take into account the access list e.data.len() as u64 } + TransactionContent::FaDeposit(_) => 0, } } pub fn is_delayed(&self) -> bool { match &self.content { - TransactionContent::Deposit(_) | TransactionContent::EthereumDelayed(_) => { - true - } + TransactionContent::Deposit(_) + | TransactionContent::EthereumDelayed(_) + | TransactionContent::FaDeposit(_) => true, TransactionContent::Ethereum(_) => false, } } @@ -183,7 +195,9 @@ impl Transaction { pub fn type_(&self) -> TransactionType { match &self.content { // The deposit is considered arbitrarily as a legacy transaction - TransactionContent::Deposit(_) => TransactionType::Legacy, + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => { + TransactionType::Legacy + } TransactionContent::Ethereum(tx) | TransactionContent::EthereumDelayed(tx) => tx.type_, } @@ -201,6 +215,7 @@ pub fn read_input( tezos_contracts: &TezosContracts, inbox_is_empty: &mut bool, parsing_context: &mut Mode::Context, + enable_fa_deposits: bool, ) -> Result, Error> { let input = host.read_input()?; @@ -213,6 +228,7 @@ pub fn read_input( smart_rollup_address, tezos_contracts, parsing_context, + enable_fa_deposits, )) } None => Ok(InputResult::NoInput), @@ -235,6 +251,12 @@ pub trait InputHandler { deposit: Deposit, inbox_content: &mut Self::Inbox, ) -> anyhow::Result<()>; + + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()>; } impl InputHandler for ProxyInput { @@ -280,6 +302,17 @@ impl InputHandler for ProxyInput { .push(handle_deposit(host, deposit)?); Ok(()) } + + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()> { + inbox_content + .transactions + .push(handle_fa_deposit(host, fa_deposit)?); + Ok(()) + } } impl InputHandler for SequencerInput { @@ -325,6 +358,17 @@ impl InputHandler for SequencerInput { let tx = handle_deposit(host, deposit)?; delayed_inbox.save_transaction(host, tx, previous_timestamp, level) } + + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + delayed_inbox: &mut Self::Inbox, + ) -> anyhow::Result<()> { + let previous_timestamp = read_last_info_per_level_timestamp(host)?; + let level = read_l1_level(host)?; + let tx = handle_fa_deposit(host, fa_deposit)?; + delayed_inbox.save_transaction(host, tx, previous_timestamp, level) + } } fn handle_transaction_chunk( @@ -406,6 +450,18 @@ fn handle_deposit( }) } +fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, +) -> Result { + let seed = host.reveal_metadata().raw_rollup_address; + let tx_hash = fa_deposit.hash(&seed).into(); + Ok(Transaction { + tx_hash, + content: TransactionContent::FaDeposit(fa_deposit), + }) +} + fn force_kernel_upgrade(host: &mut impl Runtime) -> anyhow::Result<()> { match upgrade::read_kernel_upgrade(host)? { Some(kernel_upgrade) => { @@ -442,6 +498,9 @@ pub fn handle_input( store_l1_level(host, info.level)? } Input::Deposit(deposit) => Mode::handle_deposit(host, deposit, inbox_content)?, + Input::FaDeposit(fa_deposit) => { + Mode::handle_fa_deposit(host, fa_deposit, inbox_content)? + } Input::ForceKernelUpgrade => force_kernel_upgrade(host)?, } Ok(()) @@ -460,6 +519,7 @@ fn read_and_dispatch_input( parsing_context: &mut Mode::Context, inbox_is_empty: &mut bool, res: &mut Mode::Inbox, + enable_fa_deposits: bool, ) -> anyhow::Result { let input: InputResult = read_input( host, @@ -467,6 +527,7 @@ fn read_and_dispatch_input( tezos_contracts, inbox_is_empty, parsing_context, + enable_fa_deposits, )?; match input { InputResult::NoInput => { @@ -500,6 +561,7 @@ pub fn read_proxy_inbox( host: &mut Host, smart_rollup_address: [u8; 20], tezos_contracts: &TezosContracts, + enable_fa_deposits: bool, ) -> Result, anyhow::Error> { let mut res = ProxyInboxContent { transactions: vec![], @@ -517,6 +579,7 @@ pub fn read_proxy_inbox( &mut (), &mut inbox_is_empty, &mut res, + enable_fa_deposits, ) { Err(err) => // If we failed to read or dispatch the input. @@ -564,6 +627,7 @@ pub fn read_sequencer_inbox( delayed_bridge: ContractKt1Hash, sequencer: PublicKey, delayed_inbox: &mut DelayedInbox, + enable_fa_deposits: bool, ) -> Result { // The mutable variable is used to retrieve the information of whether the // inbox was empty or not. As we consume all the inbox in one go, if the @@ -599,6 +663,7 @@ pub fn read_sequencer_inbox( &mut parsing_context, &mut inbox_is_empty, delayed_inbox, + enable_fa_deposits, ) { Err(err) => // If we failed to read or dispatch the input. @@ -768,10 +833,14 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))); - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap() - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap() + .unwrap(); let expected_transactions = vec![Transaction { tx_hash, content: Ethereum(tx), @@ -793,10 +862,14 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap() - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap() + .unwrap(); let expected_transactions = vec![Transaction { tx_hash, content: Ethereum(tx), @@ -848,6 +921,7 @@ mod tests { kernel_governance: None, kernel_security_governance: None, }, + false, ) .unwrap() .unwrap(); @@ -888,9 +962,13 @@ mod tests { new_chunk2, ))); - let _inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap(); + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap(); let num_chunks = chunked_transaction_num_chunks(&mut host, &tx_hash) .expect("The number of chunks should exist"); @@ -932,9 +1010,13 @@ mod tests { }; host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); - let _inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap(); + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap(); // The out of bounds chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -965,9 +1047,13 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); - let _inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap(); + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap(); // The unknown chunk should not exist. let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); @@ -1014,10 +1100,14 @@ mod tests { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk0))); - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap() - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap() + .unwrap(); assert_eq!( inbox_content, ProxyInboxContent { @@ -1029,10 +1119,14 @@ mod tests { for input in inputs { host.add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) } - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap() - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap() + .unwrap(); let expected_transactions = vec![Transaction { tx_hash, @@ -1086,10 +1180,14 @@ mod tests { host.add_external(framed); - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap() - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap() + .unwrap(); let expected_transactions = vec![Transaction { tx_hash, content: Ethereum(tx), @@ -1105,15 +1203,23 @@ mod tests { // an empty inbox content. As we test in isolation there is nothing // in the inbox, we mock it by adding a single input. host.add_external(Bytes::from(vec![])); - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .unwrap(); assert!(inbox_content.is_some()); // Reading again the inbox returns no inbox content at all. - let inbox_content = - read_proxy_inbox(&mut host, SMART_ROLLUP_ADDRESS, &TezosContracts::default()) - .unwrap(); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + ) + .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 5ef17e06bb8b..5b99c47c9fd5 100644 --- a/etherlink/kernel_evm/kernel/src/lib.rs +++ b/etherlink/kernel_evm/kernel/src/lib.rs @@ -356,8 +356,11 @@ mod tests { use crate::fees; use crate::main; use crate::mock_internal::MockInternal; + use crate::parsing::RollupType; use crate::safe_storage::SafeStorage; - use crate::storage::store_chain_id; + use crate::storage::{ + read_transaction_receipt_status, store_chain_id, ENABLE_FA_BRIDGE, + }; use crate::{ blueprint::Blueprint, inbox::{Transaction, TransactionContent}, @@ -365,13 +368,16 @@ mod tests { upgrade::KernelUpgrade, }; use evm_execution::account_storage::{self, EthereumAccountStorage}; + use evm_execution::fa_bridge::deposit::{ticket_hash, FaDeposit}; use evm_execution::handler::RouterInterface; use evm_execution::utilities::keccak256_hash; use evm_execution::NATIVE_TOKEN_TICKETER_PATH; use pretty_assertions::assert_eq; use primitive_types::{H160, U256}; + use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_data_encoding::nom::NomReader; use tezos_ethereum::block::BlockFees; + use tezos_ethereum::transaction::TransactionStatus; use tezos_ethereum::{ transaction::{TransactionHash, TransactionType}, tx_common::EthereumTransactionCommon, @@ -379,7 +385,7 @@ mod tests { use tezos_smart_rollup::michelson::ticket::FA2_1Ticket; use tezos_smart_rollup::michelson::{ - MichelsonContract, MichelsonOption, MichelsonPair, + MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonOption, MichelsonPair, }; use tezos_smart_rollup::outbox::{OutboxMessage, OutboxMessageTransaction}; use tezos_smart_rollup::types::{Contract, Entrypoint}; @@ -389,7 +395,7 @@ mod tests { use tezos_smart_rollup_encoding::smart_rollup::SmartRollupAddress; use tezos_smart_rollup_encoding::timestamp::Timestamp; use tezos_smart_rollup_host::path::RefPath; - use tezos_smart_rollup_mock::MockHost; + use tezos_smart_rollup_mock::{MockHost, TransferMetadata}; const DUMMY_CHAIN_ID: U256 = U256::one(); const DUMMY_BASE_FEE_PER_GAS: u64 = 12345u64; @@ -767,4 +773,102 @@ mod tests { assert_eq!(expected_message, decoded_message); } + + fn send_fa_deposit(enable_fa_bridge: bool) -> Option { + // init host + let mut mock_host = MockHost::default(); + let mut internal = MockInternal(); + let mut safe_storage = SafeStorage { + host: &mut mock_host, + internal: &mut internal, + }; + + // enable FA bridge feature + if enable_fa_bridge { + safe_storage + .store_write_all(&ENABLE_FA_BRIDGE, &[1u8]) + .unwrap(); + } + + // init rollup parameters (legacy optimized forge) + // type: + // (or + // (or (pair %deposit (bytes %routing_info) + // (ticket %ticket (pair %content (nat %token_id) + // (option %metadata bytes)))) + // (bytes %b)) + // (bytes %c)) + // value: + // { + // "deposit": { + // "routing_info": "01" * 20 + "02" * 20, + // "ticket": ( + // "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + // (1, None), + // 42 + // ) + // } + // } + let params = hex::decode( + "\ + 0505050507070a00000028010101010101010101010101010101010101010102\ + 0202020202020202020202020202020202020207070a0000001601d496def47a\ + 3be89f5d54c6e6bb13cc6645d6e166000707070700010306002a", + ) + .unwrap(); + let (_, payload) = + RollupType::nom_read(¶ms).expect("Failed to decode params"); + + let metadata = TransferMetadata::new( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + "tz1P2Po7YM526ughEsRbY4oR9zaUPDZjxFrb", + ); + safe_storage.host.add_transfer(payload, &metadata); + + // run kernel + main(&mut safe_storage).expect("Kernel error"); + // QUESTION: looks like to get to the stage with block creation we need to call main twice (maybe check blueprint instead?) [1] + main(&mut safe_storage).expect("Kernel error"); + + // reconstruct ticket + let ticket = FA2_1Ticket::new( + Contract::Originated( + ContractKt1Hash::from_base58_check( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + ) + .unwrap(), + ), + MichelsonPair::>( + 1u32.into(), + MichelsonOption(None), + ), + 42i32, + ) + .expect("Failed to construct ticket"); + + // reconstruct deposit + let deposit = FaDeposit { + amount: 42.into(), + proxy: Some(H160([2u8; 20])), + inbox_level: safe_storage.host.level(), // level not yet advanced + inbox_msg_id: 2, + receiver: H160([1u8; 20]), + ticket_hash: ticket_hash(&ticket).unwrap(), + }; + // smart rollup address as a seed + let tx_hash = deposit.hash(&[0u8; 20]); + + // read transaction receipt + read_transaction_receipt_status(&mut safe_storage, &tx_hash.0).ok() + } + + #[test] + fn test_fa_deposit_applied_if_feature_enabled() { + assert_eq!(send_fa_deposit(true), Some(TransactionStatus::Success)); + } + + #[test] + fn test_fa_deposit_rejected_if_feature_disabled() { + assert_eq!(send_fa_deposit(false), None); + } } diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index e99392142933..246fd1b102b3 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -18,6 +18,8 @@ use crate::{ upgrade::KernelUpgrade, upgrade::SequencerUpgrade, }; +use evm_execution::fa_bridge::deposit::FaDeposit; +use evm_execution::fa_bridge::TICKS_PER_FA_DEPOSIT_PARSING; use primitive_types::{H160, U256}; use rlp::Encodable; use sha3::{Digest, Keccak256}; @@ -119,6 +121,7 @@ pub enum SequencerInput { pub enum Input { ModeSpecific(Mode), Deposit(Deposit), + FaDeposit(FaDeposit), Upgrade(KernelUpgrade), SequencerUpgrade(SequencerUpgrade), RemoveSequencer, @@ -173,6 +176,8 @@ pub trait Parsable { Self: std::marker::Sized; fn on_deposit(context: &mut Self::Context); + + fn on_fa_deposit(context: &mut Self::Context); } impl ProxyInput { @@ -270,6 +275,8 @@ impl Parsable for ProxyInput { } fn on_deposit(_: &mut Self::Context) {} + + fn on_fa_deposit(_: &mut Self::Context) {} } pub struct SequencerParsingContext { @@ -364,6 +371,12 @@ impl Parsable for SequencerInput { .allocated_ticks .saturating_sub(TICKS_PER_DEPOSIT_PARSING); } + + fn on_fa_deposit(context: &mut Self::Context) { + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_PER_FA_DEPOSIT_PARSING); + } } impl InputResult { @@ -417,24 +430,43 @@ impl InputResult { } } + fn parse_fa_deposit( + host: &mut Host, + ticket: FA2_1Ticket, + routing_info: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + context: &mut Mode::Context, + ) -> Self { + // Account for tick at the beginning of the deposit, in case it fails + // directly. We prefer to overapproximate rather than under approximate. + Mode::on_fa_deposit(context); + match FaDeposit::try_parse(ticket, routing_info, inbox_level, inbox_msg_id) { + Ok(fa_deposit) => { + log!(host, Debug, "Parsed from input: {}", fa_deposit.display()); + InputResult::Input(Input::FaDeposit(fa_deposit)) + } + Err(err) => { + log!( + host, + Debug, + "Deposit ignored because of parsing errors: {}", + err + ); + InputResult::Unparsable + } + } + } + fn parse_deposit( host: &mut Host, ticket: FA2_1Ticket, receiver: MichelsonBytes, - ticketer: &Option, context: &mut Mode::Context, ) -> Self { // Account for tick at the beginning of the deposit, in case it fails // directly. We prefer to overapproximate rather than under approximate. Mode::on_deposit(context); - match &ticket.creator().0 { - Contract::Originated(kt1) if Some(kt1) == ticketer.as_ref() => (), - _ => { - log!(host, Info, "Deposit ignored because of different ticketer"); - return InputResult::Unparsable; - } - }; - // Amount let (_sign, amount_bytes) = ticket.amount().to_bytes_le(); // We use the `U256::from_little_endian` as it takes arbitrary long @@ -461,12 +493,16 @@ impl InputResult { Self::Input(Input::Deposit(content)) } + #[allow(clippy::too_many_arguments)] fn parse_internal_transfer( host: &mut Host, transfer: Transfer, smart_rollup_address: &[u8], tezos_contracts: &TezosContracts, context: &mut Mode::Context, + inbox_level: u32, + inbox_msg_id: u32, + enable_fa_deposits: bool, ) -> Self { if transfer.destination.hash().0 != smart_rollup_address { log!( @@ -482,13 +518,37 @@ impl InputResult { match transfer.payload { MichelsonOr::Left(left) => match left { MichelsonOr::Left(MichelsonPair(receiver, ticket)) => { - Self::parse_deposit( - host, - ticket, - receiver, - &tezos_contracts.ticketer, - context, - ) + match &ticket.creator().0 { + Contract::Originated(kt1) => { + if Some(kt1) == tezos_contracts.ticketer.as_ref() { + Self::parse_deposit(host, ticket, receiver, context) + } else if enable_fa_deposits { + Self::parse_fa_deposit( + host, + ticket, + receiver, + inbox_level, + inbox_msg_id, + context, + ) + } else { + log!( + host, + Info, + "FA deposit ignored because the feature is disabled" + ); + InputResult::Unparsable + } + } + _ => { + log!( + host, + Info, + "Deposit ignored because of invalid ticketer" + ); + InputResult::Unparsable + } + } } MichelsonOr::Right(MichelsonBytes(bytes)) => { Mode::parse_internal_bytes(source, &bytes, context) @@ -509,6 +569,7 @@ impl InputResult { } } + #[allow(clippy::too_many_arguments)] fn parse_internal( host: &mut Host, message: InternalInboxMessage, @@ -516,6 +577,8 @@ impl InputResult { tezos_contracts: &TezosContracts, context: &mut Mode::Context, level: u32, + msg_id: u32, + enable_fa_deposits: bool, ) -> Self { match message { InternalInboxMessage::InfoPerLevel(info) => { @@ -527,6 +590,9 @@ impl InputResult { smart_rollup_address, tezos_contracts, context, + level, + msg_id, + enable_fa_deposits, ), _ => InputResult::Unparsable, } @@ -538,6 +604,7 @@ impl InputResult { smart_rollup_address: [u8; 20], tezos_contracts: &TezosContracts, context: &mut Mode::Context, + enable_fa_deposits: bool, ) -> Self { let bytes = Message::as_ref(&input); let (input_tag, remaining) = parsable!(bytes.split_first()); @@ -548,6 +615,12 @@ impl InputResult { if *input_tag == EVM_NODE_DELAYED_INPUT_TAG { let mut delayed_inbox = DelayedInbox::new(host).unwrap(); if let Ok(transaction) = Transaction::from_rlp_bytes(remaining) { + if !enable_fa_deposits { + if let TransactionContent::FaDeposit(_) = &transaction.content { + log!(host, Info, "Skipping delayed FA deposit because the FA bridge feature is off"); + return InputResult::Unparsable; + } + } delayed_inbox .save_transaction(host, transaction, 0.into(), 0u32) .unwrap(); @@ -567,6 +640,8 @@ impl InputResult { tezos_contracts, context, input.level, + input.id, + enable_fa_deposits, ), }, Err(_) => InputResult::Unparsable, @@ -600,6 +675,7 @@ mod tests { kernel_security_governance: None, }, &mut (), + false, ), InputResult::Unparsable ) diff --git a/etherlink/kernel_evm/kernel/src/stage_one.rs b/etherlink/kernel_evm/kernel/src/stage_one.rs index 0cee85049ad4..5a18a2f6ccd4 100644 --- a/etherlink/kernel_evm/kernel/src/stage_one.rs +++ b/etherlink/kernel_evm/kernel/src/stage_one.rs @@ -23,10 +23,14 @@ pub fn fetch_proxy_blueprints( host: &mut Host, smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], tezos_contracts: &TezosContracts, + enable_fa_deposits: bool, ) -> Result { - if let Some(ProxyInboxContent { transactions }) = - read_proxy_inbox(host, smart_rollup_address, tezos_contracts)? - { + if let Some(ProxyInboxContent { transactions }) = read_proxy_inbox( + host, + smart_rollup_address, + tezos_contracts, + enable_fa_deposits, + )? { let timestamp = current_timestamp(host); let blueprint = Blueprint { transactions, @@ -88,6 +92,7 @@ fn fetch_delayed_transactions( Ok(()) } +#[allow(clippy::too_many_arguments)] fn fetch_sequencer_blueprints( host: &mut Host, smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], @@ -96,6 +101,7 @@ fn fetch_sequencer_blueprints( delayed_inbox: &mut DelayedInbox, sequencer: PublicKey, _enable_dal: bool, + enable_fa_deposits: bool, ) -> Result { match read_sequencer_inbox( host, @@ -104,6 +110,7 @@ fn fetch_sequencer_blueprints( delayed_bridge, sequencer, delayed_inbox, + enable_fa_deposits, )? { StageOneStatus::Done => { // Check if there are timed-out transactions in the delayed inbox @@ -144,10 +151,14 @@ pub fn fetch_blueprints( delayed_inbox, sequencer.clone(), *enable_dal, + config.enable_fa_bridge, + ), + ConfigurationMode::Proxy => fetch_proxy_blueprints( + host, + smart_rollup_address, + &config.tezos_contracts, + config.enable_fa_bridge, ), - ConfigurationMode::Proxy => { - fetch_proxy_blueprints(host, smart_rollup_address, &config.tezos_contracts) - } } } @@ -426,8 +437,13 @@ mod tests { )); let mut conf = dummy_sequencer_config(enable_dal); - match read_proxy_inbox(&mut host, DEFAULT_SR_ADDRESS, &conf.tezos_contracts) - .unwrap() + match read_proxy_inbox( + &mut host, + DEFAULT_SR_ADDRESS, + &conf.tezos_contracts, + false, + ) + .unwrap() { None => panic!("There should be an InboxContent"), Some(ProxyInboxContent { transactions, .. }) => assert_eq!( diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index 70601bca7ec0..0322c4d9ba24 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -146,7 +146,7 @@ pub const ENABLE_DAL: RefPath = RefPath::assert_from(b"/evm/feature_flags/enable const TRACER_INPUT: RefPath = RefPath::assert_from(b"/evm/trace/input"); // If this path contains a value, the fa bridge is enabled in the kernel. -const ENABLE_FA_BRIDGE: RefPath = +pub const ENABLE_FA_BRIDGE: RefPath = RefPath::assert_from(b"/evm/feature_flags/enable_fa_bridge"); // If the flag is set, the kernel consider that this is local evm node execution. diff --git a/etherlink/kernel_evm/kernel/src/tick_model.rs b/etherlink/kernel_evm/kernel/src/tick_model.rs index b41021bf7fe1..4b21d8c14506 100644 --- a/etherlink/kernel_evm/kernel/src/tick_model.rs +++ b/etherlink/kernel_evm/kernel/src/tick_model.rs @@ -139,7 +139,7 @@ pub fn ticks_of_valid_transaction( } // Ticks are already spent during the validation of the transaction (see // apply.rs). - Deposit(_) => resulting_ticks, + Deposit(_) | FaDeposit(_) => resulting_ticks, } } diff --git a/etherlink/tezos_contracts/fa_bridge/ticket_router_tester.tz b/etherlink/tezos_contracts/fa_bridge/ticket_router_tester.tz new file mode 100644 index 000000000000..3c6331f7b305 --- /dev/null +++ b/etherlink/tezos_contracts/fa_bridge/ticket_router_tester.tz @@ -0,0 +1,99 @@ +{ parameter + (or (nat %deposit) + (or (pair %mint (pair %content nat (option bytes)) (nat %amount)) + (or (pair %withdraw (address %receiver) (ticket %ticket (pair nat (option bytes)))) + (or (ticket %default (pair nat (option bytes))) + (pair %set + (address %target) + (or %entrypoint + (unit %default) + (or (address %routerWithdraw) (bytes %rollupDeposit))) + (mutez %xtz_amount)))))) ; + storage + (pair (pair %internal_call + (address %target) + (or %entrypoint + (unit %default) + (or (address %routerWithdraw) (bytes %rollupDeposit))) + (mutez %xtz_amount)) + (big_map %metadata string bytes)) ; + code { LAMBDA + (pair (ticket (pair nat (option bytes))) + (pair address (or unit (or address bytes)) mutez) + (big_map string bytes)) + operation + { UNPAIR ; + SWAP ; + CAR ; + UNPAIR 3 ; + SWAP ; + IF_LEFT + { DROP ; + CONTRACT (ticket (pair nat (option bytes))) ; + IF_NONE { PUSH string "FAILED_TO_GET_TKT_ENTRYPOINT" ; FAILWITH } {} ; + SWAP ; + DIG 2 ; + TRANSFER_TOKENS } + { IF_LEFT + { SWAP ; + CONTRACT %withdraw + (pair (address %receiver) (ticket %ticket (pair nat (option bytes)))) ; + IF_NONE { PUSH string "ROUTER_ENTRYPOINT_NOT_FOUND" ; FAILWITH } {} ; + DIG 2 ; + DIG 3 ; + DIG 3 ; + PAIR ; + TRANSFER_TOKENS } + { DIG 3 ; + SWAP ; + PAIR ; + LEFT bytes ; + LEFT bytes ; + SWAP ; + CONTRACT + (or (or (pair %deposit (bytes %routing_info) (ticket %ticket (pair nat (option bytes)))) + (bytes %b)) + (bytes %c)) ; + IF_NONE { PUSH string "ROLLUP_DEPOSIT_NOT_FOUND" ; FAILWITH } {} ; + DUG 2 ; + TRANSFER_TOKENS } } } ; + SWAP ; + UNPAIR ; + IF_LEFT + { DIG 2 ; DROP 2 ; NIL operation } + { IF_LEFT + { UNPAIR ; + TICKET ; + IF_NONE { PUSH string "TKT_CREATION_FAILED" ; FAILWITH } {} ; + DUP 2 ; + NIL operation ; + DIG 3 ; + DIG 3 ; + PAIR ; + DIG 3 ; + SWAP ; + EXEC ; + CONS } + { IF_LEFT + { DUP 2 ; + NIL operation ; + DIG 3 ; + DIG 3 ; + CDR ; + PAIR ; + DIG 3 ; + SWAP ; + EXEC ; + CONS } + { IF_LEFT + { DUP 2 ; + NIL operation ; + DIG 3 ; + DIG 3 ; + PAIR ; + DIG 3 ; + SWAP ; + EXEC ; + CONS } + { DIG 2 ; DROP ; UPDATE 1 ; NIL operation } } } } ; + PAIR } } diff --git a/etherlink/tezt/lib/contract_path.ml b/etherlink/tezt/lib/contract_path.ml index 43fcfe8ad72b..6f6ec2d9e404 100644 --- a/etherlink/tezt/lib/contract_path.ml +++ b/etherlink/tezt/lib/contract_path.ml @@ -19,3 +19,8 @@ let withdrawal_abi_path () = let delayed_path () = Base.( project_root // "etherlink/tezos_contracts/delayed_transaction_bridge.tz") + +let ticket_router_tester_path () = + Base.( + project_root + // "etherlink/tezos_contracts/fa_bridge/ticket_router_tester.tz") diff --git a/etherlink/tezt/lib/durable_storage_path.ml b/etherlink/tezt/lib/durable_storage_path.ml index 5f53889ca252..f78280c88cce 100644 --- a/etherlink/tezt/lib/durable_storage_path.ml +++ b/etherlink/tezt/lib/durable_storage_path.ml @@ -87,6 +87,16 @@ let reveal_config = "/__tmp/reveal_config" let enable_fa_bridge = evm "/feature_flags/enable_fa_bridge" +module Ticket_table = struct + let ticket_table = + sf + "%s/ticket_table" + (eth_account "0x0000000000000000000000000000000000000000") + + let balance ~ticket_hash ~account = + String.concat "/" [ticket_table; ticket_hash; account] +end + module Ghostnet = struct let eth_accounts = evm "/eth_accounts" diff --git a/etherlink/tezt/lib/durable_storage_path.mli b/etherlink/tezt/lib/durable_storage_path.mli index 5c23a2e13f08..157e9df9ee99 100644 --- a/etherlink/tezt/lib/durable_storage_path.mli +++ b/etherlink/tezt/lib/durable_storage_path.mli @@ -104,6 +104,12 @@ val reveal_config : path (** [enable_fa_bridge] is the path to the feature flag to activate the FA bridge. *) val enable_fa_bridge : path +module Ticket_table : sig + (** [balance ~ticket_hash ~account] returns the path where the balance of + [account] of ticket [ticket_hash] is. *) + val balance : ticket_hash:path -> account:path -> path +end + module Ghostnet : sig val eth_accounts : path diff --git a/etherlink/tezt/lib/evm_node.ml b/etherlink/tezt/lib/evm_node.ml index 4729d1480bd7..524e443f2370 100644 --- a/etherlink/tezt/lib/evm_node.ml +++ b/etherlink/tezt/lib/evm_node.ml @@ -308,11 +308,12 @@ let wait_for_retrying_connect ?timeout evm_node = wait_for_event ?timeout evm_node ~event:"retrying_connect.v0" @@ Fun.const (Some ()) -type delayed_transaction_kind = Deposit | Transaction +type delayed_transaction_kind = Deposit | Transaction | FaDeposit let delayed_transaction_kind_of_string = function | "transaction" -> Transaction | "deposit" -> Deposit + | "fa_deposit" -> FaDeposit | s -> Test.fail "%s is neither a transaction or deposit" s let wait_for_rollup_node_follower_connection_acquired ?timeout evm_node = diff --git a/etherlink/tezt/lib/evm_node.mli b/etherlink/tezt/lib/evm_node.mli index f3af0de8703d..f88d513473a4 100644 --- a/etherlink/tezt/lib/evm_node.mli +++ b/etherlink/tezt/lib/evm_node.mli @@ -232,7 +232,7 @@ val terminate : ?timeout:float -> t -> unit Lwt.t (** The same exact behavior as {!Sc_rollup_node.wait_for} but for the EVM node. *) val wait_for : ?where:string -> t -> string -> (JSON.t -> 'a option) -> 'a Lwt.t -type delayed_transaction_kind = Deposit | Transaction +type delayed_transaction_kind = Deposit | Transaction | FaDeposit type 'a evm_event_kind = | Kernel_upgrade : (string * Client.Time.t) evm_event_kind diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index d5b48750d31e..e367763834f6 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -69,6 +69,7 @@ type l1_contracts = { bridge : string; admin : string; sequencer_governance : string; + ticket_router_tester : string; } type sequencer_setup = { @@ -139,8 +140,29 @@ let setup_l1_contracts ?(dictator = Constant.bootstrap2) client = client in let* () = Client.bake_for_and_wait ~keys:[] client in + (* Originates the ticket router tester (FA bridge) contract. *) + let* ticket_router_tester = + Client.originate_contract + ~alias:"ticket-router-tester" + ~amount:Tez.zero + ~src:Constant.bootstrap4.public_key_hash + ~init: + "Pair (Pair 0x01000000000000000000000000000000000000000000 (Pair (Left \ + Unit) 0)) {}" + ~prg:(ticket_router_tester_path ()) + ~burn_cap:Tez.one + client + in + let* () = Client.bake_for_and_wait ~keys:[] client in return - {delayed_transaction_bridge; exchanger; bridge; admin; sequencer_governance} + { + delayed_transaction_bridge; + exchanger; + bridge; + admin; + sequencer_governance; + ticket_router_tester; + } let setup_sequencer ~mainnet_compat ?genesis_timestamp ?time_between_blocks ?max_blueprints_lag ?max_blueprints_ahead ?max_blueprints_catchup @@ -371,6 +393,32 @@ let send_deposit_to_delayed_inbox ~amount ~l1_contracts ~depositor ~receiver let* _ = next_rollup_node_level ~sc_rollup_node ~client in unit +let send_fa_deposit_to_delayed_inbox ~amount ~l1_contracts ~depositor ~receiver + ~sc_rollup_node ~sc_rollup_address client = + let* () = + Client.transfer + ~entrypoint:"set" + ~arg:(sf "Pair %S (Pair (Right (Right %s)) 0)" sc_rollup_address receiver) + ~amount:Tez.zero + ~giver:depositor.Account.public_key_hash + ~receiver:l1_contracts.ticket_router_tester + ~burn_cap:Tez.one + client + in + let* () = Client.bake_for_and_wait ~keys:[] client in + let* () = + Client.transfer + ~entrypoint:"mint" + ~arg:(sf "Pair (Pair 0 None) %d" amount) + ~amount:Tez.zero + ~giver:depositor.Account.public_key_hash + ~receiver:l1_contracts.ticket_router_tester + ~burn_cap:Tez.one + client + in + let* _ = next_rollup_node_level ~sc_rollup_node ~client in + unit + let register_test ~mainnet_compat ?genesis_timestamp ?time_between_blocks ?max_blueprints_lag ?max_blueprints_ahead ?max_blueprints_catchup ?catchup_cooldown ?delayed_inbox_timeout ?delayed_inbox_min_levels @@ -1049,7 +1097,7 @@ let check_delayed_inbox_is_empty ~sc_rollup_node = ~key:Durable_storage_path.delayed_inbox () in - Check.((List.length subkeys = 1) int) + Check.((List.length subkeys <= 1) int) ~error_msg:"Expected no elements in the delayed inbox" ; unit @@ -1221,6 +1269,189 @@ let test_delayed_deposit_is_included = ~error_msg:"Expected a bigger balance" ; unit +let encode_data json codec = + let* hex_string = Codec.encode ~name:codec json in + let hex = `Hex (Durable_storage_path.no_0x hex_string) in + return (Hex.to_bytes hex) + +let ticket_hash ticketer token_id = + let* ticketer_bytes = + encode_data (Ezjsonm.string ticketer) "alpha.contract" + in + let* content_bytes = + encode_data + (Ezjsonm.from_string + (sf + "{\"prim\": \"Pair\", \"args\": [{\"int\": \"%d\"}, {\"prim\": \ + \"None\"}]}" + token_id)) + "alpha.script.expr" + in + let payload = Bytes.concat Bytes.empty [ticketer_bytes; content_bytes] in + let payload_digest = Tezos_crypto.Hacl.Hash.Keccak_256.digest payload in + return (payload_digest |> Hex.of_bytes |> Hex.show) + +let ticket_balance ~ticket_hash ~account endpoint = + let account = + account |> Durable_storage_path.no_0x |> String.lowercase_ascii + in + let* ticket_balance = + let key = Durable_storage_path.Ticket_table.balance ~ticket_hash ~account in + match endpoint with + | Either.Left sc_rollup_node -> + Sc_rollup_node.RPC.call + sc_rollup_node + ~rpc_hooks:Tezos_regression.rpc_hooks + @@ Sc_rollup_rpc.get_global_block_durable_state_value + ~pvm_kind:"wasm_2_0_0" + ~operation:Sc_rollup_rpc.Value + ~key + () + | Either.Right evm_node -> + let*@ v = Rpc.state_value evm_node key in + return v + in + return + @@ Option.fold + ~none:0 + ~some:(fun ticket_balance -> + `Hex ticket_balance |> Hex.to_string |> Z.of_bits |> Z.to_int) + ticket_balance + +let test_delayed_fa_deposit_is_included = + register_both + ~da_fee:arb_da_fee_for_delayed_inbox + ~tags: + [ + "evm"; "sequencer"; "delayed_inbox"; "inclusion"; "fa_deposit"; "enabled"; + ] + ~title:"Delayed FA deposit is included" + ~enable_fa_bridge:true + ~kernels:[Kernel.Latest] + ~additional_uses:[Constant.octez_codec] + @@ fun { + client; + l1_contracts; + sc_rollup_address; + sc_rollup_node; + sequencer; + proxy; + _; + } + _protocol -> + (* let endpoint = Evm_node.endpoint sequencer in *) + let amount = 42 in + let depositor = Constant.bootstrap5 in + let receiver = "0x1074Fd1EC02cbeaa5A90450505cF3B48D834f3EB" in + + let* () = + send_fa_deposit_to_delayed_inbox + ~amount + ~l1_contracts + ~depositor + ~receiver + ~sc_rollup_node + ~sc_rollup_address + client + in + let* () = + wait_for_delayed_inbox_add_tx_and_injected + ~sequencer + ~sc_rollup_node + ~client + in + let* () = bake_until_sync ~sc_rollup_node ~proxy ~sequencer ~client () in + let* () = check_delayed_inbox_is_empty ~sc_rollup_node in + + let* zero_ticket_hash = ticket_hash l1_contracts.ticket_router_tester 0 in + + let* ticket_balance_via_rollup_node = + ticket_balance + ~ticket_hash:zero_ticket_hash + ~account:receiver + (Either.Left sc_rollup_node) + in + Check.((amount = ticket_balance_via_rollup_node) int) + ~error_msg: + "After deposit we expect %L ticket balance in the rollup node, got %R" ; + let* ticket_balance_via_sequencer = + ticket_balance + ~ticket_hash:zero_ticket_hash + ~account:receiver + (Either.Right sequencer) + in + Check.((amount = ticket_balance_via_sequencer) int) + ~error_msg: + "After deposit we expect %L ticket balance in the sequencer, got %R" ; + unit + +let test_delayed_fa_deposit_is_ignored_if_feature_disabled = + register_both + ~da_fee:arb_da_fee_for_delayed_inbox + ~tags: + [ + "evm"; + "sequencer"; + "delayed_inbox"; + "inclusion"; + "fa_deposit"; + "disabled"; + ] + ~title:"Delayed FA deposit is ignored if bridge feature is disabled" + ~enable_fa_bridge:false + ~kernels:[Kernel.Latest] + ~additional_uses:[Constant.octez_codec] + @@ fun { + client; + l1_contracts; + sc_rollup_address; + sc_rollup_node; + sequencer; + proxy; + _; + } + _protocol -> + (* let endpoint = Evm_node.endpoint sequencer in *) + let amount = 42 in + let depositor = Constant.bootstrap5 in + let receiver = "0x1074Fd1EC02cbeaa5A90450505cF3B48D834f3EB" in + + let* () = + send_fa_deposit_to_delayed_inbox + ~amount + ~l1_contracts + ~depositor + ~receiver + ~sc_rollup_node + ~sc_rollup_address + client + in + let* () = check_delayed_inbox_is_empty ~sc_rollup_node in + + let* () = bake_until_sync ~sc_rollup_node ~proxy ~sequencer ~client () in + + let* zero_ticket_hash = ticket_hash l1_contracts.ticket_router_tester 0 in + + let* ticket_balance_via_rollup_node = + ticket_balance + ~ticket_hash:zero_ticket_hash + ~account:receiver + (Either.Left sc_rollup_node) + in + Check.((0 = ticket_balance_via_rollup_node) int) + ~error_msg: + "The deposit should have been refused by rollup node, but balance is %R" ; + let* ticket_balance_via_sequencer = + ticket_balance + ~ticket_hash:zero_ticket_hash + ~account:receiver + (Either.Right sequencer) + in + Check.((0 = ticket_balance_via_sequencer) int) + ~error_msg: + "The deposit should have been refused by sequencer, but balance is %R" ; + unit + let test_delayed_deposit_from_init_rollup_node = register_both ~da_fee:arb_da_fee_for_delayed_inbox @@ -4042,6 +4273,8 @@ let () = test_extended_block_param protocols ; test_delayed_transfer_is_included protocols ; test_delayed_deposit_is_included protocols ; + test_delayed_fa_deposit_is_included protocols ; + test_delayed_fa_deposit_is_ignored_if_feature_disabled protocols ; test_largest_delayed_transfer_is_included protocols ; test_delayed_deposit_from_init_rollup_node protocols ; test_init_from_rollup_node_data_dir protocols ; -- GitLab