diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index 02f0e0f6b5988e28550ad6de437365e12651c421..1205746120f795657dfe9f6cbc9252e0533b8fde 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -2520,6 +2520,8 @@ dependencies = [ "tezos-evm-runtime-latest", "tezos-indexable-storage-latest", "tezos-protocol", + "tezos-smart-rollup", + "tezos-smart-rollup-core", "tezos-smart-rollup-encoding", "tezos-smart-rollup-host", "tezos-smart-rollup-storage", diff --git a/etherlink/kernel_latest/kernel/src/bridge.rs b/etherlink/kernel_latest/kernel/src/bridge.rs index abc820674d2896b5fdcd36305286c904a7ffeff8..10af0011a51096cd3e5681f21307ec197ed7716d 100644 --- a/etherlink/kernel_latest/kernel/src/bridge.rs +++ b/etherlink/kernel_latest/kernel/src/bridge.rs @@ -37,7 +37,6 @@ use tezos_tezlink::operation_result::{ }; use tezos_tracing::trace_kernel; -use crate::tezosx; use crate::tick_model::constants::TICKS_FOR_DEPOSIT; /// Keccak256 of Deposit(uint256,address,uint256,uint256) @@ -397,7 +396,8 @@ pub fn execute_etherlink_deposit( DepositReceiver::Tezos(Contract::Implicit(pkh)) => { let amount = mutez_from_wei(deposit.amount) .map_err(|_| BridgeError::InvalidAmount(deposit.amount))?; - tezosx::add_balance(host, pkh, amount.into()) + revm_etherlink::tezosx::add_balance(host, pkh, amount.into()) + .map_err(|e| revm_etherlink::Error::Custom(e.to_string())) } DepositReceiver::Tezos(Contract::Originated(kt1)) => { return Err(BridgeError::InvalidDepositReceiver( diff --git a/etherlink/kernel_latest/kernel/src/lib.rs b/etherlink/kernel_latest/kernel/src/lib.rs index 9fbad1093e237a4fcbbbc83a138c11cbb4f8e2bd..8fa6f409f4f3959c867ba171ed3b8622a3bda41f 100644 --- a/etherlink/kernel_latest/kernel/src/lib.rs +++ b/etherlink/kernel_latest/kernel/src/lib.rs @@ -70,7 +70,6 @@ mod simulation; mod stage_one; mod storage; mod sub_block; -mod tezosx; mod tick_model; mod transaction; mod upgrade; diff --git a/etherlink/kernel_latest/kernel/src/tezosx.rs b/etherlink/kernel_latest/kernel/src/tezosx.rs deleted file mode 100644 index 6eccf71ff8d74da6838664baedc8cd5b5a683e9e..0000000000000000000000000000000000000000 --- a/etherlink/kernel_latest/kernel/src/tezosx.rs +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Nomadic Labs -// -// SPDX-License-Identifier: MIT - -use mir::ast::PublicKeyHash; -use primitive_types::U256; -use revm_etherlink::tezosx::{ - ethereum_address_from_tezos, set_ethereum_address_mapping, ForeignAddress, -}; -use revm_etherlink::Error; -use rlp::{Decodable, Encodable, Rlp}; -use tezos_ethereum::rlp_helpers::{ - append_u256_le, append_u64_le, decode_field_u256_le, decode_field_u64_le, -}; -use tezos_evm_runtime::runtime::Runtime; -use tezos_smart_rollup::types::PublicKey; -use tezos_smart_rollup_host::path::PathError; -use tezos_smart_rollup_host::{ - path::{concat, OwnedPath, RefPath}, - runtime::RuntimeError, -}; - -// Path where Tezos accounts are stored. -const TEZOS_ACCOUNTS_PATH: RefPath = - RefPath::assert_from(b"/evm/world_state/eth_accounts/tezos"); - -// Path where all the infos of a Tezos contract are stored under the same key. -// This path must contains balance, nonce and optionally a revealed public key. -const INFO_PATH: RefPath = RefPath::assert_from(b"/info"); - -// Used as a value for the durable storage. -#[derive(Debug, Eq, PartialEq, Clone, Default)] -pub struct TezosAccountInfo { - pub balance: U256, - pub nonce: u64, - pub pub_key: Option, -} - -impl Encodable for TezosAccountInfo { - fn rlp_append(&self, s: &mut rlp::RlpStream) { - s.begin_list(3); - append_u256_le(s, &self.balance); - append_u64_le(s, &self.nonce); - match &self.pub_key { - Some(pub_key) => s.append(&pub_key.to_b58check().as_bytes()), - None => s.append_empty_data(), - }; - } -} - -impl Decodable for TezosAccountInfo { - fn decode(rlp: &Rlp) -> Result { - if !rlp.is_list() { - return Err(rlp::DecoderError::RlpExpectedToBeList); - } - if rlp.item_count()? != 3 { - return Err(rlp::DecoderError::RlpIncorrectListLen); - } - let mut it = rlp.iter(); - let balance_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; - let balance = decode_field_u256_le(&balance_decoder, "balance")?; - let nonce_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; - let nonce = decode_field_u64_le(&nonce_decoder, "nonce")?; - let pub_key_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; - let pub_key: Option = if pub_key_decoder.is_empty() { - None - } else { - let vec: Vec = pub_key_decoder.as_val()?; - let s: String = String::from_utf8(vec).map_err(|_| { - rlp::DecoderError::Custom("Invalid public key (not a string)") - })?; - let pub_key = PublicKey::from_b58check(&s).map_err(|_| { - rlp::DecoderError::Custom("Invalid public key (b58check)") - })?; - Some(pub_key) - }; - - Ok(TezosAccountInfo { - balance, - nonce, - pub_key, - }) - } -} - -fn path_to_tezos_account(pub_key_hash: &PublicKeyHash) -> Result { - let address_path: Vec = format!("/{pub_key_hash}").into(); - let address_path = OwnedPath::try_from(address_path)?; - let prefix = concat(&TEZOS_ACCOUNTS_PATH, &address_path)?; - concat(&prefix, &INFO_PATH) -} - -pub fn add_balance( - host: &mut impl Runtime, - pub_key_hash: &PublicKeyHash, - amount: U256, -) -> Result<(), Error> { - let mut info = get_tezos_account_info_or_init(host, pub_key_hash)?; - info.balance = info - .balance - .checked_add(amount) - .ok_or(Error::Custom("Balance overflow".to_string()))?; - set_tezos_account_info(host, pub_key_hash, info) -} - -pub fn get_tezos_account_info( - host: &impl Runtime, - pub_key_hash: &PublicKeyHash, -) -> Result, Error> { - let path = - path_to_tezos_account(pub_key_hash).map_err(|_| RuntimeError::PathNotFound)?; - match host.store_read_all(&path) { - Ok(bytes) => { - let account_info = TezosAccountInfo::decode(&Rlp::new(&bytes)) - .map_err(|_| RuntimeError::DecodingError)?; - Ok(Some(account_info)) - } - Err(RuntimeError::PathNotFound) => Ok(None), - Err(err) => Err(Error::Runtime(err)), - } -} - -pub fn get_tezos_account_info_or_init( - host: &mut impl Runtime, - pub_key_hash: &PublicKeyHash, -) -> Result { - match get_tezos_account_info(host, pub_key_hash)? { - Some(info) => Ok(info), - None => { - let evm_addr = ethereum_address_from_tezos(pub_key_hash); - let foreign_addr = ForeignAddress::Tezos(pub_key_hash.clone()); - set_ethereum_address_mapping(host, &evm_addr, foreign_addr)?; - Ok(TezosAccountInfo::default()) - } - } -} - -pub fn set_tezos_account_info( - host: &mut impl Runtime, - pub_key_hash: &PublicKeyHash, - info: TezosAccountInfo, -) -> Result<(), Error> { - let path = - path_to_tezos_account(pub_key_hash).map_err(|_| RuntimeError::PathNotFound)?; - let value = &info.rlp_bytes(); - Ok(host.store_write(&path, value, 0)?) -} - -#[cfg(test)] -mod tests { - use crate::tezosx::*; - use alloy_primitives::Address; - use revm_etherlink::tezosx::*; - use std::str::FromStr; - use tezos_evm_runtime::runtime::MockKernelHost; - use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; - - #[test] - fn tezos_account_info_size_constant() { - let pub_key: PublicKey = PublicKey::from_b58check( - "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", - ) - .expect("Public key should be a b58 string"); - let account = TezosAccountInfo { - balance: U256::zero(), - nonce: 0, - pub_key: Some(pub_key), - }; - let rlp_size = account.rlp_bytes().len(); - // Reading an account info in one go is safe - assert!(rlp_size < MAX_FILE_CHUNK_SIZE); - } - - #[test] - fn tezos_account_info_encoding() { - let pub_key: PublicKey = PublicKey::from_b58check( - "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", - ) - .expect("Public key should be a b58 string"); - let account = TezosAccountInfo { - balance: U256::from(1234u64), - nonce: 18, - pub_key: Some(pub_key), - }; - let bytes = &account.rlp_bytes(); - let decoded_account = TezosAccountInfo::decode(&Rlp::new(bytes)) - .expect("Account should be decodable"); - assert!(decoded_account == account); - } - - #[test] - fn tezos_account_storage() { - let mut host = MockKernelHost::default(); - - let pub_key_hash: PublicKeyHash = - PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("Public key hash should be a b58 string"); - let pub_key: PublicKey = PublicKey::from_b58check( - "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", - ) - .expect("Public key should be a b58 string"); - let account = TezosAccountInfo { - balance: U256::from(1234u64), - nonce: 18, - pub_key: Some(pub_key.clone()), - }; - - set_tezos_account_info(&mut host, &pub_key_hash, account.clone()) - .expect("Writing to the storage should have worked"); - - let read_account = get_tezos_account_info(&host, &pub_key_hash) - .expect("Reading the storage should have worked") - .expect("The path to the account should exist"); - assert_eq!(account, read_account); - } - - #[test] - fn foreign_address_encoding() { - let pub_key_hash: PublicKeyHash = - PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("Public key hash should be a b58 string"); - let source_address = ForeignAddress::Tezos(pub_key_hash); - let bytes = &source_address.rlp_bytes(); - let decoded_address = ForeignAddress::decode(&Rlp::new(bytes)) - .expect("Address should be decodable"); - assert!(decoded_address == source_address); - } - - #[test] - fn ethereum_address_mapping_storage() { - let mut host = MockKernelHost::default(); - - let address: Address = - Address::from_str("0x2E2Ac8699AD02e710951ea0F56b892Ed36916Cd5") - .expect("Hex should be an EVM address"); - let pub_key_hash: PublicKeyHash = - PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") - .expect("Public key hash should be a b58 string"); - let address_mapping = ForeignAddress::Tezos(pub_key_hash); - - set_ethereum_address_mapping(&mut host, &address, address_mapping.clone()) - .expect("Writing to the storage should have worked"); - - let read_address_mapping = get_ethereum_address_mapping(&host, &address) - .expect("Reading the storage should have worked") - .expect("The path to the account should exist"); - assert_eq!(address_mapping, read_address_mapping); - } -} diff --git a/etherlink/kernel_latest/revm/Cargo.toml b/etherlink/kernel_latest/revm/Cargo.toml index 94d0f4907b6c7169c5c8014bb4019bf4b2686d90..7ef675850e58e06bd53d8e5107c48ed907bbdfff 100644 --- a/etherlink/kernel_latest/revm/Cargo.toml +++ b/etherlink/kernel_latest/revm/Cargo.toml @@ -29,6 +29,8 @@ tezos-evm-runtime.workspace = true tezos-smart-rollup-host.workspace = true tezos-smart-rollup-storage.workspace = true tezos-smart-rollup-encoding.workspace = true +tezos-smart-rollup-core.workspace = true +tezos-smart-rollup.workspace = true tezos_crypto_rs.workspace = true tezos_data_encoding.workspace = true tezos-indexable-storage.workspace = true diff --git a/etherlink/kernel_latest/revm/src/database.rs b/etherlink/kernel_latest/revm/src/database.rs index ad36182c7fb92305310e46a713f355a5d2fa014a..11bf765c2ba928ee58c9a44d1f464983845f6bc7 100644 --- a/etherlink/kernel_latest/revm/src/database.rs +++ b/etherlink/kernel_latest/revm/src/database.rs @@ -5,7 +5,7 @@ use crate::{ custom, - helpers::legacy::FaDepositWithProxy, + helpers::legacy::{alloy_to_u256, FaDepositWithProxy}, journal::PrecompileStateChanges, precompiles::{error::CustomPrecompileError, send_outbox_message::Withdrawal}, storage::{ @@ -27,6 +27,7 @@ use revm::{ use tezos_crypto_rs::{ hash::{ContractKt1Hash, HashTrait}, public_key::PublicKey, + public_key_hash::PublicKeyHash, }; use tezos_ethereum::block::BlockConstants; use tezos_evm_logging::{log, tracing::instrument, Level}; @@ -88,6 +89,12 @@ pub trait DatabasePrecompileStateChanges { deposit_id: &U256, ) -> Result; fn ticketer(&self) -> Result; + fn tezosx_transfer_tez( + &mut self, + source: Address, + destination: &str, + amount: U256, + ) -> Result<(), CustomPrecompileError>; } pub(crate) trait DatabaseCommitPrecompileStateChanges { @@ -240,6 +247,56 @@ impl DatabasePrecompileStateChanges for EtherlinkVMDB<'_, Host> { Err(e) => Err(CustomPrecompileError::from(e)), } } + + fn tezosx_transfer_tez( + &mut self, + source: Address, + destination: &str, + amount: U256, + ) -> Result<(), CustomPrecompileError> { + let mut source_account = StorageAccount::from_address(&source)?; + let mut source_info = source_account.info(self.host)?; + let new_source_balance = + source_info.balance.checked_sub(amount).ok_or_else(|| { + CustomPrecompileError::Revert( + "insufficient balance for transfer to runtime".to_string(), + ) + })?; + let tezos_pub_key_hash = PublicKeyHash::from_b58check(destination).unwrap(); + let mut tezos_destination_account = + crate::tezosx::get_tezos_account_info(self.host, &tezos_pub_key_hash) + .map_err(|e| { + CustomPrecompileError::Revert(format!( + "failed to read destination Tezos account: {e:?}" + )) + })? + .ok_or_else(|| { + CustomPrecompileError::Revert( + "destination Tezos account not found".to_string(), + ) + })?; + tezos_destination_account.balance = tezos_destination_account + .balance + .checked_add(alloy_to_u256(&amount)) + .ok_or_else(|| { + CustomPrecompileError::Revert( + "overflow in destination balance calculation".to_string(), + ) + })?; + crate::tezosx::set_tezos_account_info( + self.host, + &tezos_pub_key_hash, + tezos_destination_account, + ) + .map_err(|e| { + CustomPrecompileError::Revert(format!( + "failed to update destination Tezos account: {e:?}" + )) + })?; + source_info.balance = new_source_balance; + source_account.set_info(self.host, source_info)?; + Ok(()) + } } impl Database for EtherlinkVMDB<'_, Host> { diff --git a/etherlink/kernel_latest/revm/src/layered_state.rs b/etherlink/kernel_latest/revm/src/layered_state.rs index 4dd6913cb10ecec285f233204bb8cfa2b331ea2d..2f98f5d8f5f4646ecd61be54cb3eac8ba2f9328c 100644 --- a/etherlink/kernel_latest/revm/src/layered_state.rs +++ b/etherlink/kernel_latest/revm/src/layered_state.rs @@ -317,6 +317,15 @@ mod tests { ) -> Result { Ok(false) } + + fn tezosx_transfer_tez( + &mut self, + _source: Address, + _destination: &str, + _amount: U256, + ) -> Result<(), CustomPrecompileError> { + Ok(()) + } } #[test] diff --git a/etherlink/kernel_latest/revm/src/lib.rs b/etherlink/kernel_latest/revm/src/lib.rs index 0df1af77c4ccc1d17835f69f1638eeb674fe7312..f132e1d020ef209283a077e89b3dec57b96411ea 100644 --- a/etherlink/kernel_latest/revm/src/lib.rs +++ b/etherlink/kernel_latest/revm/src/lib.rs @@ -413,6 +413,9 @@ pub fn run_transaction<'a, Host: Runtime>( #[cfg(test)] mod test { + use crate::tezosx::{ + get_tezos_account_info, set_tezos_account_info, TezosAccountInfo, + }; use alloy_sol_types::{ sol, ContractError, Revert, RevertReason, SolEvent, SolInterface, }; @@ -443,7 +446,12 @@ mod test { use super::Error; use crate::helpers::storage::bytes_hash; - use crate::precompiles::constants::FEED_DEPOSIT_ADDR; + use crate::precompiles::constants::{ + FEED_DEPOSIT_ADDR, RUNTIME_GATEWAY_PRECOMPILE_ADDRESS, + }; + use crate::precompiles::runtime_gateway::RuntimeGateway::{ + transferCall, RuntimeGatewayCalls, + }; use crate::storage::code::CodeStorage; use crate::test::utilities::CreateAndRevert::{ createAndRevertCall, CreateAndRevertCalls, @@ -649,6 +657,79 @@ mod test { } } + #[test] + fn test_tezosx_transfer_gateway_to_implicit_address() { + let mut host = MockKernelHost::default(); + let mut block_constants = block_constants_with_no_fees(); + block_constants.tezos_experimental_features = true; + + let caller = Address::from(&[1; 20]); + let destination = Address::from(&[2; 20]); + let destination_pk_hash = + PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx").unwrap(); + set_ethereum_address_mapping( + &mut host, + &destination, + ForeignAddress::Tezos(destination_pk_hash.clone()), + ) + .unwrap(); + set_tezos_account_info( + &mut host, + &destination_pk_hash, + TezosAccountInfo { + balance: primitive_types::U256::from(0), + nonce: 0, + pub_key: None, + }, + ) + .unwrap(); + let value_sent = U256::from(5); + let caller_info = AccountInfo { + balance: U256::MAX, + nonce: 0, + code_hash: Default::default(), + code: None, + }; + let mut caller_account = StorageAccount::from_address(&caller).unwrap(); + caller_account + .set_info_without_code(&mut host, caller_info) + .unwrap(); + + let calldata = RuntimeGatewayCalls::transfer(transferCall { + implicitAddress: destination_pk_hash.to_b58check(), + }) + .abi_encode(); + + let execution_result = run_transaction( + &mut host, + DEFAULT_SPEC_ID, + &block_constants, + None, + caller, + Some(RUNTIME_GATEWAY_PRECOMPILE_ADDRESS), + calldata.into(), + GasData::new(GAS_LIMIT, 0, GAS_LIMIT), + value_sent, + AccessList(vec![]), + None, + None, + false, + ) + .unwrap(); + + // Check the outcome of the transaction + match execution_result.result { + ExecutionResult::Success { .. } => (), + ExecutionResult::Revert { .. } | ExecutionResult::Halt { .. } => { + panic!("Transfer to implicit address should have succeeded") + } + } + let infos = get_tezos_account_info(&host, &destination_pk_hash) + .unwrap() + .unwrap(); + assert_eq!(infos.balance, primitive_types::U256::from(5)); + } + #[test] fn test_contract_call_sload_sstore() { let mut host = MockKernelHost::default(); diff --git a/etherlink/kernel_latest/revm/src/precompiles/constants.rs b/etherlink/kernel_latest/revm/src/precompiles/constants.rs index d38489388e1f6934f626fd4ff0a36261378d2767..12e95399f86a45da378e35747e5b57b341227749 100644 --- a/etherlink/kernel_latest/revm/src/precompiles/constants.rs +++ b/etherlink/kernel_latest/revm/src/precompiles/constants.rs @@ -70,19 +70,26 @@ pub(crate) const CHANGE_SEQUENCER_KEY_PRECOMPILE_ADDRESS: Address = 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, ])); +pub(crate) const RUNTIME_GATEWAY_PRECOMPILE_ADDRESS: Address = + Address(FixedBytes::new([ + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + ])); + #[cfg(test)] pub(crate) const PRECOMPILE_BURN_ADDRESS: Address = Address(FixedBytes::new([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, ])); -pub(crate) const CUSTOMS: [Address; 6] = [ +pub(crate) const CUSTOMS: [Address; 7] = [ WITHDRAWAL_SOL_ADDR, FA_BRIDGE_SOL_ADDR, SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS, TABLE_PRECOMPILE_ADDRESS, GLOBAL_COUNTER_PRECOMPILE_ADDRESS, CHANGE_SEQUENCER_KEY_PRECOMPILE_ADDRESS, + RUNTIME_GATEWAY_PRECOMPILE_ADDRESS, ]; // Rationale regarding the cost: @@ -107,6 +114,9 @@ pub(crate) const GLOBAL_COUNTER_BASE_COST: u64 = 24_200; // cold write 22100 (inserting a non zero value to a zero value). pub(crate) const UPGRADE_SEQUENCER_PRECOMPILE_BASE_COST: u64 = 24_200; +// TODO Define the cost: https://linear.app/tezos/issue/L2-662/define-cost-of-precompile-gateway-on-ethereum +pub(crate) const RUNTIME_GATEWAY_TRANSFER_BASE_COST: u64 = 1_000; + // Rationale regarding the cost: // Consumed gas is ~81000 for both queue execute_without_proxy entrypoints pub const FA_DEPOSIT_EXECUTION_COST: u64 = 100_000; diff --git a/etherlink/kernel_latest/revm/src/precompiles/mod.rs b/etherlink/kernel_latest/revm/src/precompiles/mod.rs index 3c8607a61993c81c4ac1ac8b78494ca5d10759c0..8ebd7cba8b7755ca7daaeeaa39e9295877a91ad9 100644 --- a/etherlink/kernel_latest/revm/src/precompiles/mod.rs +++ b/etherlink/kernel_latest/revm/src/precompiles/mod.rs @@ -7,6 +7,7 @@ pub mod constants; pub mod error; pub mod initializer; pub mod provider; +pub mod runtime_gateway; pub mod send_outbox_message; mod global_counter; diff --git a/etherlink/kernel_latest/revm/src/precompiles/provider.rs b/etherlink/kernel_latest/revm/src/precompiles/provider.rs index d1bcbbe28d18edbb96d75458113e7fc81819d5d4..3dce1d52861197d167b88a5ffa2c0936e59c708a 100644 --- a/etherlink/kernel_latest/revm/src/precompiles/provider.rs +++ b/etherlink/kernel_latest/revm/src/precompiles/provider.rs @@ -17,12 +17,13 @@ use crate::{ change_sequencer_key::change_sequencer_key_precompile, constants::{ CHANGE_SEQUENCER_KEY_PRECOMPILE_ADDRESS, CUSTOMS, - GLOBAL_COUNTER_PRECOMPILE_ADDRESS, SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS, - TABLE_PRECOMPILE_ADDRESS, + GLOBAL_COUNTER_PRECOMPILE_ADDRESS, RUNTIME_GATEWAY_PRECOMPILE_ADDRESS, + SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS, TABLE_PRECOMPILE_ADDRESS, }, error::CustomPrecompileError, global_counter::global_counter_precompile, guard::revert, + runtime_gateway::runtime_gateway_precompile, send_outbox_message::send_outbox_message_precompile, table::table_precompile, }, @@ -84,6 +85,9 @@ impl EtherlinkPrecompiles { CHANGE_SEQUENCER_KEY_PRECOMPILE_ADDRESS => { change_sequencer_key_precompile(&calldata, context, inputs) } + RUNTIME_GATEWAY_PRECOMPILE_ADDRESS => { + runtime_gateway_precompile(&calldata, context, inputs) + } _ => return Ok(None), }; diff --git a/etherlink/kernel_latest/revm/src/precompiles/runtime_gateway.rs b/etherlink/kernel_latest/revm/src/precompiles/runtime_gateway.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0d55d15900367245e9441d8c2ce0579167e85cd --- /dev/null +++ b/etherlink/kernel_latest/revm/src/precompiles/runtime_gateway.rs @@ -0,0 +1,87 @@ +use alloy_sol_types::{sol, SolInterface}; +use revm::{ + context::ContextTr, + interpreter::{CallInputs, Gas, InstructionResult, InterpreterResult}, + primitives::{alloy_primitives::IntoLogData, Bytes, Log}, +}; + +use crate::{ + database::DatabasePrecompileStateChanges, + journal::Journal, + precompiles::{ + constants::{ + RUNTIME_GATEWAY_PRECOMPILE_ADDRESS, RUNTIME_GATEWAY_TRANSFER_BASE_COST, + }, + error::CustomPrecompileError, + guard::out_of_gas, + runtime_gateway::RuntimeGateway::RuntimeGatewayCalls, + }, +}; + +sol! { + contract RuntimeGateway { + function transfer( + string implicitAddress, + ) external; + } + + event TransferEvent( + string implicitAddress, + uint256 amount + ); +} + +pub(crate) fn runtime_gateway_precompile( + calldata: &[u8], + context: &mut CTX, + inputs: &CallInputs, +) -> Result +where + DB: DatabasePrecompileStateChanges, + CTX: ContextTr>, +{ + // TODO: Do we need protection for STATICCALL, DELEGATECALL, CALLCODE? + + let mut gas = Gas::new(inputs.gas_limit); + + let Ok(function_call) = RuntimeGatewayCalls::abi_decode(calldata) else { + return Err(CustomPrecompileError::Revert(String::from( + "invalid input encoding", + ))); + }; + + match function_call { + RuntimeGatewayCalls::transfer(call) => { + if !gas.record_cost(RUNTIME_GATEWAY_TRANSFER_BASE_COST) { + return Ok(out_of_gas(inputs.gas_limit)); + } + + let implicit_address = call.implicitAddress; + let amount = inputs.value.get(); + + // Perform the transfer + context.db_mut().tezosx_transfer_tez( + inputs.caller, + &implicit_address, + amount, + )?; + + // Emit event + let log_data = TransferEvent { + implicitAddress: implicit_address, + amount, + }; + let log = Log { + address: RUNTIME_GATEWAY_PRECOMPILE_ADDRESS, + data: log_data.into_log_data(), + }; + context.journal_mut().log(log); + } + } + + Ok(InterpreterResult { + result: InstructionResult::Return, + gas, + output: Bytes::new(), + }) +} diff --git a/etherlink/kernel_latest/revm/src/tezosx.rs b/etherlink/kernel_latest/revm/src/tezosx.rs index 959d8cb597ee53b0cdb3a37e267718136e751581..a0dae67afd20b59c48c0829351d1fa4e7d5b4e61 100644 --- a/etherlink/kernel_latest/revm/src/tezosx.rs +++ b/etherlink/kernel_latest/revm/src/tezosx.rs @@ -2,10 +2,15 @@ // // SPDX-License-Identifier: MIT +use primitive_types::U256; use revm::primitives::{alloy_primitives::Keccak256, hex::FromHex, Address, Bytes, B256}; use rlp::{Decodable, Encodable, Rlp}; use tezos_crypto_rs::public_key_hash::PublicKeyHash; +use tezos_ethereum::rlp_helpers::{ + append_u256_le, append_u64_le, decode_field_u256_le, decode_field_u64_le, +}; use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::types::PublicKey; use tezos_smart_rollup_host::{ path::{OwnedPath, RefPath}, runtime::RuntimeError, @@ -83,7 +88,6 @@ fn path_to_ethereum_address_mapping(address: &Address) -> Result, +} + +impl Encodable for TezosAccountInfo { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + s.begin_list(3); + append_u256_le(s, &self.balance); + append_u64_le(s, &self.nonce); + match &self.pub_key { + Some(pub_key) => s.append(&pub_key.to_b58check().as_bytes()), + None => s.append_empty_data(), + }; + } +} + +impl Decodable for TezosAccountInfo { + fn decode(rlp: &Rlp) -> Result { + if !rlp.is_list() { + return Err(rlp::DecoderError::RlpExpectedToBeList); + } + if rlp.item_count()? != 3 { + return Err(rlp::DecoderError::RlpIncorrectListLen); + } + let mut it = rlp.iter(); + let balance_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; + let balance = decode_field_u256_le(&balance_decoder, "balance")?; + let nonce_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; + let nonce = decode_field_u64_le(&nonce_decoder, "nonce")?; + let pub_key_decoder = it.next().ok_or(rlp::DecoderError::RlpExpectedToBeList)?; + let pub_key: Option = if pub_key_decoder.is_empty() { + None + } else { + let vec: Vec = pub_key_decoder.as_val()?; + let s: String = String::from_utf8(vec).map_err(|_| { + rlp::DecoderError::Custom("Invalid public key (not a string)") + })?; + let pub_key = PublicKey::from_b58check(&s).map_err(|_| { + rlp::DecoderError::Custom("Invalid public key (b58check)") + })?; + Some(pub_key) + }; + + Ok(TezosAccountInfo { + balance, + nonce, + pub_key, + }) + } +} + +fn path_to_tezos_account(pub_key_hash: &PublicKeyHash) -> Result { + let address_path: Vec = format!("/{pub_key_hash}").into(); + let address_path = + OwnedPath::try_from(address_path).map_err(|e| Error::Custom(e.to_string()))?; + let prefix = concat(&TEZOS_ACCOUNTS_PATH, &address_path)?; + concat(&prefix, &INFO_PATH) +} + +pub fn add_balance( + host: &mut impl Runtime, + pub_key_hash: &PublicKeyHash, + amount: U256, +) -> Result<(), Error> { + let mut info = get_tezos_account_info_or_init(host, pub_key_hash)?; + info.balance = info + .balance + .checked_add(amount) + .ok_or(Error::Custom("Balance overflow".to_string()))?; + set_tezos_account_info(host, pub_key_hash, info) +} + +pub fn get_tezos_account_info( + host: &impl Runtime, + pub_key_hash: &PublicKeyHash, +) -> Result, Error> { + let path = + path_to_tezos_account(pub_key_hash).map_err(|_| RuntimeError::PathNotFound)?; + match host.store_read_all(&path) { + Ok(bytes) => { + let account_info = TezosAccountInfo::decode(&Rlp::new(&bytes)) + .map_err(|_| RuntimeError::DecodingError)?; + Ok(Some(account_info)) + } + Err(RuntimeError::PathNotFound) => Ok(None), + Err(err) => Err(Error::Runtime(err)), + } +} + +pub fn get_tezos_account_info_or_init( + host: &mut impl Runtime, + pub_key_hash: &PublicKeyHash, +) -> Result { + match get_tezos_account_info(host, pub_key_hash)? { + Some(info) => Ok(info), + None => { + let evm_addr = ethereum_address_from_tezos(pub_key_hash); + let foreign_addr = ForeignAddress::Tezos(pub_key_hash.clone()); + set_ethereum_address_mapping(host, &evm_addr, foreign_addr).map_err(|e| { + Error::Custom(format!("Failed to set Ethereum address mapping: {e:?}")) + })?; + Ok(TezosAccountInfo::default()) + } + } +} + +pub fn set_tezos_account_info( + host: &mut impl Runtime, + pub_key_hash: &PublicKeyHash, + info: TezosAccountInfo, +) -> Result<(), Error> { + let path = + path_to_tezos_account(pub_key_hash).map_err(|_| RuntimeError::PathNotFound)?; + let value = &info.rlp_bytes(); + Ok(host.store_write(&path, value, 0)?) +} + +#[cfg(test)] +mod tests { + use crate::tezosx::*; + use std::str::FromStr; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; + + #[test] + fn foreign_address_encoding() { + let pub_key_hash: PublicKeyHash = + PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("Public key hash should be a b58 string"); + let source_address = ForeignAddress::Tezos(pub_key_hash); + let bytes = &source_address.rlp_bytes(); + let decoded_address = ForeignAddress::decode(&Rlp::new(bytes)) + .expect("Address should be decodable"); + assert!(decoded_address == source_address); + } + + #[test] + fn ethereum_address_mapping_storage() { + let mut host = MockKernelHost::default(); + + let address: Address = + Address::from_str("0x2E2Ac8699AD02e710951ea0F56b892Ed36916Cd5") + .expect("Hex should be an EVM address"); + let pub_key_hash: PublicKeyHash = + PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("Public key hash should be a b58 string"); + let address_mapping = ForeignAddress::Tezos(pub_key_hash); + + set_ethereum_address_mapping(&mut host, &address, address_mapping.clone()) + .expect("Writing to the storage should have worked"); + + let read_address_mapping = get_ethereum_address_mapping(&host, &address) + .expect("Reading the storage should have worked") + .expect("The path to the account should exist"); + assert_eq!(address_mapping, read_address_mapping); + } + + #[test] + fn tezos_account_info_size_constant() { + let pub_key: PublicKey = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .expect("Public key should be a b58 string"); + let account = TezosAccountInfo { + balance: U256::zero(), + nonce: 0, + pub_key: Some(pub_key), + }; + let rlp_size = account.rlp_bytes().len(); + // Reading an account info in one go is safe + assert!(rlp_size < MAX_FILE_CHUNK_SIZE); + } + + #[test] + fn tezos_account_info_encoding() { + let pub_key: PublicKey = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .expect("Public key should be a b58 string"); + let account = TezosAccountInfo { + balance: U256::from(1234u64), + nonce: 18, + pub_key: Some(pub_key), + }; + let bytes = &account.rlp_bytes(); + let decoded_account = TezosAccountInfo::decode(&Rlp::new(bytes)) + .expect("Account should be decodable"); + assert!(decoded_account == account); + } + + #[test] + fn tezos_account_storage() { + let mut host = MockKernelHost::default(); + + let pub_key_hash: PublicKeyHash = + PublicKeyHash::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx") + .expect("Public key hash should be a b58 string"); + let pub_key: PublicKey = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .expect("Public key should be a b58 string"); + let account = TezosAccountInfo { + balance: U256::from(1234u64), + nonce: 18, + pub_key: Some(pub_key.clone()), + }; + + set_tezos_account_info(&mut host, &pub_key_hash, account.clone()) + .expect("Writing to the storage should have worked"); + + let read_account = get_tezos_account_info(&host, &pub_key_hash) + .expect("Reading the storage should have worked") + .expect("The path to the account should exist"); + assert_eq!(account, read_account); + } +}