From 94aba8bed4ff93252702367b4597a6ad6dc5f479 Mon Sep 17 00:00:00 2001 From: Michael Zaikin Date: Tue, 2 Jul 2024 17:32:36 +0100 Subject: [PATCH] EVM/Kernel: add event logs for native token deposits --- etherlink/CHANGES_KERNEL.md | 1 + etherlink/kernel_evm/Cargo.lock | 2 + .../evm_execution/src/fa_bridge/deposit.rs | 12 +- .../kernel_evm/evm_execution/src/handler.rs | 36 +- .../evm_execution/src/precompiles/modexp.rs | 4 +- .../src/precompiles/withdrawal.rs | 57 ++- etherlink/kernel_evm/kernel/Cargo.toml | 2 + etherlink/kernel_evm/kernel/src/apply.rs | 57 +-- .../kernel/src/block_in_progress.rs | 13 +- etherlink/kernel_evm/kernel/src/bridge.rs | 324 ++++++++++++++++++ .../kernel_evm/kernel/src/delayed_inbox.rs | 3 +- etherlink/kernel_evm/kernel/src/inbox.rs | 57 +-- etherlink/kernel_evm/kernel/src/lib.rs | 1 + .../kernel_evm/kernel/src/linked_list.rs | 25 +- etherlink/kernel_evm/kernel/src/parsing.rs | 55 ++- etherlink/kernel_evm/kernel/src/simulation.rs | 15 +- etherlink/kernel_evm/kernel/src/storage.rs | 35 +- etherlink/tezt/tests/dune | 1 + etherlink/tezt/tests/evm_sequencer.ml | 144 +++++++- manifest/product_etherlink.ml | 1 + opam/tezt-etherlink.opam | 1 + 21 files changed, 640 insertions(+), 206 deletions(-) create mode 100644 etherlink/kernel_evm/kernel/src/bridge.rs diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index d35c0655f11b..e45a39acb8b2 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -47,6 +47,7 @@ repository, but is part of [`etherlink-mainnet-launch`][mainnet-branch] instead. - FA deposits are applied if FA bridge feature is enabled (disabled on Mainnet Beta and Testnet). (!13835) - Native token withdrawals emit event log in case of successful execution. (!14014) +- Native token deposits emit EVM event log. (!14012) ## Bug fixes diff --git a/etherlink/kernel_evm/Cargo.lock b/etherlink/kernel_evm/Cargo.lock index f39c87548fd7..9f054c8ddd08 100644 --- a/etherlink/kernel_evm/Cargo.lock +++ b/etherlink/kernel_evm/Cargo.lock @@ -764,6 +764,8 @@ dependencies = [ name = "evm_kernel" version = "0.1.0" dependencies = [ + "alloy-primitives", + "alloy-sol-types", "anyhow", "bytes", "ethbloom", diff --git a/etherlink/kernel_evm/evm_execution/src/fa_bridge/deposit.rs b/etherlink/kernel_evm/evm_execution/src/fa_bridge/deposit.rs index dfe9cd5a4f30..da282d641cac 100644 --- a/etherlink/kernel_evm/evm_execution/src/fa_bridge/deposit.rs +++ b/etherlink/kernel_evm/evm_execution/src/fa_bridge/deposit.rs @@ -43,11 +43,11 @@ use super::error::FaBridgeError; /// Keccak256 of deposit(address,uint256,uint256), first 4 bytes /// This is function selector: https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector -pub const DEPOSIT_METHOD_ID: &[u8; 4] = b"\x0e\xfe\x6a\x8b"; +pub const FA_PROXY_DEPOSIT_METHOD_ID: &[u8; 4] = b"\x0e\xfe\x6a\x8b"; /// Keccak256 of Deposit(uint256,address,address,uint256,uint256,uint256) /// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events -pub const DEPOSIT_EVENT_TOPIC: &[u8; 32] = b"\ +pub const FA_DEPOSIT_EVENT_TOPIC: &[u8; 32] = b"\ \x7e\xe7\xa1\xde\x9c\x18\xce\x69\x5c\x95\xb8\xb1\x9f\xbd\xf2\x6c\ \xce\x35\x44\xe3\xca\x9e\x08\xc9\xf4\x87\x77\x67\x83\xd7\x59\x9f"; @@ -98,7 +98,7 @@ impl FaDeposit { /// Signature: deposit(address,uint256,uint256) pub fn calldata(&self) -> Vec { let mut call_data = Vec::with_capacity(100); - call_data.extend_from_slice(DEPOSIT_METHOD_ID); + call_data.extend_from_slice(FA_PROXY_DEPOSIT_METHOD_ID); call_data.extend_from_slice(&ABI_H160_LEFT_PADDING); call_data.extend_from_slice(&self.receiver.0); @@ -147,7 +147,7 @@ impl FaDeposit { // Emitted by the "system" contract address: H160::zero(), // Event ID (non-anonymous) and indexed fields - topics: vec![H256(*DEPOSIT_EVENT_TOPIC), self.ticket_hash], + topics: vec![H256(*FA_DEPOSIT_EVENT_TOPIC), self.ticket_hash], // Non-indexed fields data, } @@ -313,7 +313,7 @@ mod tests { }; assert_eq!( - DEPOSIT_METHOD_ID.to_vec(), + FA_PROXY_DEPOSIT_METHOD_ID.to_vec(), Keccak256::digest(b"deposit(address,uint256,uint256)").to_vec()[..4] ); @@ -352,7 +352,7 @@ mod tests { ); assert_eq!( - DEPOSIT_EVENT_TOPIC.to_vec(), + FA_DEPOSIT_EVENT_TOPIC.to_vec(), Keccak256::digest( b"Deposit(uint256,address,address,uint256,uint256,uint256)" ) diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index 2935c5e6f964..03aba60947bb 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -406,7 +406,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// Record the cost of a static-cost opcode pub fn record_cost(&mut self, cost: u64) -> Result<(), ExitError> { let Some(layer) = self.transaction_data.last_mut() else { - return Err(ExitError::Other(Cow::from("Recording cost, but there is no transaction in progress"))) + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); }; layer @@ -419,7 +421,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// Record code deposit. Pay per byte for a CREATE operation pub fn record_deposit(&mut self, len: usize) -> Result<(), ExitError> { let Some(layer) = self.transaction_data.last_mut() else { - return Err(ExitError::Other(Cow::from("Recording cost, but there is no transaction in progress"))) + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); }; layer @@ -436,7 +440,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { memory_cost: Option, ) -> Result<(), ExitError> { let Some(layer) = self.transaction_data.last_mut() else { - return Err(ExitError::Other(Cow::from("Recording cost, but there is no transaction in progress"))) + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); }; layer @@ -452,7 +458,11 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// implement this functionality. fn record_stipend(&mut self, stipend: u64) -> Result<(), EthereumError> { let Some(layer) = self.transaction_data.last_mut() else { - return Err(EthereumError::InconsistentTransactionStack(self.transaction_data.len(), false, false)) + return Err(EthereumError::InconsistentTransactionStack( + self.transaction_data.len(), + false, + false, + )); }; layer @@ -512,7 +522,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// Check if some location in durable storage is hot fn is_storage_hot(&self, address: H160, index: H256) -> Result { let Some(layer) = self.transaction_data.last() else { - return Err(ExitError::Other(Cow::from("Invalid transaction data stack for is_storage_hot"))) + return Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for is_storage_hot", + ))); }; Ok(layer.accessed_storage_keys.contains_storage(address, index)) @@ -521,8 +533,10 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { /// Check if address is hot fn is_address_hot(&self, address: H160) -> Result { let Some(layer) = self.transaction_data.last() else { - return Err(ExitError::Other(Cow::from("Invalid transaction data stack for is_address_hot"))) - }; + return Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for is_address_hot", + ))); + }; Ok(layer.accessed_storage_keys.contains_address(address)) } @@ -613,7 +627,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { gas_limit: Option, effective_gas_price: U256, ) -> Result { - let Some(gas_limit) = gas_limit else { return Ok(true) }; + let Some(gas_limit) = gas_limit else { + return Ok(true); + }; let amount = U256::from(gas_limit) .checked_mul(effective_gas_price) @@ -637,7 +653,9 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { unused_gas: Option, effective_gas_price: U256, ) -> Result<(), EthereumError> { - let Some(unused_gas) = unused_gas else { return Ok(()) }; + let Some(unused_gas) = unused_gas else { + return Ok(()); + }; let amount = U256::from(unused_gas) .checked_mul(effective_gas_price) diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/modexp.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/modexp.rs index 7d87d455f79a..8162be4898df 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/modexp.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/modexp.rs @@ -121,7 +121,9 @@ pub fn modexp_precompile( // cast exponent length to usize, it does not make sense to handle larger values. let Ok(exp_len) = usize::try_from(exp_len) else { - return Ok(modexp_mod_overflow_exit("exponent length: modexp mod overflow")); + return Ok(modexp_mod_overflow_exit( + "exponent length: modexp mod overflow", + )); }; // Used to extract ADJUSTED_EXPONENT_LENGTH. diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs index 37d4c66e3ff5..cd9fe46dcc66 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/withdrawal.rs @@ -100,8 +100,12 @@ pub fn withdrawal_precompile( } let Some(transfer) = transfer else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: no transfer"); - return Ok(revert_withdrawal()) + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: no transfer" + ); + return Ok(revert_withdrawal()); }; if transfer.target != WITHDRAWAL_ADDRESS @@ -116,13 +120,21 @@ pub fn withdrawal_precompile( // "cda4fee2" is the function selector for `withdraw_base58(string)` [0xcd, 0xa4, 0xfe, 0xe2, input_data @ ..] => { let Some(address_str) = abi::string_parameter(input_data, 0) else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: unable to get address argument"); - return Ok(revert_withdrawal()) + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: unable to get address argument" + ); + return Ok(revert_withdrawal()); }; let Some(target) = Contract::from_b58check(address_str).ok() else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: invalid target address string"); - return Ok(revert_withdrawal()) + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: invalid target address string" + ); + return Ok(revert_withdrawal()); }; let amount = match mutez_from_wei(transfer.value) { @@ -154,17 +166,26 @@ pub fn withdrawal_precompile( ); let Some(ticketer) = read_ticketer(handler.borrow_host()) else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: failed to read ticketer"); - return Ok(revert_withdrawal()) + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to read ticketer" + ); + return Ok(revert_withdrawal()); }; let Some(ticket) = FA2_1Ticket::new( Contract::Originated(ticketer.clone()), MichelsonPair(0.into(), MichelsonOption(None)), amount, - ).ok() else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: ticket amount is invalid"); - return Ok(revert_withdrawal()) + ) + .ok() else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: ticket amount is invalid" + ); + return Ok(revert_withdrawal()); }; let withdrawal_id = withdraw_nonce::get_and_increment(handler.borrow_host()) @@ -181,12 +202,14 @@ pub fn withdrawal_precompile( ticket, ); - let Some(message) = prepare_message( - parameters, - Contract::Originated(ticketer), - Some("burn"), - ) else { - log!(handler.borrow_host(), Info, "Withdrawal precompiled contract: failed to encode outbox message"); + let Some(message) = + prepare_message(parameters, Contract::Originated(ticketer), Some("burn")) + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to encode outbox message" + ); return Ok(revert_withdrawal()); }; diff --git a/etherlink/kernel_evm/kernel/Cargo.toml b/etherlink/kernel_evm/kernel/Cargo.toml index d44f4eb9477f..12ac36f064b6 100644 --- a/etherlink/kernel_evm/kernel/Cargo.toml +++ b/etherlink/kernel_evm/kernel/Cargo.toml @@ -67,6 +67,8 @@ getrandom = { version = "=0.2.15", features = ["custom"] } pretty_assertions.workspace = true evm-execution = { workspace = true, features = ["fa_bridge_testing"] } +alloy-sol-types.workspace = true +alloy-primitives.workspace = true [features] default = ["panic-hook"] diff --git a/etherlink/kernel_evm/kernel/src/apply.rs b/etherlink/kernel_evm/kernel/src/apply.rs index ef21d758127b..2726b7b78ac8 100644 --- a/etherlink/kernel_evm/kernel/src/apply.rs +++ b/etherlink/kernel_evm/kernel/src/apply.rs @@ -6,11 +6,7 @@ // // SPDX-License-Identifier: MIT -use alloc::borrow::Cow; -use evm::{ExitError, ExitReason, ExitSucceed}; -use evm_execution::account_storage::{ - account_path, EthereumAccount, EthereumAccountStorage, -}; +use evm_execution::account_storage::{EthereumAccount, EthereumAccountStorage}; use evm_execution::fa_bridge::deposit::FaDeposit; use evm_execution::fa_bridge::execute_fa_deposit; use evm_execution::handler::{ExecutionOutcome, ExtendedExitReason, RouterInterface}; @@ -28,12 +24,13 @@ use tezos_smart_rollup::outbox::{OutboxMessage, OutboxQueue}; use tezos_smart_rollup_host::path::{Path, RefPath}; use tezos_smart_rollup_host::runtime::Runtime; +use crate::bridge::{execute_deposit, Deposit}; use crate::configuration::Limits; use crate::error::Error; use crate::fees::{tx_execution_gas_limit, FeeUpdates}; -use crate::inbox::{Deposit, Transaction, TransactionContent}; +use crate::inbox::{Transaction, TransactionContent}; use crate::storage::index_account; -use crate::{tick_model, CONFIG}; +use crate::CONFIG; // This implementation of `Transaction` is used to share the logic of // transaction receipt and transaction object making. The functions @@ -283,10 +280,11 @@ fn is_valid_ethereum_transaction_common( } // check that enough gas is provided to cover fees - let Ok(gas_limit) = tx_execution_gas_limit(transaction, &block_constant.block_fees, is_delayed) + let Ok(gas_limit) = + tx_execution_gas_limit(transaction, &block_constant.block_fees, is_delayed) else { log!(host, Benchmarking, "Transaction status: ERROR_GAS_FEE."); - return Ok(Validity::InvalidNotEnoughGasForFees) + return Ok(Validity::InvalidNotEnoughGasForFees); }; // Gas limit max @@ -419,45 +417,14 @@ fn apply_deposit( evm_account_storage: &mut EthereumAccountStorage, deposit: &Deposit, ) -> Result, Error> { - let Deposit { amount, receiver } = deposit; - - let mut do_deposit = |()| -> Option<()> { - let mut to_account = evm_account_storage - .get_or_create(host, &account_path(receiver).ok()?) - .ok()?; - to_account.balance_add(host, *amount).ok() - }; - - let is_success = do_deposit(()).is_some(); - - let reason = if is_success { - ExitReason::Succeed(ExitSucceed::Returned) - } else { - ExitReason::Error(ExitError::Other(Cow::from("Deposit failed"))) - }; - - let gas_used = CONFIG.gas_transaction_call; - - // TODO: https://gitlab.com/tezos/tezos/-/issues/6551 - let estimated_ticks_used = tick_model::constants::TICKS_FOR_DEPOSIT; - - let execution_outcome = ExecutionOutcome { - gas_used, - reason: reason.into(), - new_address: None, - logs: vec![], - result: None, - withdrawals: vec![], - estimated_ticks_used, - }; - - let caller = H160::zero(); + let execution_outcome = execute_deposit(host, evm_account_storage, deposit, CONFIG) + .map_err(Error::InvalidRunTransaction)?; Ok(ExecutionResult::Valid(TransactionResult { - caller, + caller: H160::zero(), + gas_used: execution_outcome.gas_used.into(), + estimated_ticks_used: execution_outcome.estimated_ticks_used, execution_outcome: Some(execution_outcome), - gas_used: gas_used.into(), - estimated_ticks_used, })) } diff --git a/etherlink/kernel_evm/kernel/src/block_in_progress.rs b/etherlink/kernel_evm/kernel/src/block_in_progress.rs index 4acc758309df..7d94159ee445 100644 --- a/etherlink/kernel_evm/kernel/src/block_in_progress.rs +++ b/etherlink/kernel_evm/kernel/src/block_in_progress.rs @@ -446,7 +446,8 @@ impl BlockInProgress { mod tests { use super::BlockInProgress; - use crate::inbox::{Deposit, Transaction, TransactionContent}; + use crate::bridge::Deposit; + use crate::inbox::{Transaction, TransactionContent}; use primitive_types::{H160, H256, U256}; use rlp::{Decodable, Encodable, Rlp}; use tezos_ethereum::{ @@ -492,6 +493,8 @@ mod tests { let deposit = Deposit { amount: U256::from(i), receiver: H160::from([i; 20]), + inbox_level: 1, + inbox_msg_id: 0, }; Transaction { tx_hash: [i; TRANSACTION_HASH_SIZE], @@ -550,9 +553,9 @@ mod tests { }; let encoded = bip.rlp_bytes(); - let expected = "f901f02af878f83aa00101010101010101010101010101010101010101010101010101010101010101d802d601940101010101010101010101010101010101010101f83aa00808080808080808080808080808080808080808080808080808080808080808d802d608940808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a009090909090909090909090909090909090909090909090909090909090909090304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080880000000000000000"; + let expected = "f901f42af87cf83ca00101010101010101010101010101010101010101010101010101010101010101da02d8019401010101010101010101010101010101010101010180f83ca00808080808080808080808080808080808080808080808080808080808080808da02d8089408080808080808080808080808080808080808080180f842a00202020202020202020202020202020202020202020202020202020202020202a009090909090909090909090909090909090909090909090909090909090909090304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080880000000000000000"; - assert_eq!(hex::encode(encoded), expected); + pretty_assertions::assert_str_eq!(hex::encode(encoded), expected); let bytes = hex::decode(expected).expect("Should be valid hex string"); let decoder = Rlp::new(&bytes); @@ -584,9 +587,9 @@ mod tests { }; let encoded = bip.rlp_bytes(); - let expected = "f902272af8aff871a00101010101010101010101010101010101010101010101010101010101010101f84e01b84bf84901010180018026a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f83aa00808080808080808080808080808080808080808080808080808080808080808d802d608940808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a009090909090909090909090909090909090909090909090909090909090909090304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004880000000000000000"; + let expected = "f902292af8b1f871a00101010101010101010101010101010101010101010101010101010101010101f84e01b84bf84901010180018026a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f83ca00808080808080808080808080808080808080808080808080808080808080808da02d8089408080808080808080808080808080808080808080180f842a00202020202020202020202020202020202020202020202020202020202020202a009090909090909090909090909090909090909090909090909090909090909090304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004880000000000000000"; - assert_eq!(hex::encode(encoded), expected); + pretty_assertions::assert_str_eq!(hex::encode(encoded), expected); let bytes = hex::decode(expected).expect("Should be valid hex string"); let decoder = Rlp::new(&bytes); diff --git a/etherlink/kernel_evm/kernel/src/bridge.rs b/etherlink/kernel_evm/kernel/src/bridge.rs new file mode 100644 index 000000000000..4ccc682917a6 --- /dev/null +++ b/etherlink/kernel_evm/kernel/src/bridge.rs @@ -0,0 +1,324 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Native token (TEZ) bridge primitives and helpers. + +use std::borrow::Cow; + +use ethereum::Log; +use evm::{Config, ExitError, ExitReason, ExitSucceed}; +use evm_execution::{ + abi::{ABI_H160_LEFT_PADDING, ABI_U32_LEFT_PADDING}, + account_storage::{account_path, AccountStorageError, EthereumAccountStorage}, + handler::ExecutionOutcome, + EthereumError, +}; +use primitive_types::{H160, H256, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpEncodable}; +use sha3::{Digest, Keccak256}; +use tezos_ethereum::{ + rlp_helpers::{decode_field, next}, + wei::eth_from_mutez, +}; +use tezos_evm_logging::{log, Level::Info}; +use tezos_smart_rollup::{ + host::Runtime, + michelson::{ticket::FA2_1Ticket, MichelsonBytes}, +}; + +use crate::tick_model; + +/// Keccak256 of Deposit(uint256,address,uint256,uint256) +/// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events +pub const DEPOSIT_EVENT_TOPIC: [u8; 32] = [ + 211, 106, 47, 103, 208, 109, 40, 87, 134, 246, 26, 50, 176, 82, 185, 172, 230, 176, + 183, 171, 239, 81, 119, 181, 67, 88, 171, 220, 131, 160, 182, 155, +]; + +/// Native token bridge error +#[derive(Debug, thiserror::Error)] +pub enum BridgeError { + #[error("Invalid deposit receiver address: {0:?}")] + InvalidDepositReceiver(Vec), +} + +/// Native token deposit +#[derive(Debug, PartialEq, Clone, RlpEncodable)] +pub struct Deposit { + /// Deposit amount in wei + pub amount: U256, + /// Deposit receiver on L2 + pub receiver: H160, + /// Inbox level containing the original deposit message + pub inbox_level: u32, + /// Inbox message id + pub inbox_msg_id: u32, +} + +impl Deposit { + /// Parses a deposit from a ticket transfer (internal inbox message). + /// The "entrypoint" type is pair of ticket (FA2.1) and bytes (receiver address). + pub fn try_parse( + ticket: FA2_1Ticket, + receiver: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + ) -> Result { + // Amount + let (_sign, amount_bytes) = ticket.amount().to_bytes_le(); + // We use the `U256::from_little_endian` as it takes arbitrary long + // bytes. Afterward it's transform to `u64` to use `eth_from_mutez`, it's + // obviously safe as we deposit CTEZ and the amount is limited by + // the XTZ quantity. + let amount_mutez: u64 = U256::from_little_endian(&amount_bytes).as_u64(); + let amount: U256 = eth_from_mutez(amount_mutez); + + // EVM address + let receiver_bytes = receiver.0; + if receiver_bytes.len() != std::mem::size_of::() { + return Err(BridgeError::InvalidDepositReceiver(receiver_bytes)); + } + let receiver = H160::from_slice(&receiver_bytes); + + Ok(Self { + amount, + receiver, + inbox_level, + inbox_msg_id, + }) + } + + /// Returns log structure for an implicit deposit event. + /// + /// This event is added to the outer transaction receipt, + /// so that we can index successful deposits and update status. + /// + /// Signature: Deposit(uint256,address,uint256,uint256) + pub fn event_log(&self) -> Log { + let mut data = Vec::with_capacity(4 * 32); + + data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(&self.receiver.0); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_level.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_msg_id.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + Log { + // Emitted by the "system" contract + address: H160::zero(), + // Event ID (non-anonymous) and indexed fields + topics: vec![H256(DEPOSIT_EVENT_TOPIC)], + // Non-indexed fields + data, + } + } + + /// Returns unique deposit digest that can be used as hash for the + /// pseudo transaction. + pub fn hash(&self, seed: &[u8]) -> H256 { + let mut hasher = Keccak256::new(); + hasher.update(&self.rlp_bytes()); + hasher.update(seed); + H256(hasher.finalize().into()) + } + + pub fn display(&self) -> String { + format!("Deposit {} to {}", self.amount, self.receiver) + } +} + +impl Decodable for Deposit { + /// Decode deposit from RLP bytes in a retro-compatible manner. + /// If it is a legacy deposit it will have zero inbox level and message ID. + /// + /// NOTE that [Deposit::hash] would give the same results for "legacy" deposits, + /// but since decoding is used only for items that are already in delayed inbox this is OK: + /// the hash is calculated for items that are to be submitted to delayed inbox. + fn decode(decoder: &Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + match decoder.item_count()? { + 2 => { + let mut it = decoder.iter(); + let amount: U256 = decode_field(&next(&mut it)?, "amount")?; + let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; + Ok(Self { + amount, + receiver, + inbox_level: 0, + inbox_msg_id: 0, + }) + } + 4 => { + let mut it = decoder.iter(); + let amount: U256 = decode_field(&next(&mut it)?, "amount")?; + let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; + let inbox_level: u32 = decode_field(&next(&mut it)?, "inbox_level")?; + let inbox_msg_id: u32 = decode_field(&next(&mut it)?, "inbox_msg_id")?; + Ok(Self { + amount, + receiver, + inbox_level, + inbox_msg_id, + }) + } + _ => Err(DecoderError::RlpIncorrectListLen), + } + } +} + +pub fn execute_deposit( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + deposit: &Deposit, + config: Config, +) -> Result { + // We should be able to obtain an account for arbitrary H160 address + // otherwise it is a fatal error. + let to_account_path = + account_path(&deposit.receiver).map_err(AccountStorageError::from)?; + let mut to_account = evm_account_storage.get_or_create(host, &to_account_path)?; + + let exit_reason = match to_account.balance_add(host, deposit.amount) { + Ok(()) => ExitReason::Succeed(ExitSucceed::Returned), + Err(err) => { + log!(host, Info, "Deposit failed with {:?}", err); + ExitReason::Error(ExitError::Other(Cow::from("Deposit failed"))) + } + }; + + let logs = if exit_reason.is_succeed() { + vec![deposit.event_log()] + } else { + vec![] + }; + + // TODO: estimate how emitting an event influenced tick consumption + let gas_used = config.gas_transaction_call; + let estimated_ticks_used = tick_model::constants::TICKS_FOR_DEPOSIT; + + let execution_outcome = ExecutionOutcome { + gas_used, + reason: exit_reason.into(), + new_address: None, + logs, + result: None, + withdrawals: vec![], + estimated_ticks_used, + }; + Ok(execution_outcome) +} + +#[cfg(test)] +mod tests { + use alloy_sol_types::SolEvent; + use evm_execution::account_storage::init_account_storage; + use primitive_types::{H160, U256}; + use rlp::Decodable; + use tezos_smart_rollup_mock::MockHost; + + use crate::{bridge::DEPOSIT_EVENT_TOPIC, CONFIG}; + + use super::{execute_deposit, Deposit}; + + mod events { + alloy_sol_types::sol! { + event Deposit ( + uint256 amount, + address receiver, + uint256 inboxLevel, + uint256 inboxMsgId + ); + } + } + + fn dummy_deposit() -> Deposit { + Deposit { + amount: 1.into(), + receiver: H160([2u8; 20]), + inbox_level: 3, + inbox_msg_id: 4, + } + } + + #[test] + fn deposit_event_topic() { + assert_eq!(events::Deposit::SIGNATURE_HASH.0, DEPOSIT_EVENT_TOPIC); + } + + #[test] + fn deposit_decode_legacy() { + let mut stream = rlp::RlpStream::new_list(2); + stream.append(&U256::one()).append(&H160([1u8; 20])); + let bytes = stream.out().to_vec(); + let decoder = rlp::Rlp::new(&bytes); + let res = Deposit::decode(&decoder).unwrap(); + assert_eq!( + res, + Deposit { + amount: U256::one(), + receiver: H160([1u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } + ); + } + + #[test] + fn deposit_execution_outcome_contains_event() { + let mut host = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let deposit = dummy_deposit(); + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(outcome.is_success()); + assert_eq!(outcome.logs.len(), 1); + + let log_data = alloy_primitives::LogData::new_unchecked( + outcome.logs[0].topics.iter().map(|x| x.0.into()).collect(), + outcome.logs[0].data.clone().into(), + ); + let event = events::Deposit::decode_log_data(&log_data, true).unwrap(); + assert_eq!(event.amount, alloy_primitives::U256::from(1)); + assert_eq!( + event.receiver, + alloy_primitives::Address::from_slice(&[2u8; 20]) + ); + assert_eq!(event.inboxLevel, alloy_primitives::U256::from(3)); + assert_eq!(event.inboxMsgId, alloy_primitives::U256::from(4)); + } + + #[test] + fn deposit_execution_fails_due_to_balance_overflow() { + let mut host = MockHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let mut deposit = dummy_deposit(); + deposit.amount = U256::MAX; + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(outcome.is_success()); + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(!outcome.is_success()); + assert!(outcome.logs.is_empty()); + } +} diff --git a/etherlink/kernel_evm/kernel/src/delayed_inbox.rs b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs index 6f90f97bfb6b..f054ba892058 100644 --- a/etherlink/kernel_evm/kernel/src/delayed_inbox.rs +++ b/etherlink/kernel_evm/kernel/src/delayed_inbox.rs @@ -2,9 +2,10 @@ // SPDX-FileCopyrightText: 2024 Trilitech use crate::{ + bridge::Deposit, current_timestamp, event::Event, - inbox::{Deposit, Transaction, TransactionContent}, + inbox::{Transaction, TransactionContent}, linked_list::LinkedList, storage, }; diff --git a/etherlink/kernel_evm/kernel/src/inbox.rs b/etherlink/kernel_evm/kernel/src/inbox.rs index 226f1f1c0a9d..bcacbd651282 100644 --- a/etherlink/kernel_evm/kernel/src/inbox.rs +++ b/etherlink/kernel_evm/kernel/src/inbox.rs @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT use crate::blueprint_storage::store_sequencer_blueprint; +use crate::bridge::Deposit; use crate::configuration::{fetch_limits, DalConfiguration, TezosContracts}; use crate::dal::fetch_and_parse_sequencer_blueprints_from_dal; use crate::delayed_inbox::DelayedInbox; @@ -16,10 +17,9 @@ use crate::parsing::{ use crate::storage::{ chunked_hash_transaction_path, chunked_transaction_num_chunks, - chunked_transaction_path, clear_events, create_chunked_transaction, - get_and_increment_deposit_nonce, read_l1_level, read_last_info_per_level_timestamp, - remove_chunked_transaction, remove_sequencer, store_l1_level, - store_last_info_per_level_timestamp, store_transaction_chunk, + chunked_transaction_path, clear_events, create_chunked_transaction, read_l1_level, + read_last_info_per_level_timestamp, remove_chunked_transaction, remove_sequencer, + store_l1_level, store_last_info_per_level_timestamp, store_transaction_chunk, }; use crate::tick_model::constants::TICKS_FOR_BLUEPRINT_INTERCEPT; use crate::tick_model::maximum_ticks_for_sequencer_chunk; @@ -27,7 +27,6 @@ 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}; use tezos_crypto_rs::hash::ContractKt1Hash; @@ -40,36 +39,6 @@ use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_encoding::public_key::PublicKey; use tezos_smart_rollup_host::runtime::Runtime; -#[derive(Debug, PartialEq, Clone)] -pub struct Deposit { - pub amount: U256, - pub receiver: H160, -} - -impl Encodable for Deposit { - fn rlp_append(&self, stream: &mut rlp::RlpStream) { - stream.begin_list(2); - stream.append(&self.amount); - stream.append(&self.receiver); - } -} - -impl Decodable for Deposit { - fn decode(decoder: &rlp::Rlp) -> Result { - if !decoder.is_list() { - return Err(DecoderError::RlpExpectedToBeList); - } - if decoder.item_count()? != 2 { - return Err(DecoderError::RlpIncorrectListLen); - } - - let mut it = decoder.iter(); - let amount: U256 = decode_field(&next(&mut it)?, "amount")?; - let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; - Ok(Deposit { amount, receiver }) - } -} - #[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq, Clone)] pub enum TransactionContent { @@ -429,22 +398,8 @@ fn handle_deposit( host: &mut Host, deposit: Deposit, ) -> Result { - let deposit_nonce = get_and_increment_deposit_nonce(host)?; - - let mut buffer_amount = [0; 32]; - deposit.amount.to_little_endian(&mut buffer_amount); - - let mut to_hash = vec![]; - to_hash.extend_from_slice(&buffer_amount); - to_hash.extend_from_slice(&deposit.receiver.to_fixed_bytes()); - to_hash.extend_from_slice(&deposit_nonce.to_le_bytes()); - - let kec = Keccak256::digest(to_hash); - let tx_hash = kec - .as_slice() - .try_into() - .map_err(|_| Error::InvalidConversion)?; - + let seed = host.reveal_metadata().raw_rollup_address; + let tx_hash = deposit.hash(&seed).into(); Ok(Transaction { tx_hash, content: TransactionContent::Deposit(deposit), diff --git a/etherlink/kernel_evm/kernel/src/lib.rs b/etherlink/kernel_evm/kernel/src/lib.rs index c700f33f8ac3..7f40e1b8e082 100644 --- a/etherlink/kernel_evm/kernel/src/lib.rs +++ b/etherlink/kernel_evm/kernel/src/lib.rs @@ -44,6 +44,7 @@ mod block; mod block_in_progress; mod blueprint; mod blueprint_storage; +mod bridge; mod configuration; mod dal; mod delayed_inbox; diff --git a/etherlink/kernel_evm/kernel/src/linked_list.rs b/etherlink/kernel_evm/kernel/src/linked_list.rs index 56ddf142774b..d8f8e5ba4219 100644 --- a/etherlink/kernel_evm/kernel/src/linked_list.rs +++ b/etherlink/kernel_evm/kernel/src/linked_list.rs @@ -326,16 +326,23 @@ where /// /// Returns None if the element is not present pub fn find(&self, host: &impl Runtime, id: &Id) -> Result> { - let Some::>(pointer) = Pointer::read(host, &self.path, id)? else {return Ok(None)}; + let Some::>(pointer) = Pointer::read(host, &self.path, id)? + else { + return Ok(None); + }; storage::read_optional_rlp(host, &pointer.data_path(&self.path)?) } /// Removes and returns the element at position index within the vector. pub fn remove(&mut self, host: &mut impl Runtime, id: &Id) -> Result> { // Check if the list is empty - let Some(LinkedListPointer { front, back }) = &self.pointers else {return Ok(None)}; + let Some(LinkedListPointer { front, back }) = &self.pointers else { + return Ok(None); + }; // Get the previous and the next pointer - let Some(pointer) = Pointer::read(host, &self.path, id)? else {return Ok(None)}; + let Some(pointer) = Pointer::read(host, &self.path, id)? else { + return Ok(None); + }; let previous = match pointer.previous { Some(ref previous) => Pointer::read(host, &self.path, previous)?, None => None, @@ -404,20 +411,26 @@ where /// Returns the first element of the list /// or `None` if it is empty. pub fn first(&self, host: &impl Runtime) -> Result> { - let Some(LinkedListPointer { front, .. }) = &self.pointers else {return Ok(None)}; + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; Ok(Some(front.get_data(host, &self.path)?)) } /// Returns the first element of the list alongside its id /// or `None` if it is empty. pub fn first_with_id(&self, host: &impl Runtime) -> Result> { - let Some(LinkedListPointer { front, .. }) = &self.pointers else {return Ok(None)}; + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; Ok(Some((front.id.clone(), front.get_data(host, &self.path)?))) } /// Removes the first element of the list and returns it pub fn pop_first(&mut self, host: &mut impl Runtime) -> Result> { - let Some(LinkedListPointer { front, .. }) = &self.pointers else {return Ok(None)}; + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; let to_remove = front.id.clone(); self.remove(host, &to_remove) } diff --git a/etherlink/kernel_evm/kernel/src/parsing.rs b/etherlink/kernel_evm/kernel/src/parsing.rs index 93f5249a2aa0..0985792a0442 100644 --- a/etherlink/kernel_evm/kernel/src/parsing.rs +++ b/etherlink/kernel_evm/kernel/src/parsing.rs @@ -13,14 +13,14 @@ use crate::tick_model::constants::{ TICKS_PER_DEPOSIT_PARSING, }; use crate::{ - inbox::{Deposit, Transaction, TransactionContent}, + bridge::Deposit, + inbox::{Transaction, TransactionContent}, sequencer_blueprint::{SequencerBlueprint, UnsignedSequencerBlueprint}, 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}; use tezos_crypto_rs::{hash::ContractKt1Hash, PublicKeySignatureVerifier}; @@ -28,7 +28,6 @@ use tezos_ethereum::{ rlp_helpers::FromRlpBytes, transaction::{TransactionHash, TRANSACTION_HASH_SIZE}, tx_common::EthereumTransactionCommon, - wei::eth_from_mutez, }; use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_encoding::{ @@ -448,7 +447,7 @@ impl InputResult { log!( host, Debug, - "Deposit ignored because of parsing errors: {}", + "FA deposit ignored because of parsing errors: {}", err ); InputResult::Unparsable @@ -460,35 +459,28 @@ impl InputResult { host: &mut Host, ticket: FA2_1Ticket, receiver: 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_deposit(context); - // Amount - let (_sign, amount_bytes) = ticket.amount().to_bytes_le(); - // We use the `U256::from_little_endian` as it takes arbitrary long - // bytes. Afterward it's transform to `u64` to use `eth_from_mutez`, it's - // obviously safe as we deposit CTEZ and the amount is limited by - // the XTZ quantity. - let amount: u64 = U256::from_little_endian(&amount_bytes).as_u64(); - let amount: U256 = eth_from_mutez(amount); - - // EVM address - let receiver_bytes = receiver.0; - if receiver_bytes.len() != std::mem::size_of::() { - log!( - host, - Info, - "Deposit ignored because of invalid receiver address" - ); - return InputResult::Unparsable; + match Deposit::try_parse(ticket, receiver, inbox_level, inbox_msg_id) { + Ok(deposit) => { + log!(host, Info, "Parsed from input: {}", deposit.display()); + Self::Input(Input::Deposit(deposit)) + } + Err(err) => { + log!( + host, + Info, + "Deposit ignored because of parsing errors: {}", + err + ); + Self::Unparsable + } } - let receiver = H160::from_slice(&receiver_bytes); - - let content = Deposit { amount, receiver }; - log!(host, Info, "Deposit of {} to {}.", amount, receiver); - Self::Input(Input::Deposit(content)) } #[allow(clippy::too_many_arguments)] @@ -519,7 +511,14 @@ impl InputResult { match &ticket.creator().0 { Contract::Originated(kt1) => { if Some(kt1) == tezos_contracts.ticketer.as_ref() { - Self::parse_deposit(host, ticket, receiver, context) + Self::parse_deposit( + host, + ticket, + receiver, + inbox_level, + inbox_msg_id, + context, + ) } else if enable_fa_deposits { Self::parse_fa_deposit( host, diff --git a/etherlink/kernel_evm/kernel/src/simulation.rs b/etherlink/kernel_evm/kernel/src/simulation.rs index fa5400861c40..c74d6a9f90a0 100644 --- a/etherlink/kernel_evm/kernel/src/simulation.rs +++ b/etherlink/kernel_evm/kernel/src/simulation.rs @@ -543,7 +543,8 @@ impl TxValidation { tx_data_size, ); - let Ok(gas_limit) = tx_execution_gas_limit(transaction, &block_fees, false) else { + let Ok(gas_limit) = tx_execution_gas_limit(transaction, &block_fees, false) + else { return Self::to_error(GAS_LIMIT_TOO_LOW); }; @@ -607,7 +608,9 @@ impl TxValidation { let tx = &self.transaction; let evm_account_storage = account_storage::init_account_storage()?; // Get the caller - let Ok(caller) = tx.caller() else {return Self::to_error(INCORRECT_SIGNATURE)}; + let Ok(caller) = tx.caller() else { + return Self::to_error(INCORRECT_SIGNATURE); + }; // Get the caller account let caller_account_path = evm_execution::account_storage::account_path(&caller)?; let caller_account = evm_account_storage.get(host, &caller_account_path)?; @@ -656,8 +659,12 @@ impl TryFrom<&[u8]> for Message { type Error = DecoderError; fn try_from(bytes: &[u8]) -> Result { - let Some(&tag) = bytes.first() else {return Err(DecoderError::Custom("Empty simulation message"))}; - let Some(bytes) = bytes.get(1..) else {return Err(DecoderError::Custom("Empty simulation message"))}; + let Some(&tag) = bytes.first() else { + return Err(DecoderError::Custom("Empty simulation message")); + }; + let Some(bytes) = bytes.get(1..) else { + return Err(DecoderError::Custom("Empty simulation message")); + }; match tag { EVALUATION_TAG => Evaluation::from_bytes(bytes).map(Message::Evaluation), diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index 0fa270b54b50..29ee684a5524 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -9,7 +9,7 @@ use crate::block_in_progress::BlockInProgress; use crate::event::Event; use crate::simulation::SimulationResult; use anyhow::Context; -use evm_execution::account_storage::{AccountStorageError, EthereumAccount}; +use evm_execution::account_storage::EthereumAccount; use evm_execution::storage::blocks::add_new_block_hash; use evm_execution::trace::TracerInput; use num_derive::{FromPrimitive, ToPrimitive}; @@ -132,8 +132,6 @@ const EVM_INFO_PER_LEVEL_STATS_TOTAL: RefPath = pub const SIMULATION_RESULT: RefPath = RefPath::assert_from(b"/evm/simulation_result"); -pub const DEPOSIT_NONCE: RefPath = RefPath::assert_from(b"/evm/deposit_nonce"); - /// Path where all indexes are stored. pub const EVM_INDEXES: RefPath = RefPath::assert_from(b"/evm/world_state/indexes"); @@ -723,7 +721,8 @@ pub fn read_burned_fees(host: &mut impl Runtime) -> U256 { pub fn read_sequencer_pool_address(host: &impl Runtime) -> Option { let mut bytes = [0; std::mem::size_of::()]; - let Ok(20) = host.store_read_slice(&SEQUENCER_POOL_PATH, 0, bytes.as_mut_slice()) else { + let Ok(20) = host.store_read_slice(&SEQUENCER_POOL_PATH, 0, bytes.as_mut_slice()) + else { log!(host, Debug, "No sequencer pool address set"); return None; }; @@ -887,26 +886,6 @@ pub fn read_maximum_gas_per_transaction(host: &mut Host) -> Optio read_u64(host, &MAXIMUM_GAS_PER_TRANSACTION).ok() } -pub fn get_and_increment_deposit_nonce( - host: &mut Host, -) -> Result { - let current_nonce = || -> Option { - let bytes = host.store_read_all(&DEPOSIT_NONCE).ok()?; - let slice_of_bytes: [u8; 4] = bytes[..] - .try_into() - .map_err(|_| Error::InvalidConversion) - .ok()?; - Some(u32::from_le_bytes(slice_of_bytes)) - }; - - let nonce = current_nonce().unwrap_or(0u32); - let new_nonce = nonce - .checked_add(1) - .ok_or(AccountStorageError::NonceOverflow)?; - host.store_write_all(&DEPOSIT_NONCE, &new_nonce.to_le_bytes())?; - Ok(nonce) -} - pub fn index_block( host: &mut impl Runtime, block_hash: &H256, @@ -1027,8 +1006,12 @@ pub fn delete_block_in_progress(host: &mut Host) -> anyhow::Resul pub fn sequencer(host: &Host) -> anyhow::Result> { if host.store_has(&SEQUENCER)?.is_some() { let bytes = host.store_read_all(&SEQUENCER)?; - let Ok(tz1_b58) = String::from_utf8(bytes) else { return Ok(None) }; - let Ok(tz1) = PublicKey::from_b58check(&tz1_b58) else { return Ok(None)}; + let Ok(tz1_b58) = String::from_utf8(bytes) else { + return Ok(None); + }; + let Ok(tz1) = PublicKey::from_b58check(&tz1_b58) else { + return Ok(None); + }; Ok(Some(tz1)) } else { Ok(None) diff --git a/etherlink/tezt/tests/dune b/etherlink/tezt/tests/dune index a730d7e22ad1..36fda9452988 100644 --- a/etherlink/tezt/tests/dune +++ b/etherlink/tezt/tests/dune @@ -10,6 +10,7 @@ octez-libs.tezt-wrapper tezt-tezos tezt_etherlink + octez-evm-node-libs.evm_node_lib_dev_encoding tezos-protocol-alpha.protocol) (preprocess (staged_pps ppx_import ppx_deriving.show)) (library_flags (:standard -linkall)) diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 4c00092f732d..739405612bb1 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -1180,13 +1180,36 @@ let test_send_deposit_to_delayed_inbox = ~key:"/evm/delayed-inbox" () in - Check.( - list_mem - string - "a07feb67aff94089c8d944f5f8ffb5acc37306da9102fc310264e90999a42eb1" - delayed_transactions_hashes) - ~error_msg:"the deposit is not present in the delayed inbox" ; - unit + let* deposit = + 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.Value (* taking the first item *) + ~key: + (sf + "/evm/delayed-inbox/%s/data" + (List.hd delayed_transactions_hashes)) + () + in + let deposit_bytes = Hex.to_bytes (`Hex (Option.get deposit)) in + + match Evm_node_lib_dev_encoding.Rlp.decode deposit_bytes with + | Ok + Evm_node_lib_dev_encoding.Rlp.( + List (List (Value tag :: List (Value amnt :: Value rcvr :: _) :: _) :: _)) + -> + let deposit_tag = tag |> Hex.of_bytes |> Hex.show in + Check.((deposit_tag = "02") string) + ~error_msg:"Expected tag %R, but got: %L" ; + let receiver = rcvr |> Hex.of_bytes |> Hex.show in + Check.((receiver = "1074fd1ec02cbeaa5a90450505cf3b48d834f3eb") string) + ~error_msg:"Expected receiver %R, but got: %L" ; + let amount = amnt |> Hex.of_bytes |> Hex.show in + (* 16000000000000000000 big endian *) + Check.((amount = "de0b6b3a76400000") string) + ~error_msg:"Expected amount %R, but got: %L" ; + unit + | _ -> failwith "invalid delayed inbox item" let test_rpc_produceBlock = register_all @@ -2751,6 +2774,112 @@ let test_upgrade_kernel_auto_sync = unit +let test_legacy_deposits_dispatched_after_kernel_upgrade = + let genesis_timestamp = + Client.(At (Time.of_notation_exn "2020-01-01T00:00:00Z")) + in + let activation_timestamp = "2020-01-01T00:00:01Z" in + register_upgrade_all + ~kernels:[Mainnet; Ghostnet] + ~upgrade_to:(fun _ -> Latest) + ~genesis_timestamp + ~time_between_blocks:Nothing + ~tags:["evm"; "sequencer"; "upgrade"; "deposit"; "legacy"; "delayed"] + ~title: + "After kernel upgrade a legacy deposit from delayed inbox can be decoded \ + and processed." + @@ fun from + to_ + { + sc_rollup_node; + l1_contracts; + sc_rollup_address; + client; + sequencer; + proxy; + _; + } + _protocol -> + let* () = + match Kernel.commit_of from with + | Some from_commit -> + let* _ = + check_kernel_version ~evm_node:sequencer ~equal:true from_commit + in + unit + | None -> unit + in + + let* receiver_balance_prev = + Eth_cli.balance + ~account:Eth_account.bootstrap_accounts.(1).address + ~endpoint:(Evm_node.endpoint sequencer) + in + + let* () = + send_deposit_to_delayed_inbox + ~amount:Tez.(of_int 16) + ~l1_contracts + ~depositor:Constant.bootstrap5 + ~receiver:Eth_account.bootstrap_accounts.(1).address + ~sc_rollup_node + ~sc_rollup_address + client + in + + let _, to_use = Kernel.to_uses_and_tags to_ in + let* () = + upgrade + ~sc_rollup_node + ~sc_rollup_address + ~admin:Constant.bootstrap2.public_key_hash + ~admin_contract:l1_contracts.admin + ~client + ~upgrade_to:to_use + ~activation_timestamp + in + + (* The sequencer follows finalized levels of the octez-node, + so we need to have 2 tezos levels before the sequencer sees the upgrade *) + let* _ = + repeat 2 (fun () -> + let* _ = next_rollup_node_level ~sc_rollup_node ~client in + unit) + in + + (* Produce a block where the upgrade would happen *) + let*@ _ = produce_block ~timestamp:"2020-01-01T00:00:10Z" sequencer in + let* () = bake_until_sync ~sc_rollup_node ~proxy ~sequencer ~client () in + + (* Ensure the kernel is upgraded *) + let* () = + match Kernel.commit_of from with + | Some from_commit -> + let* _ = + check_kernel_version ~evm_node:sequencer ~equal:false from_commit + in + unit + | None -> unit + in + + (* Produce a block where deposit would be handled *) + let*@ _ = produce_block ~timestamp:"2020-01-01T00:00:20Z" sequencer in + let* () = bake_until_sync ~sc_rollup_node ~proxy ~sequencer ~client () in + + (* Deposit must be applied by now *) + let* receiver_balance_next = + Eth_cli.balance + ~account:Eth_account.bootstrap_accounts.(1).address + ~endpoint:(Evm_node.endpoint sequencer) + in + Check.((receiver_balance_next > receiver_balance_prev) Wei.typ) + ~error_msg:"Expected a bigger balance" ; + + (* Ensure delayed inbox is empty now *) + let* () = check_delayed_inbox_is_empty ~sc_rollup_node in + + unit + let test_delayed_transfer_timeout = register_all ~delayed_inbox_timeout:3 @@ -4968,6 +5097,7 @@ let () = test_fa_withdrawal_is_included protocols ; test_largest_delayed_transfer_is_included protocols ; test_delayed_deposit_from_init_rollup_node protocols ; + test_legacy_deposits_dispatched_after_kernel_upgrade protocols ; test_init_from_rollup_node_data_dir protocols ; test_init_from_rollup_node_with_delayed_inbox protocols ; test_observer_applies_blueprint protocols ; diff --git a/manifest/product_etherlink.ml b/manifest/product_etherlink.ml index a6d48cceb2dc..d91533c177ac 100644 --- a/manifest/product_etherlink.ml +++ b/manifest/product_etherlink.ml @@ -190,6 +190,7 @@ let _tezt_etherlink = tezt_wrapper |> open_ |> open_ ~m:"Base"; tezt_tezos |> open_ |> open_ ~m:"Runnable.Syntax"; tezt_etherlink |> open_; + evm_node_lib_dev_encoding; Protocol.(main alpha); ] ~with_macos_security_framework:true diff --git a/opam/tezt-etherlink.opam b/opam/tezt-etherlink.opam index d1f412a2b9bc..2208e1ccb35a 100644 --- a/opam/tezt-etherlink.opam +++ b/opam/tezt-etherlink.opam @@ -15,6 +15,7 @@ depends: [ "ppx_import" {with-test} "ppx_deriving" {with-test} "tezt" { with-test & >= "4.1.0" & < "5.0.0" } + "octez-evm-node-libs" { with-test & = version } "tezos-protocol-alpha" {with-test} ] build: [ -- GitLab