diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index 88c6358dae86914053abf1dc8f9c548025a753cf..9b74bfabe088cb7135598e4d99e35edcb56f1884 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -2432,7 +2432,9 @@ dependencies = [ name = "revm-etherlink" version = "0.1.0" dependencies = [ + "alloy-sol-types", "format_no_std", + "num-bigint", "primitive-types", "revm", "tezos-evm-logging-latest", @@ -2440,6 +2442,8 @@ dependencies = [ "tezos-smart-rollup-encoding", "tezos-smart-rollup-host", "tezos-smart-rollup-storage", + "tezos_crypto_rs", + "tezos_data_encoding", "tezos_ethereum_latest", "thiserror 1.0.69", ] diff --git a/etherlink/kernel_latest/revm/Cargo.toml b/etherlink/kernel_latest/revm/Cargo.toml index fea0d2e10c3bbb89a1b7025b33e0835cda6be2de..303d52c11ebb3bd88b2d31a2a850a3ccb639f38f 100644 --- a/etherlink/kernel_latest/revm/Cargo.toml +++ b/etherlink/kernel_latest/revm/Cargo.toml @@ -16,12 +16,16 @@ revm = { version = "25.0.0", default-features = false } # Types primitive-types.workspace = true tezos_ethereum.workspace = true +alloy-sol-types.workspace = true +num-bigint.workspace = true # SDK tezos-evm-runtime.workspace = true tezos-smart-rollup-host.workspace = true tezos-smart-rollup-storage.workspace = true tezos-smart-rollup-encoding.workspace = true +tezos_crypto_rs.workspace = true +tezos_data_encoding.workspace = true # Miscs format_no_std = { version = "1.2.0" } diff --git a/etherlink/kernel_latest/revm/src/database.rs b/etherlink/kernel_latest/revm/src/database.rs index 55bf9b7c3de178101607cc7e5072dc4ca0a10455..79cc9445aabf5f9e003d12e977ed5ee16ec6db64 100644 --- a/etherlink/kernel_latest/revm/src/database.rs +++ b/etherlink/kernel_latest/revm/src/database.rs @@ -1,10 +1,14 @@ // SPDX-FileCopyrightText: 2025 Functori +// SPDX-FileCopyrightText: 2025 Nomadic Labs // // SPDX-License-Identifier: MIT +use std::mem; + use crate::{ block_storage::{get_block_hash, BLOCKS_STORED}, code_storage::CodeStorage, + send_outbox_message::Withdrawal, world_state_handler::{account_path, StorageAccount, WorldStateHandler}, Error, }; @@ -34,6 +38,8 @@ pub struct EtherlinkVMDB<'a, Host: Runtime> { /// error and we need to revert the changes made to the durable /// storage. commit_status: &'a mut bool, + /// Withdrawals accumulated by the current execution and consumed at the end of it + withdrawals: Vec, } // See: https://github.com/rust-lang/rust-clippy/issues/5787 @@ -50,12 +56,15 @@ impl<'a, Host: Runtime> EtherlinkVMDB<'a, Host> { block, world_state_handler, commit_status, + withdrawals: vec![], } } } -pub trait AccountDatabase: Database { +pub trait PrecompileDatabase: Database { fn get_or_create_account(&self, address: Address) -> Result; + fn push_withdrawal(&mut self, withdrawal: Withdrawal); + fn take_withdrawals(&mut self) -> Vec; } impl EtherlinkVMDB<'_, Host> { @@ -78,7 +87,7 @@ impl EtherlinkVMDB<'_, Host> { } } -impl AccountDatabase for EtherlinkVMDB<'_, Host> { +impl PrecompileDatabase for EtherlinkVMDB<'_, Host> { fn get_or_create_account(&self, address: Address) -> Result { // TODO: get_account function should be implemented whenever errors are // reintroduced @@ -86,6 +95,14 @@ impl AccountDatabase for EtherlinkVMDB<'_, Host> { .get_or_create(self.host, &account_path(&address)) .map_err(|err| Error::Custom(err.to_string())) } + + fn push_withdrawal(&mut self, withdrawal: Withdrawal) { + self.withdrawals.push(withdrawal); + } + + fn take_withdrawals(&mut self) -> Vec { + mem::take(&mut self.withdrawals) + } } impl Database for EtherlinkVMDB<'_, Host> { diff --git a/etherlink/kernel_latest/revm/src/lib.rs b/etherlink/kernel_latest/revm/src/lib.rs index f97c23b3bf5cdb85beaf85f7aaa0482e3f98fde8..2fe5a8b3c6376545d0e2635818dda7a9003d0df2 100644 --- a/etherlink/kernel_latest/revm/src/lib.rs +++ b/etherlink/kernel_latest/revm/src/lib.rs @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2025 Functori +// SPDX-FileCopyrightText: 2025 Nomadic Labs // // SPDX-License-Identifier: MIT @@ -7,7 +8,7 @@ use precompile_provider::EtherlinkPrecompiles; use revm::context::result::EVMError; use revm::{ context::{ - result::ExecutionResult, transaction::AccessList, BlockEnv, CfgEnv, + result::ExecutionResult, transaction::AccessList, BlockEnv, CfgEnv, ContextTr, DBErrorMarker, Evm, LocalContext, TxEnv, }, context_interface::block::BlobExcessGasAndPrice, @@ -23,12 +24,14 @@ use tezos_smart_rollup_host::runtime::RuntimeError; use thiserror::Error; use world_state_handler::{account_path, WorldStateHandler}; +use crate::{database::PrecompileDatabase, send_outbox_message::Withdrawal}; + mod block_storage; mod code_storage; mod database; mod precompile_provider; +mod send_outbox_message; mod storage_helpers; -mod withdrawal; mod world_state_handler; const ETHERLINK_CHAIN_ID: u64 = 42793; @@ -52,8 +55,7 @@ pub struct ExecutionOutcome { pub result: ExecutionResult, /// Withdrawals generated by the transaction. This field will be empty if the /// transaction fails (or if the transaction doesn't produce any withdrawals). - // TODO: Replace () by a Withdrawal struct/type. - pub withdrawals: Vec<()>, + pub withdrawals: Vec, } fn block_env(block_constants: &BlockConstants) -> Result { @@ -163,7 +165,7 @@ pub fn run_transaction<'a, Host: Runtime>( host: &'a mut Host, block_constants: &'a BlockConstants, world_state_handler: &'a mut WorldStateHandler, - precompiles: EtherlinkPrecompiles, + precompiles: EtherlinkPrecompiles, caller: Address, destination: Option
, call_data: Bytes, @@ -198,7 +200,11 @@ pub fn run_transaction<'a, Host: Runtime>( let evm = evm(db, &block_env, &tx); - let execution_result = evm.with_precompiles(precompiles).transact_commit(&tx)?; + let mut evm_context = evm.with_precompiles(precompiles); + + let execution_result = evm_context.transact_commit(&tx)?; + + let withdrawals = evm_context.db_mut().take_withdrawals(); // !commit_status := if something went wrong while commiting if !commit_status { @@ -209,7 +215,7 @@ pub fn run_transaction<'a, Host: Runtime>( Ok(ExecutionOutcome { result: execution_result, // contains logs and gas_used. - withdrawals: vec![], + withdrawals, }) } @@ -223,7 +229,7 @@ mod test { }, context_interface::block::BlobExcessGasAndPrice, inspector::inspectors::GasInspector, - primitives::{hex::FromHex, Address, Bytes, FixedBytes, TxKind, B256, U256}, + primitives::{hex::FromHex, Address, Bytes, FixedBytes, TxKind, U256}, state::{AccountInfo, Bytecode}, Context, Database, ExecuteCommitEvm, ExecuteEvm, Journal, MainBuilder, }; @@ -231,9 +237,14 @@ mod test { use tezos_evm_runtime::runtime::MockKernelHost; use utilities::{block_env, etherlink_vm_db, evm, tx_env}; - use crate::{database::EtherlinkVMDB, precompile_provider::EtherlinkPrecompiles}; use crate::{ - withdrawal::WITHDRAWAL_EVENT_TOPIC, world_state_handler::new_world_state_handler, + database::{EtherlinkVMDB, PrecompileDatabase}, + precompile_provider::EtherlinkPrecompiles, + send_outbox_message::SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS, + }; + use crate::{ + send_outbox_message::SendWithdrawalInput, + world_state_handler::new_world_state_handler, }; mod utilities { @@ -362,13 +373,31 @@ mod test { } #[test] - fn test_withdrawal_precompile_contract() { + fn test_outbox_precompile_contract() { + // import reexported version to build `SendWithdrawalInput` + use alloy_sol_types::{sol_data::FixedBytes, SolEvent, SolType}; + let caller = Address::from_hex("1111111111111111111111111111111111111111").unwrap(); let destination = - Address::from_hex("0xff00000000000000000000000000000000000001").unwrap(); + Address::from_hex(SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS).unwrap(); let value = U256::from(5); + let mut abi_data = [0u8; 32]; + abi_data[0] = 0x01; + abi_data[1..21].fill(0x11); + abi_data[22] = 0x00; + let fixed = FixedBytes::<22>::abi_decode(&abi_data, true).unwrap(); + let input = SendWithdrawalInput { + target: fixed, + ticketer: fixed, + amount: U256::from(42), + } + .encode_data(); + let mut data = Vec::with_capacity(1 + input.len()); + data.extend_from_slice(&[0x0c, 0x22, 0xd2, 0x8f]); + data.extend_from_slice(&input); + let block = block_env(None, 1_000_000); let tx = tx_env( caller, @@ -376,7 +405,7 @@ mod test { block.gas_limit, 0, value, - Bytes::from_hex("cda4fee200112233").unwrap(), + Bytes::from(data), None, ); @@ -393,16 +422,13 @@ mod test { let evm = evm(db, &block, &tx); let tracer = GasInspector::default(); - let mut host = MockKernelHost::default(); - let precompiles = EtherlinkPrecompiles::new(&mut host); + let precompiles = EtherlinkPrecompiles::new(); let mut evm = evm.with_inspector(tracer).with_precompiles(precompiles); let execution_result: ExecutionResult = evm.transact(&tx).unwrap(); + let withdrawals = evm.db_mut().take_withdrawals(); - let expected_topics = vec![B256::new(WITHDRAWAL_EVENT_TOPIC)]; - assert_eq!( - execution_result.logs().first().unwrap().topics(), - expected_topics - ) + assert!(execution_result.is_success()); + assert!(!withdrawals.is_empty()); } #[test] diff --git a/etherlink/kernel_latest/revm/src/precompile_provider.rs b/etherlink/kernel_latest/revm/src/precompile_provider.rs index f54ae9893f9180b186014a6bf04ac1ce334832f9..f5f24f602466a7545f0f990c241fed6e3cb187f3 100644 --- a/etherlink/kernel_latest/revm/src/precompile_provider.rs +++ b/etherlink/kernel_latest/revm/src/precompile_provider.rs @@ -1,30 +1,39 @@ +// SPDX-FileCopyrightText: 2025 Nomadic Labs +// +// SPDX-License-Identifier: MIT + use revm::{ context::{Cfg, ContextTr, LocalContextTr}, handler::{EthPrecompiles, PrecompileProvider}, interpreter::{CallInput, InputsImpl, InterpreterResult}, - primitives::{hex::FromHex, Address, HashSet}, + primitives::{hex::FromHex, Address}, }; -use tezos_evm_runtime::runtime::Runtime; -use crate::{database::AccountDatabase, withdrawal::withdrawal_precompile}; +use crate::{ + database::PrecompileDatabase, + send_outbox_message::{ + send_outbox_message_precompile, SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS, + }, +}; -pub struct EtherlinkPrecompiles<'a, Host: Runtime> { - host: &'a mut Host, - customs: HashSet
, +pub struct EtherlinkPrecompiles { + customs: Vec
, builtins: EthPrecompiles, } -impl<'a, Host: Runtime> EtherlinkPrecompiles<'a, Host> { - #[allow(dead_code)] - pub fn new(host: &'a mut Host) -> Self { +impl Default for EtherlinkPrecompiles { + fn default() -> Self { + Self::new() + } +} + +impl EtherlinkPrecompiles { + pub fn new() -> Self { Self { - host, - customs: HashSet::from([ - // Withdrawals - Address::from_hex("0xff00000000000000000000000000000000000001").unwrap(), - // FA bridge - Address::from_hex("0xff00000000000000000000000000000000000002").unwrap(), - ]), + customs: vec![ + // Send outbox message + Address::from_hex(SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS).unwrap(), + ], builtins: EthPrecompiles::default(), } } @@ -47,7 +56,7 @@ impl<'a, Host: Runtime> EtherlinkPrecompiles<'a, Host> { ) -> Result, String> where CTX: ContextTr, - CTX::Db: AccountDatabase, + CTX::Db: PrecompileDatabase, { // NIT: can probably do this more efficiently by keeping an immutable // reference on the slice but next mutable call makes it nontrivial @@ -64,11 +73,8 @@ impl<'a, Host: Runtime> EtherlinkPrecompiles<'a, Host> { CallInput::Bytes(bytes) => bytes.to_vec(), }; - if address - == &Address::from_hex("0xff00000000000000000000000000000000000001").unwrap() - { - let result = withdrawal_precompile( - self.host, + if address == self.customs.first().unwrap() { + let result = send_outbox_message_precompile( &input_bytes, context, is_static, @@ -82,10 +88,10 @@ impl<'a, Host: Runtime> EtherlinkPrecompiles<'a, Host> { } } -impl PrecompileProvider for EtherlinkPrecompiles<'_, Host> +impl PrecompileProvider for EtherlinkPrecompiles where CTX: ContextTr, - CTX::Db: AccountDatabase, + CTX::Db: PrecompileDatabase, { type Output = InterpreterResult; diff --git a/etherlink/kernel_latest/revm/src/send_outbox_message.rs b/etherlink/kernel_latest/revm/src/send_outbox_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..0cb49a9ff92faee102d27ad46e8fd7088ecafc84 --- /dev/null +++ b/etherlink/kernel_latest/revm/src/send_outbox_message.rs @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2025 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use alloy_sol_types::{sol, SolEvent}; +use num_bigint::{BigInt, Sign}; +use revm::{ + context::{ContextTr, Transaction}, + interpreter::{Gas, InputsImpl, InstructionResult, InterpreterResult}, + primitives::{Address, Bytes, U256}, +}; +use tezos_data_encoding::nom::NomReader; +use tezos_smart_rollup_encoding::michelson::{ + MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonOption, MichelsonPair, + MichelsonTimestamp, +}; +use tezos_smart_rollup_encoding::outbox::OutboxMessage; +use tezos_smart_rollup_encoding::{ + contract::Contract, entrypoint::Entrypoint, michelson::ticket::FA2_1Ticket, + outbox::OutboxMessageTransaction, +}; + +use crate::database::PrecompileDatabase; + +pub(crate) const SEND_OUTBOX_MESSAGE_PRECOMPILE_ADDRESS: &str = + "0xff00000000000000000000000000000000000003"; + +sol! { + event SendWithdrawalInput ( + bytes22 target, + bytes22 ticketer, + uint256 amount, + ); +} + +/// Withdrawal interface of the ticketer contract +pub type RouterInterface = MichelsonPair; + +/// Interface of the default entrypoint of the fast withdrawal contract. +/// +/// The parameters corresponds to (from left to right w.r.t. `MichelsonPair`): +/// * withdrawal_id +/// * ticket +/// * timestamp +/// * withdrawer's address +/// * generic payload +/// * l2 caller's address +pub type FastWithdrawalInterface = MichelsonPair< + MichelsonNat, + MichelsonPair< + FA2_1Ticket, + MichelsonPair< + MichelsonTimestamp, + MichelsonPair< + MichelsonContract, + MichelsonPair, + >, + >, + >, +>; + +/// Outbox messages that implements the different withdrawal interfaces, +/// ready to be encoded and posted. +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq)] +pub enum Withdrawal { + Standard(OutboxMessage), + Fast(OutboxMessage), +} + +fn revert() -> InterpreterResult { + InterpreterResult { + result: InstructionResult::Revert, + gas: Gas::new(0), + output: Bytes::new(), + } +} + +fn prepare_message(target: Contract, ticketer: Contract, amount: BigInt) -> Withdrawal { + let ticket = FA2_1Ticket::new( + ticketer.clone(), + MichelsonPair(0.into(), MichelsonOption(None)), + amount, + ) + .unwrap(); + + let parameters = MichelsonPair::( + MichelsonContract(target.clone()), + ticket, + ); + + let entrypoint = Entrypoint::try_from(String::from("burn")).unwrap(); + + let message = OutboxMessageTransaction { + parameters, + entrypoint, + destination: ticketer, + }; + + Withdrawal::Standard(OutboxMessage::AtomicTransactionBatch(vec![message].into())) +} + +pub fn send_outbox_message_precompile( + input: &[u8], + context: &mut CTX, + is_static: bool, + transfer: &InputsImpl, + current: &Address, +) -> Result +where + CTX: ContextTr, + CTX::Db: PrecompileDatabase, +{ + if transfer.target_address != *current + || context.tx().caller() == *current + || is_static + { + return Ok(revert()); + } + + match input { + // "0c22d28f" is the function selector for `push_withdrawal_to_outbox(bytes22,bytes22,uint256)` + [0x0c, 0x22, 0xd2, 0x8f, input_data @ ..] => { + // Decode + let (target, ticketer, amount) = + SendWithdrawalInput::abi_decode_data(input_data, true) + .map_err(|e| e.to_string())?; + let (_, target) = + Contract::nom_read(target.as_slice()).map_err(|e| e.to_string())?; + let (_, ticketer) = + Contract::nom_read(ticketer.as_slice()).map_err(|e| e.to_string())?; + let amount = BigInt::from_bytes_be( + Sign::Plus, + &amount.to_be_bytes::<{ U256::BYTES }>(), + ); + + // Build + let withdrawal = prepare_message(target, ticketer, amount); + + // Push + context.db_mut().push_withdrawal(withdrawal); + + let result = InterpreterResult { + result: InstructionResult::Return, + gas: Gas::new(0), + output: Bytes::new(), + }; + Ok(result) + } + _ => Ok(revert()), + } +} diff --git a/etherlink/kernel_latest/revm/src/withdrawal.rs b/etherlink/kernel_latest/revm/src/withdrawal.rs deleted file mode 100644 index 4bf6ccc4697284f011ada6e64bfc597fa7f71246..0000000000000000000000000000000000000000 --- a/etherlink/kernel_latest/revm/src/withdrawal.rs +++ /dev/null @@ -1,108 +0,0 @@ -use revm::{ - context::{ContextTr, JournalTr, Transaction}, - interpreter::{Gas, InputsImpl, InstructionResult, InterpreterResult}, - primitives::{Address, Bytes, Log, B256}, -}; -use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; -use tezos_smart_rollup_encoding::michelson::{ - MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonPair, MichelsonTimestamp, -}; -use tezos_smart_rollup_encoding::outbox::OutboxMessage; - -use crate::database::AccountDatabase; - -/// Withdrawal interface of the ticketer contract -pub type RouterInterface = MichelsonPair; - -/// Interface of the default entrypoint of the fast withdrawal contract. -/// -/// The parameters corresponds to (from left to right w.r.t. `MichelsonPair`): -/// * withdrawal_id -/// * ticket -/// * timestamp -/// * withdrawer's address -/// * generic payload -/// * l2 caller's address -pub type FastWithdrawalInterface = MichelsonPair< - MichelsonNat, - MichelsonPair< - FA2_1Ticket, - MichelsonPair< - MichelsonTimestamp, - MichelsonPair< - MichelsonContract, - MichelsonPair, - >, - >, - >, ->; - -/// Outbox messages that implements the different withdrawal interfaces, -/// ready to be encoded and posted. -#[allow(dead_code)] -#[derive(Debug, PartialEq, Eq)] -pub enum Withdrawal { - Standard(OutboxMessage), - Fast(OutboxMessage), -} - -/// Keccak256 of Withdrawal(uint256,address,bytes22,uint256) -/// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events -pub const WITHDRAWAL_EVENT_TOPIC: [u8; 32] = [ - 45, 90, 215, 147, 24, 31, 91, 107, 215, 39, 192, 194, 22, 70, 30, 1, 158, 207, 228, - 102, 53, 63, 221, 233, 204, 248, 19, 244, 91, 132, 250, 130, -]; - -// `ticks_of_withdraw()` is 880_000 / 1000 (gas to tick ratio) -const WITHDRAWAL_GAS: u64 = 880; - -fn revert_withdrawal() -> InterpreterResult { - InterpreterResult { - result: InstructionResult::Revert, - gas: Gas::new(WITHDRAWAL_GAS), - output: Bytes::new(), - } -} - -pub fn withdrawal_precompile( - _host: &'_ mut Host, - input: &[u8], - context: &mut CTX, - is_static: bool, - transfer: &InputsImpl, - current: &Address, -) -> Result -where - CTX: ContextTr, - CTX::Db: AccountDatabase, -{ - if transfer.target_address != *current - || context.tx().caller() == *current - || is_static - { - return Ok(revert_withdrawal()); - } - - let _account = context.db_mut().get_or_create_account(*current); - - match input { - // "cda4fee2" is the function selector for `withdraw_base58(string)` - [0xcd, 0xa4, 0xfe, 0xe2, input_data @ ..] => { - context.journal_mut().log( - Log::new( - *current, - vec![B256::new(WITHDRAWAL_EVENT_TOPIC)], - Bytes::copy_from_slice(input_data), - ) - .unwrap(), - ); - let result = InterpreterResult { - result: InstructionResult::Return, - gas: Gas::new(0), - output: Bytes::new(), - }; - Ok(result) - } - _ => Ok(revert_withdrawal()), - } -}