diff --git a/src/kernel_evm/Cargo.lock b/src/kernel_evm/Cargo.lock index 5559a286920049ef58d27b3ea2db80d5afc57030..37aa5e07ce7f3551b151ea72d578ddda47ba02c6 100644 --- a/src/kernel_evm/Cargo.lock +++ b/src/kernel_evm/Cargo.lock @@ -489,6 +489,7 @@ name = "evm_kernel" version = "0.1.0" dependencies = [ "anyhow", + "evm", "evm-execution", "hex", "libsecp256k1", diff --git a/src/kernel_evm/kernel/Cargo.toml b/src/kernel_evm/kernel/Cargo.toml index 90ea807a62dd287a45d55335c8b5bb2cea641830..41460b8842dcd3434cd85774d6dac10db87893c0 100644 --- a/src/kernel_evm/kernel/Cargo.toml +++ b/src/kernel_evm/kernel/Cargo.toml @@ -27,6 +27,7 @@ sha3.workspace = true libsecp256k1.workspace = true tezos_crypto_rs.workspace = true +evm.workspace = true evm-execution.workspace = true tezos_ethereum.workspace = true tezos-evm-logging.workspace = true diff --git a/src/kernel_evm/kernel/src/apply.rs b/src/kernel_evm/kernel/src/apply.rs index f05173a32bc944f1c9b64643de697e1721d2a097..af2d3d5dac060c8a0ec9407ad8c03ff3c8a2fd4f 100644 --- a/src/kernel_evm/kernel/src/apply.rs +++ b/src/kernel_evm/kernel/src/apply.rs @@ -292,7 +292,7 @@ pub fn apply_transaction( host: &mut Host, block_constants: &BlockConstants, precompiles: &PrecompileBTreeMap, - transaction: Transaction, + transaction: &Transaction, index: u32, evm_account_storage: &mut EthereumAccountStorage, accounts_index: &mut IndexableStorage, @@ -325,7 +325,7 @@ pub fn apply_transaction( caller, to, ); - let object_info = make_object_info(&transaction, caller, index, gas_used); + let object_info = make_object_info(transaction, caller, index, gas_used); index_new_accounts(host, accounts_index, &receipt_info)?; Ok(Some((receipt_info, object_info))) } diff --git a/src/kernel_evm/kernel/src/block.rs b/src/kernel_evm/kernel/src/block.rs index 58867fe608b3dadba56702db849e315753ee9687..1c8b045ae4cc6757c259b75450dfc9c0a5a68a51 100644 --- a/src/kernel_evm/kernel/src/block.rs +++ b/src/kernel_evm/kernel/src/block.rs @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: 2023 Nomadic Labs // SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Marigold // // SPDX-License-Identifier: MIT @@ -28,35 +29,41 @@ fn compute( evm_account_storage: &mut EthereumAccountStorage, accounts_index: &mut IndexableStorage, ) -> Result<(), anyhow::Error> { + // iteration over all remaining transaction in the block while block_in_progress.has_tx() { + // is reboot necessary ? if block_in_progress.would_overflow() { + // TODO: https://gitlab.com/tezos/tezos/-/issues/6094 + // there should be an upper bound on gasLimit // TODO: https://gitlab.com/tezos/tezos/-/issues/5873 // store the block in progress return Ok(()); } let transaction = block_in_progress.pop_tx().ok_or(Error::Reboot)?; - block_in_progress.account_for_transaction(&transaction); - let tx_hash = transaction.tx_hash; // If `apply_transaction` returns `None`, the transaction should be // ignored, i.e. invalid signature or nonce. - if let Some((receipt_info, object_info)) = apply_transaction( + match apply_transaction( host, block_constants, precompiles, - transaction, + &transaction, block_in_progress.index, evm_account_storage, accounts_index, )? { - block_in_progress.register_transaction(tx_hash, object_info.gas_used)?; - let receipt = block_in_progress.make_receipt(receipt_info); - storage::store_transaction_receipt(host, &receipt) - .context("Failed to store the receipt")?; - let object = block_in_progress.make_object(object_info); - storage::store_transaction_object(host, &object) - .context("Failed to store the transaction object")?; - } + Some((receipt_info, object_info)) => { + block_in_progress.register_valid_transaction( + &transaction, + object_info, + receipt_info, + host, + )?; + } + None => { + block_in_progress.account_for_invalid_transaction(); + } + }; } Ok(()) } @@ -115,13 +122,15 @@ pub fn produce( mod tests { use super::*; use crate::blueprint::Blueprint; + use crate::inbox::Transaction; use crate::inbox::TransactionContent; - use crate::inbox::{Transaction, TransactionContent::Ethereum}; + use crate::inbox::TransactionContent::Ethereum; use crate::indexable_storage::internal_for_tests::{get_value, length}; use crate::storage::internal_for_tests::{ read_transaction_receipt, read_transaction_receipt_status, }; use crate::storage::{init_blocks_index, init_transaction_hashes_index}; + use crate::tick_model; use evm_execution::account_storage::{ account_path, init_account_storage, EthereumAccountStorage, }; @@ -228,8 +237,11 @@ mod tests { dummy_eth_gen_transaction(nonce, v, r, s) } - fn dummy_eth_transaction_deploy() -> EthereumTransactionCommon { - let nonce = U256::from(0); + fn dummy_eth_transaction_deploy_from_nonce_and_pk( + nonce: u64, + private_key: &str, + ) -> EthereumTransactionCommon { + let nonce = U256::from(nonce); let gas_price = U256::from(40000000000u64); let gas_limit = 42000u64; let value = U256::zero(); @@ -249,12 +261,15 @@ mod tests { s: H256::zero(), }; + tx.sign_transaction(private_key.to_string()).unwrap() + } + + fn dummy_eth_transaction_deploy() -> EthereumTransactionCommon { // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 - tx.sign_transaction( - "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701" - .to_string(), + dummy_eth_transaction_deploy_from_nonce_and_pk( + 0, + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701", ) - .unwrap() } fn produce_block_with_several_valid_txs( @@ -742,7 +757,7 @@ mod tests { } #[test] - fn test_ticks_transaction() { + fn test_ticks_valid_transaction() { // init host let mut host = MockHost::default(); @@ -762,7 +777,7 @@ mod tests { content: TransactionContent::Ethereum(dummy_eth_transaction_deploy()), }; - let transactions = vec![valid_tx.clone()].into(); + let transactions = vec![valid_tx].into(); // act let block_in_progress = @@ -780,7 +795,48 @@ mod tests { "Gas used for contract creation" ); - assert_eq!(ticks, valid_tx.estimate_ticks()); + assert_eq!( + ticks, + tick_model::ticks_of_gas(21123) + tick_model::block_overhead_ticks() + ); + } + + #[test] + fn test_ticks_invalid() { + // init host + let mut host = MockHost::default(); + let evm_account_storage = init_account_storage().unwrap(); + + // tx is invalid because wrong nonce + let valid_tx = Transaction { + tx_hash: [0; TRANSACTION_HASH_SIZE], + content: TransactionContent::Ethereum( + dummy_eth_transaction_deploy_from_nonce_and_pk( + 42, + "e922354a3e5902b5ac474f3ff08a79cff43533826b8f451ae2190b65a9d26158", + ), + ), + }; + + let transactions = vec![valid_tx].into(); + + // act + let block_in_progress = + compute_block(transactions, &mut host, evm_account_storage); + + // assert + let ticks = block_in_progress.estimated_ticks; + let block = block_in_progress + .finalize_and_store(&mut host) + .expect("should be able to finalize block"); + + assert_eq!(block.gas_used, U256::zero(), "no gas used"); + // crypto + tx overhead + init overhead + assert_eq!( + ticks, + tick_model::ticks_of_invalid_transaction() + + tick_model::block_overhead_ticks() + ); } #[test] @@ -812,7 +868,7 @@ mod tests { let mut block_in_progress = BlockInProgress::new(U256::from(1), U256::from(1), transactions); // block is almost full wrt ticks - block_in_progress.estimated_ticks = block_in_progress::MAX_TICKS - 1000; + block_in_progress.estimated_ticks = tick_model::MAX_TICKS - 1000; // act compute::( @@ -835,7 +891,7 @@ mod tests { ); assert_eq!( block_in_progress.estimated_ticks, - block_in_progress::MAX_TICKS - 1000, + tick_model::MAX_TICKS - 1000, "should not have consumed any tick" ); diff --git a/src/kernel_evm/kernel/src/block_in_progress.rs b/src/kernel_evm/kernel/src/block_in_progress.rs index e52974078c5e37facba05aefc2d22bf811992e4b..167f6bf401610baf2a0463e7950541ccbed9e43b 100644 --- a/src/kernel_evm/kernel/src/block_in_progress.rs +++ b/src/kernel_evm/kernel/src/block_in_progress.rs @@ -8,23 +8,18 @@ use crate::apply::{TransactionObjectInfo, TransactionReceiptInfo}; use crate::current_timestamp; use crate::error::Error; use crate::error::TransferError::CumulativeGasUsedOverflow; -use crate::inbox::{Transaction, TransactionContent}; +use crate::inbox::Transaction; use crate::storage; +use crate::tick_model; use anyhow::Context; use primitive_types::{H256, U256}; use std::collections::VecDeque; use tezos_ethereum::block::L2Block; -use tezos_ethereum::transaction::*; +use tezos_ethereum::transaction::{ + TransactionObject, TransactionReceipt, TransactionStatus, TransactionType, +}; use tezos_smart_rollup_host::runtime::Runtime; -/// DO NOT TOUCH ////////////////////////////////////////////////////////////// -/// Those constants where derived from benchmarking -pub const TICK_PER_GAS: u64 = 2000; -pub const TICK_FOR_CRYPTO: u64 = 25_000_000; -pub const MAX_TICKS: u64 = 10_000_000_000; -pub const TICK_FOR_DEPOSIT: u64 = TICK_FOR_CRYPTO; -/// DO NOT TOUCH ////////////////////////////////////////////////////////////// - /// Container for all data needed during block computation pub struct BlockInProgress { /// block number @@ -63,24 +58,43 @@ impl BlockInProgress { // it should be computed at the end, and not included in the receipt // the block is referenced in the storage by the block number anyway hash: H256(number.into()), - estimated_ticks: 0, + estimated_ticks: tick_model::block_overhead_ticks(), } } - pub fn register_transaction( + pub fn register_valid_transaction( &mut self, - tx_hash: [u8; 32], - gas_used: U256, + transaction: &Transaction, + object_info: TransactionObjectInfo, + receipt_info: TransactionReceiptInfo, + host: &mut Host, ) -> Result<(), anyhow::Error> { + // account for gas self.cumulative_gas = self .cumulative_gas - .checked_add(gas_used) + .checked_add(object_info.gas_used) .ok_or(Error::Transfer(CumulativeGasUsedOverflow))?; - self.valid_txs.push(tx_hash); + + // account for ticks + self.estimated_ticks += + tick_model::ticks_of_valid_transaction(transaction, &receipt_info); + + // register transaction as done + self.valid_txs.push(transaction.tx_hash); self.index += 1; + + // store info + storage::store_transaction_receipt(host, &self.make_receipt(receipt_info)) + .context("Failed to store the receipt")?; + storage::store_transaction_object(host, &self.make_object(object_info)) + .context("Failed to store the transaction object")?; Ok(()) } + pub fn account_for_invalid_transaction(&mut self) { + self.estimated_ticks += tick_model::ticks_of_invalid_transaction(); + } + pub fn finalize_and_store( self, host: &mut Host, @@ -104,28 +118,15 @@ impl BlockInProgress { !self.tx_queue.is_empty() } - pub fn estimate_ticks_for_transaction(transaction: &Transaction) -> u64 { - match &transaction.content { - TransactionContent::Ethereum(eth) => { - eth.gas_limit * TICK_PER_GAS + TICK_FOR_CRYPTO - } - TransactionContent::Deposit(_) => TICK_FOR_DEPOSIT, - } - } - pub fn would_overflow(&self) -> bool { match self.tx_queue.front() { Some(transaction) => { - self.estimated_ticks + transaction.estimate_ticks() > MAX_TICKS + tick_model::estimate_would_overflow(self.estimated_ticks, transaction) } None => false, // should not happen, but false is a safe value anyway } } - pub fn account_for_transaction(&mut self, transaction: &Transaction) { - self.estimated_ticks += transaction.estimate_ticks(); - } - pub fn make_receipt( &self, receipt_info: TransactionReceiptInfo, diff --git a/src/kernel_evm/kernel/src/inbox.rs b/src/kernel_evm/kernel/src/inbox.rs index e792f750880658a04fc5f4ed724273e5ab84cc6a..0e70b6f360e1495693f80f263f98c742dbd509bf 100644 --- a/src/kernel_evm/kernel/src/inbox.rs +++ b/src/kernel_evm/kernel/src/inbox.rs @@ -3,15 +3,8 @@ // SPDX-FileCopyrightText: 2023 Functori // // SPDX-License-Identifier: MIT -use primitive_types::{H160, U256}; -use sha3::{Digest, Keccak256}; -use tezos_crypto_rs::hash::ContractKt1Hash; -use tezos_ethereum::signatures::EthereumTransactionCommon; -use tezos_evm_logging::log; -use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; -use tezos_smart_rollup_host::runtime::Runtime; -use crate::block_in_progress::BlockInProgress; +use crate::error::UpgradeProcessError::{InvalidUpgradeNonce, NoDictator}; use crate::parsing::{ Input, InputResult, MAX_SIZE_PER_CHUNK, SIGNATURE_HASH_SIZE, UPGRADE_NONCE_SIZE, }; @@ -20,12 +13,17 @@ use crate::storage::{ get_and_increment_deposit_nonce, read_dictator_key, read_kernel_upgrade_nonce, store_last_info_per_level_timestamp, }; - -use crate::error::UpgradeProcessError::{InvalidUpgradeNonce, NoDictator}; +use crate::tick_model; use crate::upgrade::check_dictator_signature; use crate::Error; - +use primitive_types::{H160, U256}; +use sha3::{Digest, Keccak256}; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_ethereum::signatures::EthereumTransactionCommon; use tezos_ethereum::transaction::TransactionHash; +use tezos_evm_logging::log; +use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; +use tezos_smart_rollup_host::runtime::Runtime; #[derive(Debug, PartialEq, Clone)] pub struct Deposit { @@ -47,9 +45,11 @@ pub struct Transaction { } impl Transaction { + /// give an approximation of the number of ticks necessary to process the + /// transaction. Overapproximated using the [gas_limit] and benchmarks pub fn estimate_ticks(&self) -> u64 { // all details of tick model stay in the same module - BlockInProgress::estimate_ticks_for_transaction(self) + tick_model::estimate_ticks_for_transaction(self) } } diff --git a/src/kernel_evm/kernel/src/lib.rs b/src/kernel_evm/kernel/src/lib.rs index 53eb6945ca2fdd1eaade49044dde7c5ef34518ab..ec326d5d0539bec1ffdcada8b559af52c22990b8 100644 --- a/src/kernel_evm/kernel/src/lib.rs +++ b/src/kernel_evm/kernel/src/lib.rs @@ -39,6 +39,7 @@ mod parsing; mod safe_storage; mod simulation; mod storage; +mod tick_model; mod upgrade; /// The chain id will need to be unique when the EVM rollup is deployed in diff --git a/src/kernel_evm/kernel/src/tick_model.rs b/src/kernel_evm/kernel/src/tick_model.rs new file mode 100644 index 0000000000000000000000000000000000000000..41f5bcd937b5c3e37bb8d60394a341fad0b79005 --- /dev/null +++ b/src/kernel_evm/kernel/src/tick_model.rs @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use crate::{apply::TransactionReceiptInfo, inbox::Transaction}; +use evm::Config; +use tezos_ethereum::signatures::EthereumTransactionCommon; + +/////////////////////////////////////////////////////////////////////////////// +/// TICK MODEL CONSTANTS +/// The following values were estimated using benchmarking, and should not be +/// modified without running more + +/// Overapproximation of the amount of ticks for a deposit +/// TODO : https://gitlab.com/tezos/tezos/-/issues/5873 +/// estimate value using benchmarks +const TICK_FOR_DEPOSIT: u64 = TICK_FOR_CRYPTO; +/// Overapproximation of the amount of ticks per gas unit +/// TODO : https://gitlab.com/tezos/tezos/-/issues/5873 +/// estimate value using benchmarks +const TICK_PER_GAS: u64 = 2000; +// Overapproximation of ticks used in signature verification +const TICK_FOR_CRYPTO: u64 = 25_000_000; +/// Overapproximation of the ticks used by the kernel to process a transaction +/// before checking or execution +/// TODO : https://gitlab.com/tezos/tezos/-/issues/5873 +/// estimate value using benchmarks +const TRANSACTION_OVERHEAD: u64 = 1_000_000; +/// Maximum number of ticks for a kernel run as set by the PVM +/// see [ticks_per_snapshot] +pub const MAX_TICKS: u64 = 11_000_000_000; +/// Safety margin the kernel enforce to avoid approaching the maximum number of +/// ticks +const SAFETY_MARGIN: u64 = 2_000_000_000; +/// Overapproximation of the number of ticks the kernel uses to initialise and +/// reload its state +/// TODO : https://gitlab.com/tezos/tezos/-/issues/5873 +/// estimate value using benchmarks +const INITIALISATION_OVERHEAD: u64 = 1_000_000_000; +/// see [maximum_reboots_per_input] +const _MAXIMUM_REBOOTS: u64 = 1_000; +/// The minimum amount of gas for an ethereum transaction. +const BASE_GAS: u64 = Config::london().gas_transaction_call; + +/////////////////////////////////////////////////////////////////////////////// + +pub fn estimate_ticks_for_transaction(transaction: &Transaction) -> u64 { + match &transaction.content { + crate::inbox::TransactionContent::Deposit(_) => ticks_of_deposit(), + crate::inbox::TransactionContent::Ethereum(eth) => ticks_of_gas(eth.gas_limit), + } +} + +fn ticks_of_deposit() -> u64 { + TICK_FOR_DEPOSIT + TRANSACTION_OVERHEAD +} + +pub fn ticks_of_gas(gas: u64) -> u64 { + // the ticks used in crypto are not derived from gas + // the base fee (for all transaction) represent those ticks + // so the minimum amount is deduced from the gas + let gas_execution = gas.saturating_sub(BASE_GAS); + gas_execution + .saturating_mul(TICK_PER_GAS) + .saturating_add(TRANSACTION_OVERHEAD) + .saturating_add(TICK_FOR_CRYPTO) +} + +/// Check that a transaction can fit inside the tick limit +pub fn estimate_would_overflow(estimated_ticks: u64, transaction: &Transaction) -> bool { + estimate_ticks_for_transaction(transaction) + .saturating_add(estimated_ticks) + .saturating_add(SAFETY_MARGIN) + > MAX_TICKS +} + +/// Initial amount for the tick accumulator, corresponding to the overhead of a +/// block +pub fn block_overhead_ticks() -> u64 { + INITIALISATION_OVERHEAD +} + +/// An invalid transaction could not be transmitted to the VM, eg. the nonce +/// was wrong, or the signature verification failed. +pub fn ticks_of_invalid_transaction() -> u64 { + // invalid transaction only cost crypto ticks + TICK_FOR_CRYPTO + TRANSACTION_OVERHEAD +} + +pub fn ticks_of_valid_transaction( + transaction: &Transaction, + receipt_info: &TransactionReceiptInfo, +) -> u64 { + match &transaction.content { + crate::inbox::TransactionContent::Ethereum(eth) => { + ticks_of_valid_transaction_ethereum(eth, receipt_info) + } + crate::inbox::TransactionContent::Deposit(_) => ticks_of_deposit(), + } +} + +/// A valid transaction is a transaction that could be transmitted to +/// evm_execution. It can succeed (with or without effect on the state) +/// or fail (if the VM encountered an error). +pub fn ticks_of_valid_transaction_ethereum( + transaction: &EthereumTransactionCommon, + receipt_info: &TransactionReceiptInfo, +) -> u64 { + match &receipt_info.execution_outcome { + Some(outcome) => ticks_of_gas(outcome.gas_used), + None => ticks_of_gas(transaction.gas_limit), + } +}