Repository: linera-io/linera-protocol
Pull request #3715: Implement the processing of the EVM gas usage.
Author: login: MathieuDutSik
Created: 2025-04-03T19:16:45Z | Merged: 2025-05-06T14:02:22Z

## Motivation

Running the EVM has its cost, known famously as gas/fuel. We need to have adequate tracking of gas usage.

## Proposal

The following was done:
* For each operation done in storage, we track whether it maps from or to 0 (meaning inexistent) and whether it changes the value. In a separate set of tests, we identified when the corresponding gas costs.
* The identified gas costs are then subtracted from the gas_usage returned by REVM. This is because the storage costs are being billed at the end when the block is created. Double-counting the costs would be inadequate.
* The `consume_fuel` and other are multiplied in order to take the `VmRuntime` as arguments. The additional values are added to the `Resources` and other entries.
* In a classical REVM run, the runs from separate contracts are grouped together. However, we do not do that and run the different contracts separately. Therefore, the other calls are set to zero. The grouping of the different runs is then done in the `Resources` data types.
* The service tests are bounded in gas usage by the hardcoded value of 20_000_000.
* The metrics are corrected accordingly.

## Test Plan

The CI.

## Release Plan

Normal release of Linera.

## Links

None.


Diff:
diff --git a/CLI.md b/CLI.md
index c769b3456243..16fabe3acd09 100644
--- a/CLI.md
+++ b/CLI.md
@@ -473,7 +473,8 @@ View or update the resource control policy
 ###### **Options:**
 
 * `--block <BLOCK>` — Set the base price for creating a block
-* `--fuel-unit <FUEL_UNIT>` — Set the price per unit of fuel
+* `--wasm-fuel-unit <WASM_FUEL_UNIT>` — Set the price per unit of Wasm fuel
+* `--evm-fuel-unit <EVM_FUEL_UNIT>` — Set the price per unit of EVM fuel
 * `--read-operation <READ_OPERATION>` — Set the price per read operation
 * `--write-operation <WRITE_OPERATION>` — Set the price per write operation
 * `--byte-read <BYTE_READ>` — Set the price per byte read
@@ -489,7 +490,8 @@ View or update the resource control policy
 * `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
 * `--service-as-oracle-query <SERVICE_AS_ORACLE_QUERY>` — Set the price per query to a service as an oracle
 * `--http-request <HTTP_REQUEST>` — Set the price for performing an HTTP request
-* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
+* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block
+* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block
 * `--maximum-service-oracle-execution-ms <MAXIMUM_SERVICE_ORACLE_EXECUTION_MS>` — Set the maximum time in milliseconds that a block can spend executing services as oracles
 * `--maximum-block-size <MAXIMUM_BLOCK_SIZE>` — Set the maximum size of a block, in bytes
 * `--maximum-blob-size <MAXIMUM_BLOB_SIZE>` — Set the maximum size of data blobs, compressed bytecode and other binary blobs, in bytes
@@ -533,7 +535,8 @@ Create genesis configuration for a Linera deployment. Create initial user chains
   Possible values: `no-fees`, `testnet`
 
 * `--block-price <BLOCK_PRICE>` — Set the base price for creating a block. (This will overwrite value from `--policy-config`)
-* `--fuel-unit-price <FUEL_UNIT_PRICE>` — Set the price per unit of fuel. (This will overwrite value from `--policy-config`)
+* `--wasm-fuel-unit-price <WASM_FUEL_UNIT_PRICE>` — Set the price per unit of Wasm fuel. (This will overwrite value from `--policy-config`)
+* `--evm-fuel-unit-price <EVM_FUEL_UNIT_PRICE>` — Set the price per unit of EVM fuel. (This will overwrite value from `--policy-config`)
 * `--read-operation-price <READ_OPERATION_PRICE>` — Set the price per read operation. (This will overwrite value from `--policy-config`)
 * `--write-operation-price <WRITE_OPERATION_PRICE>` — Set the price per write operation. (This will overwrite value from `--policy-config`)
 * `--byte-read-price <BYTE_READ_PRICE>` — Set the price per byte read. (This will overwrite value from `--policy-config`)
@@ -549,7 +552,8 @@ Create genesis configuration for a Linera deployment. Create initial user chains
 * `--message-byte-price <MESSAGE_BYTE_PRICE>` — Set the additional price for each byte in the argument of a user message. (This will overwrite value from `--policy-config`)
 * `--service-as-oracle-query-price <SERVICE_AS_ORACLE_QUERY_PRICE>` — Set the price per query to a service as an oracle
 * `--http-request-price <HTTP_REQUEST_PRICE>` — Set the price for performing an HTTP request
-* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block. (This will overwrite value from `--policy-config`)
+* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block. (This will overwrite value from `--policy-config`)
+* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block. (This will overwrite value from `--policy-config`)
 * `--maximum-service-oracle-execution-ms <MAXIMUM_SERVICE_ORACLE_EXECUTION_MS>` — Set the maximum time in milliseconds that a block can spend executing services as oracles
 * `--maximum-block-size <MAXIMUM_BLOCK_SIZE>` — Set the maximum size of a block. (This will overwrite value from `--policy-config`)
 * `--maximum-bytecode-size <MAXIMUM_BYTECODE_SIZE>` — Set the maximum size of decompressed contract or service bytecode, in bytes. (This will overwrite value from `--policy-config`)
diff --git a/examples/meta-counter/src/contract.rs b/examples/meta-counter/src/contract.rs
index d2dc8b52e068..9fc2198e15af 100644
--- a/examples/meta-counter/src/contract.rs
+++ b/examples/meta-counter/src/contract.rs
@@ -60,7 +60,7 @@ impl Contract for MetaCounterContract {
         } = operation;
 
         let mut message = self.runtime.prepare_message(message).with_grant(Resources {
-            fuel: fuel_grant,
+            wasm_fuel: fuel_grant,
             ..Resources::default()
         });
         if authenticated {
diff --git a/linera-base/src/data_types.rs b/linera-base/src/data_types.rs
index 960eef0f48cb..27b2d90a4a77 100644
--- a/linera-base/src/data_types.rs
+++ b/linera-base/src/data_types.rs
@@ -269,8 +269,10 @@ impl Display for Timestamp {
     Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, WitLoad, WitStore, WitType,
 )]
 pub struct Resources {
-    /// An amount of execution fuel.
-    pub fuel: u64,
+    /// An amount of Wasm execution fuel.
+    pub wasm_fuel: u64,
+    /// An amount of EVM execution fuel.
+    pub evm_fuel: u64,
     /// A number of read operations to be executed.
     pub read_operations: u32,
     /// A number of write operations to be executed.
diff --git a/linera-base/src/unit_tests.rs b/linera-base/src/unit_tests.rs
index a31c3dde0fb2..3e43ab81d984 100644
--- a/linera-base/src/unit_tests.rs
+++ b/linera-base/src/unit_tests.rs
@@ -51,7 +51,8 @@ fn resources_test_case() -> Resources {
         blobs_to_publish: 73,
         blob_bytes_to_read: 67,
         blob_bytes_to_publish: 71,
-        fuel: 1_000,
+        wasm_fuel: 1_000,
+        evm_fuel: 1_000,
         message_size: 4,
         messages: 93,
         read_operations: 12,
@@ -75,7 +76,8 @@ fn send_message_request_test_case() -> SendMessageRequest<Vec<u8>> {
             blobs_to_publish: 1000,
             blob_bytes_to_read: 10,
             blob_bytes_to_publish: 100,
-            fuel: 8,
+            wasm_fuel: 8,
+            evm_fuel: 8,
             message_size: 1,
             messages: 0,
             read_operations: 1,
diff --git a/linera-chain/src/chain.rs b/linera-chain/src/chain.rs
index 853f3fc9f1eb..e6010d5dff55 100644
--- a/linera-chain/src/chain.rs
+++ b/linera-chain/src/chain.rs
@@ -109,30 +109,40 @@ static WASM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
 });
 
 #[cfg(with_metrics)]
-static WASM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
+static EVM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
     register_histogram_vec(
-        "wasm_num_reads_per_block",
-        "Wasm number of reads per block",
+        "evm_fuel_used_per_block",
+        "EVM fuel used per block",
+        &[],
+        exponential_bucket_interval(10.0, 1_000_000.0),
+    )
+});
+
+#[cfg(with_metrics)]
+static VM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
+    register_histogram_vec(
+        "vm_num_reads_per_block",
+        "VM number of reads per block",
         &[],
         exponential_bucket_interval(0.1, 100.0),
     )
 });
 
 #[cfg(with_metrics)]
-static WASM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
+static VM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
     register_histogram_vec(
-        "wasm_bytes_read_per_block",
-        "Wasm number of bytes read per block",
+        "vm_bytes_read_per_block",
+        "VM number of bytes read per block",
         &[],
         exponential_bucket_interval(0.1, 10_000_000.0),
     )
 });
 
 #[cfg(with_metrics)]
-static WASM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
+static VM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
     register_histogram_vec(
-        "wasm_bytes_written_per_block",
-        "Wasm number of bytes written per block",
+        "vm_bytes_written_per_block",
+        "VM number of bytes written per block",
         &[],
         exponential_bucket_interval(0.1, 10_000_000.0),
     )
@@ -1140,14 +1150,17 @@ where
         NUM_BLOCKS_EXECUTED.with_label_values(&[]).inc();
         WASM_FUEL_USED_PER_BLOCK
             .with_label_values(&[])
-            .observe(tracker.fuel as f64);
-        WASM_NUM_READS_PER_BLOCK
+            .observe(tracker.wasm_fuel as f64);
+        EVM_FUEL_USED_PER_BLOCK
+            .with_label_values(&[])
+            .observe(tracker.evm_fuel as f64);
+        VM_NUM_READS_PER_BLOCK
             .with_label_values(&[])
             .observe(tracker.read_operations as f64);
-        WASM_BYTES_READ_PER_BLOCK
+        VM_BYTES_READ_PER_BLOCK
             .with_label_values(&[])
             .observe(tracker.bytes_read as f64);
-        WASM_BYTES_WRITTEN_PER_BLOCK
+        VM_BYTES_WRITTEN_PER_BLOCK
             .with_label_values(&[])
             .observe(tracker.bytes_written as f64);
     }
diff --git a/linera-chain/src/unit_tests/chain_tests.rs b/linera-chain/src/unit_tests/chain_tests.rs
index c59f15f52334..65972516c0ff 100644
--- a/linera-chain/src/unit_tests/chain_tests.rs
+++ b/linera-chain/src/unit_tests/chain_tests.rs
@@ -314,7 +314,7 @@ async fn test_application_permissions() -> anyhow::Result<()> {
     );
 
     // Also, blocks without an application operation or incoming message are forbidden.
-    let invalid_block = make_child_block(&value.clone());
+    let invalid_block = make_child_block(&value);
     let result = chain
         .execute_block(&invalid_block, time, None, &[], None)
         .await;
diff --git a/linera-core/src/unit_tests/client_tests.rs b/linera-core/src/unit_tests/client_tests.rs
index 99ca5736b5ea..411c048f5817 100644
--- a/linera-core/src/unit_tests/client_tests.rs
+++ b/linera-core/src/unit_tests/client_tests.rs
@@ -1080,7 +1080,7 @@ where
     let mut builder = TestBuilder::new(storage_builder, 4, 1, &mut signer)
         .await?
         .with_policy(ResourceControlPolicy {
-            maximum_fuel_per_block: 30_000,
+            maximum_wasm_fuel_per_block: 30_000,
             blob_read: initial_balance + Amount::ONE,
             blob_published: initial_balance + Amount::ONE,
             blob_byte_read: initial_balance + Amount::ONE,
diff --git a/linera-core/src/unit_tests/wasm_client_tests.rs b/linera-core/src/unit_tests/wasm_client_tests.rs
index 9afa2f403a77..a1d2a2f1f3c6 100644
--- a/linera-core/src/unit_tests/wasm_client_tests.rs
+++ b/linera-core/src/unit_tests/wasm_client_tests.rs
@@ -882,7 +882,7 @@ async fn test_memory_fuel_limit(wasm_runtime: WasmRuntime) -> anyhow::Result<()>
     // Set a fuel limit that is enough to instantiate the application and do one increment
     // operation, but not ten. We also verify blob fees for the bytecode.
     let policy = ResourceControlPolicy {
-        maximum_fuel_per_block: 30_000,
+        maximum_wasm_fuel_per_block: 30_000,
         blob_read: Amount::from_tokens(10), // Should not be charged.
         blob_published: Amount::from_attos(100),
         blob_byte_read: Amount::from_tokens(10), // Should not be charged.
diff --git a/linera-execution/src/evm/database.rs b/linera-execution/src/evm/database.rs
index d48a75fe8e09..ffae528a5d14 100644
--- a/linera-execution/src/evm/database.rs
+++ b/linera-execution/src/evm/database.rs
@@ -20,22 +20,47 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage.
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage for the first time.
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of not changing the state of the variable in the storage.
+const SSTORE_COST_NO_OPERATION: u64 = 100;
+
+/// The cost of overwriting the storage to a different value.
+const SSTORE_COST_RESET: u64 = 2900;
+
+/// The refund from releasing data.
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    key_no_operation: u64,
+    key_reset: u64,
+    key_set: u64,
+    key_release: u64,
+    key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.key_no_operation * SSTORE_COST_NO_OPERATION;
+        storage_costs += self.key_reset * SSTORE_COST_RESET;
+        storage_costs += self.key_set * SSTORE_COST_SET;
+        storage_costs += self.key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
     pub runtime: Arc<Mutex<Runtime>>,
     pub changes: EvmState,
 }
@@ -43,22 +68,41 @@ pub(crate) struct DatabaseRuntime<Runtime> {
 impl<Runtime> Clone for DatabaseRuntime<Runtime> {
     fn clone(&self) -> Self {
         Self {
+            storage_stats: self.storage_stats.clone(),
             runtime: self.runtime.clone(),
             changes: self.changes.clone(),
         }
     }
 }
 
+#[repr(u8)]
+enum KeyTag {
+    /// Key prefix for the storage of the zero contract.
+    NullAddress,
+    /// Key prefix for the storage of the contract address.
+    ContractAddress,
+}
+
+#[repr(u8)]
+pub enum KeyCategory {
+    AccountInfo,
+    AccountState,
+    Storage,
+}
+
 impl<Runtime> DatabaseRuntime<Runtime> {
-    fn get_uint256_key(val: u8, index: U256) -> Result<Vec<u8>, ExecutionError> {
+    /// Encode the `index` of the EVM storage associated to the smart contract
+    /// in a linera key.
+    fn get_linera_key(val: u8, index: U256) -> Result<Vec<u8>, ExecutionError> {
         let mut key = vec![val, KeyCategory::Storage as u8];
         bcs::serialize_into(&mut key, &index)?;
         Ok(key)
     }
 
+    /// Returns the tag associated to the contract.
     fn get_contract_address_key(&self, address: &Address) -> Option<u8> {
         if address == &Address::ZERO {
-            return Some(KeyTag::ZeroContractAddress as u8);
+            return Some(KeyTag::NullAddress as u8);
         }
         if address == &Address::ZERO.create(0) {
             return Some(KeyTag::ContractAddress as u8);
@@ -66,12 +110,26 @@ impl<Runtime> DatabaseRuntime<Runtime> {
         None
     }
 
+    /// Creates a new `DatabaseRuntime`.
     pub fn new(runtime: Runtime) -> Self {
+        let storage_stats = StorageStats::default();
         Self {
+            storage_stats: Arc::new(Mutex::new(storage_stats)),
             runtime: Arc::new(Mutex::new(runtime)),
             changes: HashMap::new(),
         }
     }
+
+    /// Returns the current storage states and clears it to default.
+    pub fn take_storage_stats(&self) -> StorageStats {
+        let mut storage_stats_read = self
+            .storage_stats
+            .lock()
+            .expect("The lock should be possible");
+        let storage_stats = storage_stats_read.clone();
+        *storage_stats_read = StorageStats::default();
+        storage_stats
+    }
 }
 
 impl<Runtime> Database for DatabaseRuntime<Runtime>
@@ -151,7 +209,14 @@ where
         let Some(val) = val else {
             panic!("There is no storage associated to externally owned account");
         };
-        let key = Self::get_uint256_key(val, index)?;
+        let key = Self::get_linera_key(val, index)?;
+        {
+            let mut storage_stats = self
+                .storage_stats
+                .lock()
+                .expect("The lock should be possible");
+            storage_stats.key_read += 1;
+        }
         let result = {
             let mut runtime = self.runtime.lock().expect("The lock should be possible");
             let promise = runtime.read_value_bytes_new(key)?;
@@ -171,6 +236,10 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut storage_stats = self
+            .storage_stats
+            .lock()
+            .expect("The lock should be possible");
         let mut runtime = self.runtime.lock().expect("The lock should be possible");
         let mut batch = Batch::new();
         let mut list_new_balances = Vec::new();
@@ -207,8 +276,21 @@ where
                     };
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
-                        let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.present_value() == value.original_value() {
+                            storage_stats.key_no_operation += 1;
+                        } else {
+                            let key = Self::get_linera_key(val, *index)?;
+                            if value.original_value() == U256::ZERO {
+                                batch.put_key_value(key, &value.present_value())?;
+                                storage_stats.key_set += 1;
+                            } else if value.present_value() == U256::ZERO {
+                                batch.delete_key(key);
+                                storage_stats.key_release += 1;
+                            } else {
+                                batch.put_key_value(key, &value.present_value())?;
+                                storage_stats.key_reset += 1;
+                            }
+                        }
                     }
                 }
             } else {
@@ -239,7 +321,7 @@ where
 {
     pub fn is_initialized(&self) -> Result<bool, ExecutionError> {
         let mut keys = Vec::new();
-        for key_tag in [KeyTag::ZeroContractAddress, KeyTag::ContractAddress] {
+        for key_tag in [KeyTag::NullAddress, KeyTag::ContractAddress] {
             let key = vec![key_tag as u8, KeyCategory::AccountInfo as u8];
             keys.push(key);
         }
diff --git a/linera-execution/src/evm/revm.rs b/linera-execution/src/evm/revm.rs
index c0edae7af47b..8dfc7291cdd7 100644
--- a/linera-execution/src/evm/revm.rs
+++ b/linera-execution/src/evm/revm.rs
@@ -12,7 +12,7 @@ use linera_base::{
     data_types::{Bytecode, Resources, SendMessageRequest, StreamUpdate},
     ensure,
     identifiers::{ApplicationId, ChainId, StreamName},
-    vm::EvmQuery,
+    vm::{EvmQuery, VmRuntime},
 };
 use num_enum::TryFromPrimitive;
 use revm::{
@@ -35,9 +35,10 @@ use {
 };
 
 use crate::{
-    evm::database::DatabaseRuntime, ContractRuntime, ContractSyncRuntimeHandle, EvmExecutionError,
-    EvmRuntime, ExecutionError, ServiceRuntime, ServiceSyncRuntimeHandle, UserContract,
-    UserContractInstance, UserContractModule, UserService, UserServiceInstance, UserServiceModule,
+    evm::database::{DatabaseRuntime, StorageStats},
+    ContractRuntime, ContractSyncRuntimeHandle, EvmExecutionError, EvmRuntime, ExecutionError,
+    ServiceRuntime, ServiceSyncRuntimeHandle, UserContract, UserContractInstance,
+    UserContractModule, UserService, UserServiceInstance, UserServiceModule,
 };
 
 /// This is the selector of the `execute_message` that should be called
@@ -287,6 +288,14 @@ enum PrecompileTag {
     MessageIsBouncing,
 }
 
+fn get_precompile_output(result: Vec<u8>) -> PrecompileOutput {
+    // The gas usage is set to zero since the proper accounting is done
+    // by the called application
+    let gas_used = 0;
+    let bytes = Bytes::copy_from_slice(&result);
+    PrecompileOutput { gas_used, bytes }
+}
+
 struct GeneralContractCall;
 
 impl<Runtime: ContractRuntime>
@@ -382,11 +391,7 @@ impl GeneralContractCall {
                 _ => Err(format!("{tag:?} is not available in GeneralContractCall")),
             }
         }?;
-        // The gas usage count is done somewhere else.
-        let gas_used = 0;
-        let bytes = Bytes::copy_from_slice(&result);
-        let result = PrecompileOutput { gas_used, bytes };
-        Ok(result)
+        Ok(get_precompile_output(result))
     }
 }
 
@@ -434,11 +439,7 @@ impl GeneralServiceCall {
                     runtime.try_query_application(target, argument)
                 }
                 .map_err(|error| format!("{}", error))?;
-                // We do not know how much gas was used.
-                let gas_used = 0;
-                let bytes = Bytes::copy_from_slice(&result);
-                let result = PrecompileOutput { gas_used, bytes };
-                Ok(result)
+                Ok(get_precompile_output(result))
             }
             _ => Err(format!("{tag:?} is not available in GeneralServiceCall")),
         }
@@ -461,6 +462,17 @@ fn failing_outcome() -> CallOutcome {
     }
 }
 
+fn get_interpreter_result(
+    result: &[u8],
+    inputs: &mut CallInputs,
+) -> Result<InterpreterResult, ExecutionError> {
+    let mut result = bcs::from_bytes::<InterpreterResult>(result)?;
+    // This effectively means that no cost is incurred by the call to another contract.
+    // This is fine since the costs are incurred by the other contract itself.
+    result.gas = Gas::new(inputs.gas_limit);
+    Ok(result)
+}
+
 struct CallInterceptorContract<Runtime> {
     db: DatabaseRuntime<Runtime>,
 }
@@ -506,9 +518,8 @@ impl<Runtime: ContractRuntime> CallInterceptorContract<Runtime> {
             let mut runtime = self.db.runtime.lock().expect("The lock should be possible");
             runtime.try_call_application(authenticated, target, argument)?
         };
-        let result = bcs::from_bytes::<InterpreterResult>(&result)?;
         let call_outcome = CallOutcome {
-            result,
+            result: get_interpreter_result(&result, inputs)?,
             memory_offset: inputs.return_memory_offset.clone(),
         };
         Ok(Some(call_outcome))
@@ -561,9 +572,8 @@ impl<Runtime: ServiceRuntime> CallInterceptorService<Runtime> {
             let mut runtime = self.db.runtime.lock().expect("The lock should be possible");
             runtime.try_query_application(target, evm_query)?
         };
-        let result = bcs::from_bytes::<InterpreterResult>(&result)?;
         let call_outcome = CallOutcome {
-            result,
+            result: get_interpreter_result(&result, inputs)?,
             memory_offset: inputs.return_memory_offset.clone(),
         };
         Ok(Some(call_outcome))
@@ -583,12 +593,13 @@ enum Choice {
 #[derive(Debug)]
 struct ExecutionResultSuccess {
     reason: SuccessReason,
+    gas_final: u64,
     logs: Vec<Log>,
     output: Output,
 }
 
 impl ExecutionResultSuccess {
-    fn interpreter_result_and_logs(self) -> Result<(Vec<u8>, Vec<Log>), ExecutionError> {
+    fn interpreter_result_and_logs(self) -> Result<(u64, Vec<u8>, Vec<Log>), ExecutionError> {
         let result: InstructionResult = self.reason.into();
         let Output::Call(output) = self.output else {
             unreachable!("The Output is not a call which is impossible");
@@ -600,15 +611,15 @@ impl ExecutionResultSuccess {
             gas,
         };
         let result = bcs::to_bytes(&result)?;
-        Ok((result, self.logs))
+        Ok((self.gas_final, result, self.logs))
     }
 
-    fn output_and_logs(self) -> (Vec<u8>, Vec<Log>) {
+    fn output_and_logs(self) -> (u64, Vec<u8>, Vec<Log>) {
         let Output::Call(output) = self.output else {
             unreachable!("It is impossible for a Choice::Call to lead to an Output::Create");
         };
         let output = output.as_ref().to_vec();
-        (output, self.logs)
+        (self.gas_final, output, self.logs)
     }
 }
 
@@ -629,7 +640,7 @@ where
 
     fn execute_operation(&mut self, operation: Vec<u8>) -> Result<Vec<u8>, ExecutionError> {
         ensure_message_length(operation.len(), 4)?;
-        let (output, logs) = if &operation[..4] == INTERPRETER_RESULT_SELECTOR {
+        let (gas_final, output, logs) = if &operation[..4] == INTERPRETER_RESULT_SELECTOR {
             ensure_message_length(operation.len(), 8)?;
             forbid_execute_operation_origin(&operation[4..8])?;
             let result = self.init_transact_commit(Choice::Call, &operation[4..])?;
@@ -640,6 +651,7 @@ where
             let result = self.init_transact_commit(Choice::Call, &operation)?;
             result.output_and_logs()
         };
+        self.consume_fuel(gas_final)?;
         self.write_logs(logs, "operation")?;
         Ok(output)
     }
@@ -647,7 +659,8 @@ where
     fn execute_message(&mut self, message: Vec<u8>) -> Result<(), ExecutionError> {
         let operation = get_revm_execute_message_bytes(message);
         let result = self.init_transact_commit(Choice::Call, &operation)?;
-        let (output, logs) = result.output_and_logs();
+        let (gas_final, output, logs) = result.output_and_logs();
+        self.consume_fuel(gas_final)?;
         self.write_logs(logs, "message")?;
         assert_eq!(output.len(), 0);
         Ok(())
@@ -664,20 +677,27 @@ where
 }
 
 fn process_execution_result(
+    storage_stats: StorageStats,
     result: ExecutionResult,
 ) -> Result<ExecutionResultSuccess, ExecutionError> {
     match result {
         ExecutionResult::Success {
             reason,
-            gas_used: _,
-            gas_refunded: _,
+            gas_used,
+            gas_refunded,
             logs,
             output,
-        } => Ok(ExecutionResultSuccess {
-            reason,
-            logs,
-            output,
-        }),
+        } => {
+            let mut gas_final = gas_used;
+            gas_final -= storage_stats.storage_costs();
+            assert_eq!(gas_refunded, storage_stats.storage_refund());
+            Ok(ExecutionResultSuccess {
+                reason,
+                gas_final,
+                logs,
+                output,
+            })
+        }
         ExecutionResult::Revert { gas_used, output } => {
             let error = EvmExecutionError::Revert { gas_used, output };
             Err(ExecutionError::EvmError(error))
@@ -729,12 +749,12 @@ where
     fn transact_commit(
         &mut self,
         ch: Choice,
-        vec: &[u8],
+        input: &[u8],
     ) -> Result<ExecutionResultSuccess, ExecutionError> {
         let (kind, tx_data) = match ch {
-            Choice::Create => (TxKind::Create, Bytes::copy_from_slice(vec)),
+            Choice::Create => (TxKind::Create, Bytes::copy_from_slice(input)),
             Choice::Call => {
-                let tx_data = Bytes::copy_from_slice(vec);
+                let tx_data = Bytes::copy_from_slice(input);
                 (TxKind::Call(Address::ZERO.create(0)), tx_data)
             }
         };
@@ -742,6 +762,10 @@ where
             db: self.db.clone(),
         };
         let block_env = self.db.get_block_env()?;
+        let gas_limit = {
+            let mut runtime = self.db.runtime.lock().expect("The lock should be possible");
+            runtime.remaining_fuel(VmRuntime::Evm)
+        }?;
         let result = {
             let mut evm: Evm<'_, _, _> = Evm::builder()
                 .with_ref_db(&mut self.db)
@@ -750,6 +774,7 @@ where
                     tx.clear();
                     tx.transact_to = kind;
                     tx.data = tx_data;
+                    tx.gas_limit = gas_limit;
                 })
                 .modify_block_env(|block| {
                     *block = block_env;
@@ -774,8 +799,14 @@ where
                 ExecutionError::EvmError(error)
             })
         }?;
+        let storage_stats = self.db.take_storage_stats();
         self.db.commit_changes()?;
-        process_execution_result(result)
+        process_execution_result(storage_stats, result)
+    }
+
+    fn consume_fuel(&mut self, gas_final: u64) -> Result<(), ExecutionError> {
+        let mut runtime = self.db.runtime.lock().expect("The lock should be possible");
+        runtime.consume_fuel(gas_final, VmRuntime::Evm)
     }
 
     fn write_logs(&mut self, logs: Vec<Log>, origin: &str) -> Result<(), ExecutionError> {
@@ -824,16 +855,18 @@ where
         };
 
         ensure_message_length(query.len(), 4)?;
+        // We drop the logs since the "eth_call" execution does not return any log.
+        // Also, for handle_query, we do not have associated costs.
+        // More generally, there is gas costs associated to service operation.
         let answer = if &query[..4] == INTERPRETER_RESULT_SELECTOR {
             let result = self.init_transact(&query[4..])?;
-            let (answer, _logs) = result.interpreter_result_and_logs()?;
+            let (_gas_final, answer, _logs) = result.interpreter_result_and_logs()?;
             answer
         } else {
             let result = self.init_transact(&query)?;
-            let (output, _logs) = result.output_and_logs();
+            let (_gas_final, output, _logs) = result.output_and_logs();
             serde_json::to_vec(&output)?
         };
-        // We drop the logs since the "eth_call" execution does not return any log.
         Ok(answer)
     }
 }
@@ -855,8 +888,7 @@ where
                 };
                 vec_init.extend_from_slice(&argument);
                 let kind = TxKind::Create;
-                let tx_data = Bytes::copy_from_slice(&vec_init);
-                let (_, changes) = self.transact(kind, tx_data)?;
+                let (_, changes) = self.transact(kind, &vec_init)?;
                 changes
             };
             self.db.changes = changes;
@@ -865,53 +897,62 @@ where
         forbid_execute_operation_origin(&vec[..4])?;
         let contract_address = Address::ZERO.create(0);
         let kind = TxKind::Call(contract_address);
-        let tx_data = Bytes::copy_from_slice(vec);
-        let (execution_result, _) = self.transact(kind, tx_data)?;
+        let (execution_result, _) = self.transact(kind, vec)?;
         Ok(execution_result)
     }
 
     fn transact(
         &mut self,
         kind: TxKind,
-        tx_data: Bytes,
+        input: &[u8],
     ) -> Result<(ExecutionResultSuccess, EvmState), ExecutionError> {
+        let tx_data = Bytes::copy_from_slice(input);
         let mut inspector = CallInterceptorService {
             db: self.db.clone(),
         };
 
         let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        // The runtime costs are not available in service operations.
+        // We need to set a limit to gas usage in order to avoid blocking
+        // the validator.
+        // We set up the limit similarly to Infura to 20 million.
+        let gas_limit = 20_000_000;
+        let result_state = {
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                    tx.gas_limit = gas_limit;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.take_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
     }
diff --git a/linera-execution/src/lib.rs b/linera-execution/src/lib.rs
index 75ed88f442a3..caa29eb98c9d 100644
--- a/linera-execution/src/lib.rs
+++ b/linera-execution/src/lib.rs
@@ -44,6 +44,7 @@ use linera_base::{
     },
     ownership::ChainOwnership,
     task,
+    vm::VmRuntime,
 };
 use linera_views::{batch::Batch, views::ViewError};
 use serde::{Deserialize, Serialize};
@@ -226,8 +227,8 @@ pub enum ExecutionError {
     ExcessiveRead,
     #[error("Excessive number of bytes written to storage")]
     ExcessiveWrite,
-    #[error("Block execution required too much fuel")]
-    MaximumFuelExceeded,
+    #[error("Block execution required too much fuel for VM {0}")]
+    MaximumFuelExceeded(VmRuntime),
     #[error("Services running as oracles in block took longer than allowed")]
     MaximumServiceOracleExecutionTimeExceeded,
     #[error("Service running as an oracle produced a response that's too large")]
@@ -719,10 +720,10 @@ pub trait ContractRuntime: BaseRuntime {
     fn authenticated_caller_id(&mut self) -> Result<Option<ApplicationId>, ExecutionError>;
 
     /// Returns the amount of execution fuel remaining before execution is aborted.
-    fn remaining_fuel(&mut self) -> Result<u64, ExecutionError>;
+    fn remaining_fuel(&mut self, vm_runtime: VmRuntime) -> Result<u64, ExecutionError>;
 
     /// Consumes some of the execution fuel.
-    fn consume_fuel(&mut self, fuel: u64) -> Result<(), ExecutionError>;
+    fn consume_fuel(&mut self, fuel: u64, vm_runtime: VmRuntime) -> Result<(), ExecutionError>;
 
     /// Schedules a message to be sent.
     fn send_message(&mut self, message: SendMessageRequest<Vec<u8>>) -> Result<(), ExecutionError>;
diff --git a/linera-execution/src/policy.rs b/linera-execution/src/policy.rs
index f512fa6c2df6..2cf914335ce3 100644
--- a/linera-execution/src/policy.rs
+++ b/linera-execution/src/policy.rs
@@ -10,6 +10,7 @@ use linera_base::{
     data_types::{Amount, ArithmeticError, BlobContent, CompressedBytecode, Resources},
     ensure,
     identifiers::BlobType,
+    vm::VmRuntime,
 };
 use serde::{Deserialize, Serialize};
 
@@ -20,8 +21,10 @@ use crate::ExecutionError;
 pub struct ResourceControlPolicy {
     /// The base price for creating a new block.
     pub block: Amount,
-    /// The price per unit of fuel (aka gas) for VM execution.
-    pub fuel_unit: Amount,
+    /// The price per unit of fuel (aka gas) for Wasm execution.
+    pub wasm_fuel_unit: Amount,
+    /// The price per unit of fuel (aka gas) for EVM execution.
+    pub evm_fuel_unit: Amount,
     /// The price of one read operation.
     pub read_operation: Amount,
     /// The price of one write operation.
@@ -56,8 +59,10 @@ pub struct ResourceControlPolicy {
 
     // TODO(#1538): Cap the number of transactions per block and the total size of their
     // arguments.
-    /// The maximum amount of fuel a block can consume.
-    pub maximum_fuel_per_block: u64,
+    /// The maximum amount of Wasm fuel a block can consume.
+    pub maximum_wasm_fuel_per_block: u64,
+    /// The maximum amount of EVM fuel a block can consume.
+    pub maximum_evm_fuel_per_block: u64,
     /// The maximum time in milliseconds that a block can spend executing services as oracles.
     pub maximum_service_oracle_execution_ms: u64,
     /// The maximum size of a block. This includes the block proposal itself as well as
@@ -89,7 +94,8 @@ impl fmt::Display for ResourceControlPolicy {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let ResourceControlPolicy {
             block,
-            fuel_unit,
+            wasm_fuel_unit,
+            evm_fuel_unit,
             read_operation,
             write_operation,
             byte_read,
@@ -105,7 +111,8 @@ impl fmt::Display for ResourceControlPolicy {
             message_byte,
             service_as_oracle_query,
             http_request,
-            maximum_fuel_per_block,
+            maximum_wasm_fuel_per_block,
+            maximum_evm_fuel_per_block,
             maximum_service_oracle_execution_ms,
             maximum_block_size,
             maximum_blob_size,
@@ -123,7 +130,8 @@ impl fmt::Display for ResourceControlPolicy {
             f,
             "Resource control policy:\n\
             {block:.2} base cost per block\n\
-            {fuel_unit:.2} cost per fuel unit\n\
+            {wasm_fuel_unit:.2} cost per Wasm fuel unit\n\
+            {evm_fuel_unit:.2} cost per EVM fuel unit\n\
             {read_operation:.2} cost per read operation\n\
             {write_operation:.2} cost per write operation\n\
             {byte_read:.2} cost per byte read\n\
@@ -139,7 +147,8 @@ impl fmt::Display for ResourceControlPolicy {
             {message:.2} per outgoing messages\n\
             {message_byte:.2} per byte in the argument of an outgoing messages\n\
             {http_request:.2} per HTTP request performed\n\
-            {maximum_fuel_per_block} maximum fuel per block\n\
+            {maximum_wasm_fuel_per_block} maximum Wasm fuel per block\n\
+            {maximum_evm_fuel_per_block} maximum EVM fuel per block\n\
             {maximum_service_oracle_execution_ms} ms maximum service-as-oracle execution time per \
                 block\n\
             {maximum_block_size} maximum size of a block\n\
@@ -171,7 +180,8 @@ impl ResourceControlPolicy {
     pub fn no_fees() -> Self {
         Self {
             block: Amount::ZERO,
-            fuel_unit: Amount::ZERO,
+            wasm_fuel_unit: Amount::ZERO,
+            evm_fuel_unit: Amount::ZERO,
             read_operation: Amount::ZERO,
             write_operation: Amount::ZERO,
             byte_read: Amount::ZERO,
@@ -187,7 +197,8 @@ impl ResourceControlPolicy {
             message_byte: Amount::ZERO,
             service_as_oracle_query: Amount::ZERO,
             http_request: Amount::ZERO,
-            maximum_fuel_per_block: u64::MAX,
+            maximum_wasm_fuel_per_block: u64::MAX,
+            maximum_evm_fuel_per_block: u64::MAX,
             maximum_service_oracle_execution_ms: u64::MAX,
             maximum_block_size: u64::MAX,
             maximum_blob_size: u64::MAX,
@@ -203,13 +214,22 @@ impl ResourceControlPolicy {
         }
     }
 
+    /// The maximum fuel per block according to the `VmRuntime`.
+    pub fn maximum_fuel_per_block(&self, vm_runtime: VmRuntime) -> u64 {
+        match vm_runtime {
+            VmRuntime::Wasm => self.maximum_wasm_fuel_per_block,
+            VmRuntime::Evm => self.maximum_evm_fuel_per_block,
+        }
+    }
+
     /// Creates a policy with no cost for anything except fuel.
     ///
     /// This can be used in tests that need whole numbers in their chain balance.
     #[cfg(with_testing)]
     pub fn only_fuel() -> Self {
         Self {
-            fuel_unit: Amount::from_micros(1),
+            wasm_fuel_unit: Amount::from_micros(1),
+            evm_fuel_unit: Amount::from_micros(1),
             ..Self::no_fees()
         }
     }
@@ -221,7 +241,8 @@ impl ResourceControlPolicy {
     pub fn fuel_and_block() -> Self {
         Self {
             block: Amount::from_millis(1),
-            fuel_unit: Amount::from_micros(1),
+            wasm_fuel_unit: Amount::from_micros(1),
+            evm_fuel_unit: Amount::from_micros(1),
             ..Self::no_fees()
         }
     }
@@ -231,7 +252,8 @@ impl ResourceControlPolicy {
     pub fn all_categories() -> Self {
         Self {
             block: Amount::from_millis(1),
-            fuel_unit: Amount::from_nanos(1),
+            wasm_fuel_unit: Amount::from_nanos(1),
+            evm_fuel_unit: Amount::from_nanos(1),
             byte_read: Amount::from_attos(100),
             byte_written: Amount::from_attos(1_000),
             blob_read: Amount::from_nanos(1),
@@ -251,7 +273,8 @@ impl ResourceControlPolicy {
     pub fn testnet() -> Self {
         Self {
             block: Amount::from_millis(1),
-            fuel_unit: Amount::from_nanos(10),
+            wasm_fuel_unit: Amount::from_nanos(10),
+            evm_fuel_unit: Amount::from_nanos(10),
             byte_read: Amount::from_nanos(10),
             byte_written: Amount::from_nanos(100),
             blob_read: Amount::from_nanos(100),
@@ -267,7 +290,8 @@ impl ResourceControlPolicy {
             message: Amount::from_micros(10),
             service_as_oracle_query: Amount::from_millis(10),
             http_request: Amount::from_micros(50),
-            maximum_fuel_per_block: 100_000_000,
+            maximum_wasm_fuel_per_block: 100_000_000,
+            maximum_evm_fuel_per_block: 100_000_000,
             maximum_service_oracle_execution_ms: 10_000,
             maximum_block_size: 1_000_000,
             maximum_blob_size: 1_000_000,
@@ -289,7 +313,8 @@ impl ResourceControlPolicy {
 
     pub fn total_price(&self, resources: &Resources) -> Result<Amount, ArithmeticError> {
         let mut amount = Amount::ZERO;
-        amount.try_add_assign(self.fuel_price(resources.fuel)?)?;
+        amount.try_add_assign(self.fuel_price(resources.wasm_fuel, VmRuntime::Wasm)?)?;
+        amount.try_add_assign(self.fuel_price(resources.evm_fuel, VmRuntime::Evm)?)?;
         amount.try_add_assign(self.read_operations_price(resources.read_operations)?)?;
         amount.try_add_assign(self.write_operations_price(resources.write_operations)?)?;
         amount.try_add_assign(self.bytes_read_price(resources.bytes_to_read as u64)?)?;
@@ -371,13 +396,25 @@ impl ResourceControlPolicy {
         self.http_request.try_mul(count as u128)
     }
 
-    pub(crate) fn fuel_price(&self, fuel: u64) -> Result<Amount, ArithmeticError> {
-        self.fuel_unit.try_mul(u128::from(fuel))
+    fn fuel_unit_price(&self, vm_runtime: VmRuntime) -> Amount {
+        match vm_runtime {
+            VmRuntime::Wasm => self.wasm_fuel_unit,
+            VmRuntime::Evm => self.evm_fuel_unit,
+        }
+    }
+
+    pub(crate) fn fuel_price(
+        &self,
+        fuel: u64,
+        vm_runtime: VmRuntime,
+    ) -> Result<Amount, ArithmeticError> {
+        self.fuel_unit_price(vm_runtime).try_mul(u128::from(fuel))
     }
 
     /// Returns how much fuel can be paid with the given balance.
-    pub(crate) fn remaining_fuel(&self, balance: Amount) -> u64 {
-        u64::try_from(balance.saturating_div(self.fuel_unit)).unwrap_or(u64::MAX)
+    pub(crate) fn remaining_fuel(&self, balance: Amount, vm_runtime: VmRuntime) -> u64 {
+        let fuel_unit = self.fuel_unit_price(vm_runtime);
+        u64::try_from(balance.saturating_div(fuel_unit)).unwrap_or(u64::MAX)
     }
 
     pub fn check_blob_size(&self, content: &BlobContent) -> Result<(), ExecutionError> {
diff --git a/linera-execution/src/resources.rs b/linera-execution/src/resources.rs
index b28f5c3c5e72..ba4fdaecd464 100644
--- a/linera-execution/src/resources.rs
+++ b/linera-execution/src/resources.rs
@@ -10,6 +10,7 @@ use linera_base::{
     data_types::{Amount, ArithmeticError, BlobContent},
     ensure,
     identifiers::AccountOwner,
+    vm::VmRuntime,
 };
 use linera_views::{context::Context, views::ViewError};
 use serde::Serialize;
@@ -33,8 +34,10 @@ pub struct ResourceTracker {
     pub blocks: u32,
     /// The total size of the block so far.
     pub block_size: u64,
-    /// The fuel used so far.
-    pub fuel: u64,
+    /// The EVM fuel used so far.
+    pub evm_fuel: u64,
+    /// The Wasm fuel used so far.
+    pub wasm_fuel: u64,
     /// The number of read operations.
     pub read_operations: u32,
     /// The number of write operations.
@@ -71,6 +74,15 @@ pub struct ResourceTracker {
     pub grants: Amount,
 }
 
+impl ResourceTracker {
+    fn fuel(&self, vm_runtime: VmRuntime) -> u64 {
+        match vm_runtime {
+            VmRuntime::Wasm => self.wasm_fuel,
+            VmRuntime::Evm => self.evm_fuel,
+        }
+    }
+}
+
 /// How to access the balance of an account.
 pub trait BalanceHolder {
     fn balance(&self) -> Result<Amount, ArithmeticError>;
@@ -119,14 +131,13 @@ where
     }
 
     /// Obtains the amount of fuel that could be spent by consuming the entire balance.
-    pub(crate) fn remaining_fuel(&self) -> u64 {
+    pub(crate) fn remaining_fuel(&self, vm_runtime: VmRuntime) -> u64 {
+        let balance = self.balance().unwrap_or(Amount::MAX);
+        let fuel = self.tracker.as_ref().fuel(vm_runtime);
+        let maximum_fuel_per_block = self.policy.maximum_fuel_per_block(vm_runtime);
         self.policy
-            .remaining_fuel(self.balance().unwrap_or(Amount::MAX))
-            .min(
-                self.policy
-                    .maximum_fuel_per_block
-                    .saturating_sub(self.tracker.as_ref().fuel),
-            )
+            .remaining_fuel(balance, vm_runtime)
+            .min(maximum_fuel_per_block.saturating_sub(fuel))
     }
 
     /// Tracks the allocation of a grant.
@@ -208,18 +219,38 @@ where
     }
 
     /// Tracks a number of fuel units used.
-    pub(crate) fn track_fuel(&mut self, fuel: u64) -> Result<(), ExecutionError> {
-        self.tracker.as_mut().fuel = self
-            .tracker
-            .as_ref()
-            .fuel
-            .checked_add(fuel)
-            .ok_or(ArithmeticError::Overflow)?;
-        ensure!(
-            self.tracker.as_ref().fuel <= self.policy.maximum_fuel_per_block,
-            ExecutionError::MaximumFuelExceeded
-        );
-        self.update_balance(self.policy.fuel_price(fuel)?)
+    pub(crate) fn track_fuel(
+        &mut self,
+        fuel: u64,
+        vm_runtime: VmRuntime,
+    ) -> Result<(), ExecutionError> {
+        match vm_runtime {
+            VmRuntime::Wasm => {
+                self.tracker.as_mut().wasm_fuel = self
+                    .tracker
+                    .as_ref()
+                    .wasm_fuel
+                    .checked_add(fuel)
+                    .ok_or(ArithmeticError::Overflow)?;
+                ensure!(
+                    self.tracker.as_ref().wasm_fuel <= self.policy.maximum_wasm_fuel_per_block,
+                    ExecutionError::MaximumFuelExceeded(vm_runtime)
+                );
+            }
+            VmRuntime::Evm => {
+                self.tracker.as_mut().evm_fuel = self
+                    .tracker
+                    .as_ref()
+                    .evm_fuel
+                    .checked_add(fuel)
+                    .ok_or(ArithmeticError::Overflow)?;
+                ensure!(
+                    self.tracker.as_ref().evm_fuel <= self.policy.maximum_evm_fuel_per_block,
+                    ExecutionError::MaximumFuelExceeded(vm_runtime)
+                );
+            }
+        }
+        self.update_balance(self.policy.fuel_price(fuel, vm_runtime)?)
     }
 
     /// Tracks a read operation.
diff --git a/linera-execution/src/runtime.rs b/linera-execution/src/runtime.rs
index f43f20872e62..e74e2e48083f 100644
--- a/linera-execution/src/runtime.rs
+++ b/linera-execution/src/runtime.rs
@@ -22,6 +22,7 @@ use linera_base::{
         StreamId, StreamName,
     },
     ownership::ChainOwnership,
+    vm::VmRuntime,
 };
 use linera_views::batch::Batch;
 use oneshot::Receiver;
@@ -1173,13 +1174,13 @@ impl ContractRuntime for ContractSyncRuntimeHandle {
         Ok(this.current_application().caller_id)
     }
 
-    fn remaining_fuel(&mut self) -> Result<u64, ExecutionError> {
-        Ok(self.inner().resource_controller.remaining_fuel())
+    fn remaining_fuel(&mut self, vm_runtime: VmRuntime) -> Result<u64, ExecutionError> {
+        Ok(self.inner().resource_controller.remaining_fuel(vm_runtime))
     }
 
-    fn consume_fuel(&mut self, fuel: u64) -> Result<(), ExecutionError> {
+    fn consume_fuel(&mut self, fuel: u64, vm_runtime: VmRuntime) -> Result<(), ExecutionError> {
         let mut this = self.inner();
-        this.resource_controller.track_fuel(fuel)
+        this.resource_controller.track_fuel(fuel, vm_runtime)
     }
 
     fn send_message(&mut self, message: SendMessageRequest<Vec<u8>>) -> Result<(), ExecutionError> {
diff --git a/linera-execution/src/wasm/runtime_api.rs b/linera-execution/src/wasm/runtime_api.rs
index d4c62e080e52..60c2e7c0dfae 100644
--- a/linera-execution/src/wasm/runtime_api.rs
+++ b/linera-execution/src/wasm/runtime_api.rs
@@ -9,6 +9,7 @@ use linera_base::{
     http,
     identifiers::{Account, AccountOwner, ApplicationId, ChainId, MessageId, StreamName},
     ownership::{ChainOwnership, ChangeApplicationPermissionsError, CloseChainError},
+    vm::VmRuntime,
 };
 use linera_views::batch::{Batch, WriteOperation};
 use linera_witty::{wit_export, Instance, RuntimeError};
@@ -618,7 +619,7 @@ where
         caller
             .user_data_mut()
             .runtime_mut()
-            .consume_fuel(fuel)
+            .consume_fuel(fuel, VmRuntime::Wasm)
             .map_err(|e| RuntimeError::Custom(e.into()))
     }
 
diff --git a/linera-execution/tests/fee_consumption.rs b/linera-execution/tests/fee_consumption.rs
index 16cf0f7c2aa3..5273ebabca97 100644
--- a/linera-execution/tests/fee_consumption.rs
+++ b/linera-execution/tests/fee_consumption.rs
@@ -12,6 +12,7 @@ use linera_base::{
     data_types::{Amount, BlockHeight, OracleResponse},
     http,
     identifiers::{Account, AccountOwner, MessageId},
+    vm::VmRuntime,
 };
 use linera_execution::{
     test_utils::{
@@ -206,7 +207,8 @@ async fn test_fee_consumption(
 
     let prices = ResourceControlPolicy {
         block: Amount::from_tokens(2),
-        fuel_unit: Amount::from_tokens(3),
+        wasm_fuel_unit: Amount::from_tokens(3),
+        evm_fuel_unit: Amount::from_tokens(3),
         read_operation: Amount::from_tokens(5),
         write_operation: Amount::from_tokens(7),
         byte_read: Amount::from_tokens(11),
@@ -218,7 +220,8 @@ async fn test_fee_consumption(
         message_byte: Amount::from_tokens(31),
         service_as_oracle_query: Amount::from_millis(37),
         http_request: Amount::from_tokens(41),
-        maximum_fuel_per_block: 4_868_145_137,
+        maximum_wasm_fuel_per_block: 4_868_145_137,
+        maximum_evm_fuel_per_block: 4_868_145_137,
         maximum_block_size: 43,
         maximum_service_oracle_execution_ms: 47,
         maximum_blob_size: 53,
@@ -381,7 +384,7 @@ impl FeeSpend {
     /// The fee amount required for this runtime operation.
     pub fn amount(&self, policy: &ResourceControlPolicy) -> Amount {
         match self {
-            FeeSpend::Fuel(units) => policy.fuel_unit.saturating_mul(*units as u128),
+            FeeSpend::Fuel(units) => policy.wasm_fuel_unit.saturating_mul(*units as u128),
             FeeSpend::Read(_key, value) => {
                 let value_read_fee = value
                     .as_ref()
@@ -398,7 +401,7 @@ impl FeeSpend {
     /// Executes the operation with the `runtime`
     pub fn execute(self, runtime: &mut impl ContractRuntime) -> Result<(), ExecutionError> {
         match self {
-            FeeSpend::Fuel(units) => runtime.consume_fuel(units),
+            FeeSpend::Fuel(units) => runtime.consume_fuel(units, VmRuntime::Wasm),
             FeeSpend::Read(key, value) => {
                 let promise = runtime.read_value_bytes_new(key)?;
                 let response = runtime.read_value_bytes_wait(&promise)?;
diff --git a/linera-execution/tests/fixtures/evm_too_long_service.sol b/linera-execution/tests/fixtures/evm_too_long_service.sol
new file mode 100644
index 000000000000..f8984713e96d
--- /dev/null
+++ b/linera-execution/tests/fixtures/evm_too_long_service.sol
@@ -0,0 +1,19 @@
+// Copyright (c) Zefchain Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.0;
+
+contract ExampleLongService {
+    constructor(uint64 start_value) {
+    }
+
+    // An operation that costs more than 20.000.000 gas fuel.
+    function too_long_run() external view returns (uint256) {
+       uint256 v = 0;
+       for (uint256 i=0; i<500; i++) {
+           for (uint256 j=0; j<500; j++) {
+               v = v + i + j;
+           }
+       }
+       return v;
+    }
+}
diff --git a/linera-execution/tests/revm.rs b/linera-execution/tests/revm.rs
index a631433b6e12..7a7e13bc5e7a 100644
--- a/linera-execution/tests/revm.rs
+++ b/linera-execution/tests/revm.rs
@@ -98,7 +98,7 @@ async fn test_fuel_for_counter_revm_application() -> anyhow::Result<()> {
 
     let increments = [2_u64, 9_u64, 7_u64, 1000_u64];
     let policy = ResourceControlPolicy {
-        fuel_unit: Amount::from_attos(1),
+        evm_fuel_unit: Amount::from_attos(1),
         ..ResourceControlPolicy::default()
     };
     let amount = Amount::from_tokens(1);
@@ -150,3 +150,187 @@ async fn test_fuel_for_counter_revm_application() -> anyhow::Result<()> {
     }
     Ok(())
 }
+
+#[tokio::test]
+async fn test_terminate_execute_operation_by_lack_of_fuel() -> anyhow::Result<()> {
+    let module = load_solidity_example("tests/fixtures/evm_example_counter.sol")?;
+
+    sol! {
+        struct ConstructorArgs {
+            uint64 initial_value;
+        }
+        function increment(uint64 input);
+        function get_value();
+    }
+
+    let initial_value = 10000;
+    let args = ConstructorArgs { initial_value };
+    let constructor_argument = args.abi_encode();
+    let constructor_argument = serde_json::to_string(&constructor_argument)?.into_bytes();
+    let instantiation_argument = Vec::<u8>::new();
+    let instantiation_argument = serde_json::to_string(&instantiation_argument)?.into_bytes();
+    let state = SystemExecutionState {
+        description: Some(dummy_chain_description(0)),
+        ..Default::default()
+    };
+    let (mut app_desc, contract_blob, service_blob) = create_dummy_user_application_description(1);
+    app_desc.parameters = constructor_argument;
+    let chain_id = app_desc.creator_chain_id;
+    let mut view = state
+        .into_view_with(chain_id, ExecutionRuntimeConfig::default())
+        .await;
+    let app_id = From::from(&app_desc);
+    let app_desc_blob_id = Blob::new_application_description(&app_desc).id();
+    let contract_blob_id = contract_blob.id();
+    let service_blob_id = service_blob.id();
+
+    let contract = EvmContractModule::Revm {
+        module: module.clone(),
+    };
+    view.context()
+        .extra()
+        .user_contracts()
+        .insert(app_id, contract.clone().into());
+
+    let service = EvmServiceModule::Revm { module };
+    view.context()
+        .extra()
+        .user_services()
+        .insert(app_id, service.into());
+
+    view.simulate_instantiation(
+        contract.into(),
+        Timestamp::from(2),
+        app_desc,
+        instantiation_argument,
+        contract_blob,
+        service_blob,
+    )
+    .await?;
+
+    let operation_context = OperationContext {
+        chain_id,
+        height: BlockHeight(0),
+        round: Some(0),
+        authenticated_signer: None,
+        authenticated_caller_id: None,
+        timestamp: Default::default(),
+    };
+
+    let policy = ResourceControlPolicy {
+        evm_fuel_unit: Amount::from_attos(1),
+        maximum_evm_fuel_per_block: 20000,
+        ..ResourceControlPolicy::default()
+    };
+    let amount = Amount::from_tokens(1);
+    *view.system.balance.get_mut() = amount;
+    let mut controller = ResourceController {
+        policy: Arc::new(policy),
+        tracker: ResourceTracker::default(),
+        account: None,
+    };
+
+    // Trying the increment, should fail
+    let mut txn_tracker = TransactionTracker::new_replaying_blobs([
+        app_desc_blob_id,
+        contract_blob_id,
+        service_blob_id,
+    ]);
+    let input = 2;
+    let operation = incrementCall { input };
+    let bytes = operation.abi_encode();
+    let operation = Operation::User {
+        application_id: app_id,
+        bytes,
+    };
+    let result = view
+        .execute_operation(
+            operation_context,
+            operation,
+            &mut txn_tracker,
+            &mut controller,
+        )
+        .await;
+
+    assert!(result.is_err());
+
+    Ok(())
+}
+
+#[tokio::test]
+async fn test_terminate_query_by_lack_of_fuel() -> anyhow::Result<()> {
+    let module = load_solidity_example("tests/fixtures/evm_too_long_service.sol")?;
+
+    sol! {
+        struct ConstructorArgs {
+            uint64 initial_value;
+        }
+        function too_long_run();
+    }
+
+    let args = ConstructorArgs { initial_value: 0 };
+    let constructor_argument = args.abi_encode();
+    let constructor_argument = serde_json::to_string(&constructor_argument)?.into_bytes();
+    let instantiation_argument = Vec::<u8>::new();
+    let instantiation_argument = serde_json::to_string(&instantiation_argument)?.into_bytes();
+    let state = SystemExecutionState {
+        description: Some(dummy_chain_description(0)),
+        ..Default::default()
+    };
+    let (mut app_desc, contract_blob, service_blob) = create_dummy_user_application_description(1);
+    app_desc.parameters = constructor_argument;
+    let chain_id = app_desc.creator_chain_id;
+    let mut view = state
+        .into_view_with(chain_id, ExecutionRuntimeConfig::default())
+        .await;
+    let app_id = From::from(&app_desc);
+
+    let contract = EvmContractModule::Revm {
+        module: module.clone(),
+    };
+    view.context()
+        .extra()
+        .user_contracts()
+        .insert(app_id, contract.clone().into());
+
+    let service = EvmServiceModule::Revm { module };
+    view.context()
+        .extra()
+        .user_services()
+        .insert(app_id, service.into());
+
+    view.simulate_instantiation(
+        contract.into(),
+        Timestamp::from(2),
+        app_desc,
+        instantiation_argument,
+        contract_blob,
+        service_blob,
+    )
+    .await?;
+
+    let query_context = QueryContext {
+        chain_id,
+        next_block_height: BlockHeight(0),
+        local_time: Timestamp::from(0),
+    };
+
+    let amount = Amount::from_tokens(1);
+    *view.system.balance.get_mut() = amount;
+
+    // Trying to read the value, should fail
+    let query = too_long_runCall {};
+    let query = query.abi_encode();
+    let query = EvmQuery::Query(query);
+    let bytes = serde_json::to_vec(&query)?;
+
+    let query = Query::User {
+        application_id: app_id,
+        bytes,
+    };
+
+    let result = view.query_application(query_context, query, None).await;
+    assert!(result.is_err());
+
+    Ok(())
+}
diff --git a/linera-execution/tests/wasm.rs b/linera-execution/tests/wasm.rs
index 01bf7cb2cc2c..b8124944fa0b 100644
--- a/linera-execution/tests/wasm.rs
+++ b/linera-execution/tests/wasm.rs
@@ -77,7 +77,7 @@ async fn test_fuel_for_counter_wasm_application(
     };
     let increments = [2_u64, 9, 7, 1000];
     let policy = ResourceControlPolicy {
-        fuel_unit: Amount::from_attos(1),
+        wasm_fuel_unit: Amount::from_attos(1),
         ..ResourceControlPolicy::default()
     };
     let amount = Amount::from_tokens(1);
@@ -104,7 +104,7 @@ async fn test_fuel_for_counter_wasm_application(
         let txn_outcome = txn_tracker.into_outcome().unwrap();
         assert!(txn_outcome.outgoing_messages.is_empty());
     }
-    assert_eq!(controller.tracker.fuel, expected_fuel);
+    assert_eq!(controller.tracker.wasm_fuel, expected_fuel);
     assert_eq!(
         controller
             .with_state(&mut view.system)
diff --git a/linera-rpc/tests/snapshots/format__format.yaml.snap b/linera-rpc/tests/snapshots/format__format.yaml.snap
index de519a6f3a61..fa66640d5e1e 100644
--- a/linera-rpc/tests/snapshots/format__format.yaml.snap
+++ b/linera-rpc/tests/snapshots/format__format.yaml.snap
@@ -851,7 +851,9 @@ ResourceControlPolicy:
   STRUCT:
     - block:
         TYPENAME: Amount
-    - fuel_unit:
+    - wasm_fuel_unit:
+        TYPENAME: Amount
+    - evm_fuel_unit:
         TYPENAME: Amount
     - read_operation:
         TYPENAME: Amount
@@ -883,7 +885,8 @@ ResourceControlPolicy:
         TYPENAME: Amount
     - http_request:
         TYPENAME: Amount
-    - maximum_fuel_per_block: U64
+    - maximum_wasm_fuel_per_block: U64
+    - maximum_evm_fuel_per_block: U64
     - maximum_service_oracle_execution_ms: U64
     - maximum_block_size: U64
     - maximum_bytecode_size: U64
diff --git a/linera-sdk/src/contract/conversions_to_wit.rs b/linera-sdk/src/contract/conversions_to_wit.rs
index 8538cf1e4efd..4967d3fed869 100644
--- a/linera-sdk/src/contract/conversions_to_wit.rs
+++ b/linera-sdk/src/contract/conversions_to_wit.rs
@@ -132,7 +132,8 @@ impl From<ApplicationId> for wit_contract_api::ApplicationId {
 impl From<Resources> for wit_contract_api::Resources {
     fn from(resources: Resources) -> Self {
         wit_contract_api::Resources {
-            fuel: resources.fuel,
+            wasm_fuel: resources.wasm_fuel,
+            evm_fuel: resources.evm_fuel,
             read_operations: resources.read_operations,
             write_operations: resources.write_operations,
             bytes_to_read: resources.bytes_to_read,
diff --git a/linera-sdk/wit/contract-runtime-api.wit b/linera-sdk/wit/contract-runtime-api.wit
index bff1b7e5256f..8c0445ef233b 100644
--- a/linera-sdk/wit/contract-runtime-api.wit
+++ b/linera-sdk/wit/contract-runtime-api.wit
@@ -100,7 +100,8 @@ interface contract-runtime-api {
     }
 
     record resources {
-        fuel: u64,
+        wasm-fuel: u64,
+        evm-fuel: u64,
         read-operations: u32,
         write-operations: u32,
         bytes-to-read: u32,
diff --git a/linera-service-graphql-client/gql/service_schema.graphql b/linera-service-graphql-client/gql/service_schema.graphql
index 15f96bf50a62..e81981ee4bce 100644
--- a/linera-service-graphql-client/gql/service_schema.graphql
+++ b/linera-service-graphql-client/gql/service_schema.graphql
@@ -967,9 +967,13 @@ input ResourceControlPolicy {
 	"""
 	block: Amount!
 	"""
-	The price per unit of fuel (aka gas) for VM execution.
+	The price per unit of fuel (aka gas) for Wasm execution.
 	"""
-	fuelUnit: Amount!
+	wasmFuelUnit: Amount!
+	"""
+	The price per unit of fuel (aka gas) for EVM execution.
+	"""
+	evmFuelUnit: Amount!
 	"""
 	The price of one read operation.
 	"""
@@ -1031,9 +1035,13 @@ input ResourceControlPolicy {
 	"""
 	httpRequest: Amount!
 	"""
-	The maximum amount of fuel a block can consume.
+	The maximum amount of Wasm fuel a block can consume.
+	"""
+	maximumWasmFuelPerBlock: Int!
+	"""
+	The maximum amount of EVM fuel a block can consume.
 	"""
-	maximumFuelPerBlock: Int!
+	maximumEvmFuelPerBlock: Int!
 	"""
 	The maximum time in milliseconds that a block can spend executing services as oracles.
 	"""
diff --git a/linera-service/src/linera/command.rs b/linera-service/src/linera/command.rs
index 4029c8cd2f98..10eeb68feef5 100644
--- a/linera-service/src/linera/command.rs
+++ b/linera-service/src/linera/command.rs
@@ -239,9 +239,13 @@ pub enum ClientCommand {
         #[arg(long)]
         block: Option<Amount>,
 
-        /// Set the price per unit of fuel.
+        /// Set the price per unit of Wasm fuel.
         #[arg(long)]
-        fuel_unit: Option<Amount>,
+        wasm_fuel_unit: Option<Amount>,
+
+        /// Set the price per unit of EVM fuel.
+        #[arg(long)]
+        evm_fuel_unit: Option<Amount>,
 
         /// Set the price per read operation.
         #[arg(long)]
@@ -303,9 +307,13 @@ pub enum ClientCommand {
         #[arg(long)]
         http_request: Option<Amount>,
 
-        /// Set the maximum amount of fuel per block.
+        /// Set the maximum amount of Wasm fuel per block.
         #[arg(long)]
-        maximum_fuel_per_block: Option<u64>,
+        maximum_wasm_fuel_per_block: Option<u64>,
+
+        /// Set the maximum amount of EVM fuel per block.
+        #[arg(long)]
+        maximum_evm_fuel_per_block: Option<u64>,
 
         /// Set the maximum time in milliseconds that a block can spend executing services as oracles.
         #[arg(long)]
@@ -442,10 +450,15 @@ pub enum ClientCommand {
         #[arg(long)]
         block_price: Option<Amount>,
 
-        /// Set the price per unit of fuel.
+        /// Set the price per unit of Wasm fuel.
+        /// (This will overwrite value from `--policy-config`)
+        #[arg(long)]
+        wasm_fuel_unit_price: Option<Amount>,
+
+        /// Set the price per unit of EVM fuel.
         /// (This will overwrite value from `--policy-config`)
         #[arg(long)]
-        fuel_unit_price: Option<Amount>,
+        evm_fuel_unit_price: Option<Amount>,
 
         /// Set the price per read operation.
         /// (This will overwrite value from `--policy-config`)
@@ -520,10 +533,15 @@ pub enum ClientCommand {
         #[arg(long)]
         http_request_price: Option<Amount>,
 
-        /// Set the maximum amount of fuel per block.
+        /// Set the maximum amount of Wasm fuel per block.
+        /// (This will overwrite value from `--policy-config`)
+        #[arg(long)]
+        maximum_wasm_fuel_per_block: Option<u64>,
+
+        /// Set the maximum amount of EVM fuel per block.
         /// (This will overwrite value from `--policy-config`)
         #[arg(long)]
-        maximum_fuel_per_block: Option<u64>,
+        maximum_evm_fuel_per_block: Option<u64>,
 
         /// Set the maximum time in milliseconds that a block can spend executing services as oracles.
         #[arg(long)]
diff --git a/linera-service/src/linera/main.rs b/linera-service/src/linera/main.rs
index 73c3d3000f07..1f97879e8a41 100644
--- a/linera-service/src/linera/main.rs
+++ b/linera-service/src/linera/main.rs
@@ -688,7 +688,8 @@ impl Runnable for Job {
                                 }
                                 ResourceControlPolicy {
                                     block,
-                                    fuel_unit,
+                                    wasm_fuel_unit,
+                                    evm_fuel_unit,
                                     read_operation,
                                     write_operation,
                                     byte_read,
@@ -704,7 +705,8 @@ impl Runnable for Job {
                                     message_byte,
                                     service_as_oracle_query,
                                     http_request,
-                                    maximum_fuel_per_block,
+                                    maximum_wasm_fuel_per_block,
+                                    maximum_evm_fuel_per_block,
                                     maximum_service_oracle_execution_ms,
                                     maximum_block_size,
                                     maximum_blob_size,
@@ -721,7 +723,10 @@ impl Runnable for Job {
                                     let existing_policy = policy.clone();
                                     policy = linera_execution::ResourceControlPolicy {
                                         block: block.unwrap_or(existing_policy.block),
-                                        fuel_unit: fuel_unit.unwrap_or(existing_policy.fuel_unit),
+                                        wasm_fuel_unit: wasm_fuel_unit
+                                            .unwrap_or(existing_policy.wasm_fuel_unit),
+                                        evm_fuel_unit: evm_fuel_unit
+                                            .unwrap_or(existing_policy.evm_fuel_unit),
                                         read_operation: read_operation
                                             .unwrap_or(existing_policy.read_operation),
                                         write_operation: write_operation
@@ -748,8 +753,10 @@ impl Runnable for Job {
                                             .unwrap_or(existing_policy.service_as_oracle_query),
                                         http_request: http_request
                                             .unwrap_or(existing_policy.http_request),
-                                        maximum_fuel_per_block: maximum_fuel_per_block
-                                            .unwrap_or(existing_policy.maximum_fuel_per_block),
+                                        maximum_wasm_fuel_per_block: maximum_wasm_fuel_per_block
+                                            .unwrap_or(existing_policy.maximum_wasm_fuel_per_block),
+                                        maximum_evm_fuel_per_block: maximum_evm_fuel_per_block
+                                            .unwrap_or(existing_policy.maximum_evm_fuel_per_block),
                                         maximum_service_oracle_execution_ms:
                                             maximum_service_oracle_execution_ms.unwrap_or(
                                                 existing_policy.maximum_service_oracle_execution_ms,
@@ -1832,7 +1839,8 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
             num_other_initial_chains,
             policy_config,
             block_price,
-            fuel_unit_price,
+            wasm_fuel_unit_price,
+            evm_fuel_unit_price,
             read_operation_price,
             write_operation_price,
             byte_read_price,
@@ -1848,7 +1856,8 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
             message_byte_price,
             service_as_oracle_query_price,
             http_request_price,
-            maximum_fuel_per_block,
+            maximum_wasm_fuel_per_block,
+            maximum_evm_fuel_per_block,
             maximum_service_oracle_execution_ms,
             maximum_block_size,
             maximum_blob_size,
@@ -1870,7 +1879,8 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
             let existing_policy = policy_config.into_policy();
             let policy = linera_execution::ResourceControlPolicy {
                 block: block_price.unwrap_or(existing_policy.block),
-                fuel_unit: fuel_unit_price.unwrap_or(existing_policy.fuel_unit),
+                wasm_fuel_unit: wasm_fuel_unit_price.unwrap_or(existing_policy.wasm_fuel_unit),
+                evm_fuel_unit: evm_fuel_unit_price.unwrap_or(existing_policy.evm_fuel_unit),
                 read_operation: read_operation_price.unwrap_or(existing_policy.read_operation),
                 write_operation: write_operation_price.unwrap_or(existing_policy.write_operation),
                 byte_read: byte_read_price.unwrap_or(existing_policy.byte_read),
@@ -1888,8 +1898,10 @@ async fn run(options: &ClientOptions) -> Result<i32, Error> {
                 service_as_oracle_query: service_as_oracle_query_price
                     .unwrap_or(existing_policy.service_as_oracle_query),
                 http_request: http_request_price.unwrap_or(existing_policy.http_request),
-                maximum_fuel_per_block: maximum_fuel_per_block
-                    .unwrap_or(existing_policy.maximum_fuel_per_block),
+                maximum_wasm_fuel_per_block: maximum_wasm_fuel_per_block
+                    .unwrap_or(existing_policy.maximum_wasm_fuel_per_block),
+                maximum_evm_fuel_per_block: maximum_evm_fuel_per_block
+                    .unwrap_or(existing_policy.maximum_evm_fuel_per_block),
                 maximum_service_oracle_execution_ms: maximum_service_oracle_execution_ms
                     .unwrap_or(existing_policy.maximum_service_oracle_execution_ms),
                 maximum_block_size: maximum_block_size


Reviews:
user: login: deuszx
body: First pass.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:18:12Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:29:21Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:31:40Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:38:01Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T12:54:35Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:07:13Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:07:38Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:08:16Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:09:22Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:27:07Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:27:26Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:31:21Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:31:49Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:32:08Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:32:58Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:34:46Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:37:09Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:39:19Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:40:50Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:42:07Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:44:21Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:46:29Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:51:34Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:05:33Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:10:00Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:10:49Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:45:36Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:00:33Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:03:47Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:06:04Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:16:45Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:47:06Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:42:19Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:42:36Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:43:37Z
user: login: deuszx
body: No blockers but we also don't have any tests for these. Do you plan on adding them in the next PR? We should test that:
1. EVM execution is failing with OutOfGas as expected
2. That the gas is properly accounted for when one contract calls another
3. That the errors are propagated back to the caller
4. That Linera service is capped with `max_block_gas` 
5. probably more
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:46:32Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:06:56Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:08:42Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:14:57Z
user: login: deuszx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:16:48Z
user: login: MathieuDutSik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:26:41Z
user: login: deuszx
body: No blockers but I really think these changes should have accompanying tests.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:29:18Z

Review comments:
diff_hunk: @@ -489,7 +490,8 @@ View or update the resource control policy
 * `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
 * `--service-as-oracle-query <SERVICE_AS_ORACLE_QUERY>` — Set the price per query to a service as an oracle
 * `--http-request <HTTP_REQUEST>` — Set the price for performing an HTTP request
-* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
+* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block
+* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block
path: CLI.md
user: login: deuszx
body: How do we deal with a case when a Wasm contract calls EVM contract in the same block? Is that possible? If so, would it mean the block can end up double the size? i.e. using up both Maximum Wasm fuel (for the block) and Maximum Evm fuel.
created_at: 2025-05-03T19:07:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Apparently EIP-3529 removed storage refunds.
created_at: 2025-05-03T19:12:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
     pub runtime: Arc<Mutex<Runtime>>,
     pub changes: EvmState,
 }
 
 impl<Runtime> Clone for DatabaseRuntime<Runtime> {
     fn clone(&self) -> Self {
         Self {
+            storage_stats: self.storage_stats.clone(),
             runtime: self.runtime.clone(),
             changes: self.changes.clone(),
         }
     }
 }
 
+#[repr(u8)]
+enum KeyTag {
+    /// Key prefix for the storage of the zero contract.
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: What is a "zero contract"? There's no such thing officially and it could mean multiple things (`0x00...` address, an uninintialized contract, precompiles).
created_at: 2025-05-03T19:17:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,11 +113,23 @@ impl<Runtime> DatabaseRuntime<Runtime> {
     }
 
     pub fn new(runtime: Runtime) -> Self {
+        let storage_stats = StorageStats::default();
         Self {
+            storage_stats: Arc::new(Mutex::new(storage_stats)),
             runtime: Arc::new(Mutex::new(runtime)),
             changes: HashMap::new(),
         }
     }
+
+    pub fn reset_storage_stats(&self) -> StorageStats {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Public methods should have docs - I'm surprised our CI lets this pass.
created_at: 2025-05-03T19:17:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,11 +113,23 @@ impl<Runtime> DatabaseRuntime<Runtime> {
     }
 
     pub fn new(runtime: Runtime) -> Self {
+        let storage_stats = StorageStats::default();
         Self {
+            storage_stats: Arc::new(Mutex::new(storage_stats)),
             runtime: Arc::new(Mutex::new(runtime)),
             changes: HashMap::new(),
         }
     }
+
+    pub fn reset_storage_stats(&self) -> StorageStats {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Maybe better name is `take_storage_stats`? Similarly to other Rust methods that take the value and replace it with a default.
created_at: 2025-05-03T19:18:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: ```suggestion
/// The cost of storing a non-zero value in the storage for the first time
```
created_at: 2025-05-03T19:20:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: ```suggestion
/// The cost of storing overwriting the storage to a different value.
/// Unlike `SSTORE_COST_SET`, this resuses initialised storage but overwrites it.
```
created_at: 2025-05-03T19:21:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Could we extend the API of this type and add `fn is_new()`? Then these ifs would change to
```rust
if value.is_new() {
  batch.put_key_value(key, &value.present_value())?;
  number_key_set += 1;
} else {
  number_key_set_zero += 1;
}
```
created_at: 2025-05-03T19:24:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
+                                batch.put_key_value(key, &value.present_value())?;
+                                number_key_set += 1;
+                            } else {
+                                number_key_set_zero += 1;
+                            }
+                        } else if value.present_value() != U256::ZERO {
+                            if value.present_value() == value.original_value() {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Similarly here we could extend with `fn is_overwrite` and `is_delete` below.
created_at: 2025-05-03T19:29:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Why does this need to be a `Mutex`? Is it shared concurrently between threads?
created_at: 2025-05-03T19:31:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -227,7 +228,7 @@ pub enum ExecutionError {
     #[error("Excessive number of bytes written to storage")]
     ExcessiveWrite,
     #[error("Block execution required too much fuel")]
-    MaximumFuelExceeded,
+    MaximumFuelExceeded(VmRuntime),
path: linera-execution/src/lib.rs
user: login: deuszx
body: Maybe include the VM type in the error string.
created_at: 2025-05-03T19:44:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -371,13 +396,25 @@ impl ResourceControlPolicy {
         self.http_request.try_mul(count as u128)
     }
 
-    pub(crate) fn fuel_price(&self, fuel: u64) -> Result<Amount, ArithmeticError> {
-        self.fuel_unit.try_mul(u128::from(fuel))
+    fn fuel_unit(&self, vm_runtime: VmRuntime) -> Amount {
path: linera-execution/src/policy.rs
user: login: deuszx
body: ```suggestion
    fn fuel_unit_price(&self, vm_runtime: VmRuntime) -> Amount {
```
created_at: 2025-05-03T19:46:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,14 +131,13 @@ where
     }
 
     /// Obtains the amount of fuel that could be spent by consuming the entire balance.
-    pub(crate) fn remaining_fuel(&self) -> u64 {
+    pub(crate) fn remaining_fuel(&self, vm_runtime: VmRuntime) -> u64 {
path: linera-execution/src/resources.rs
user: login: deuszx
body: I feel like this should be the other way around:

At the start of the execution user "buys" the gas at the price defined by the protocol and we use that to track remaining available gas without constantly recomputing it.
created_at: 2025-05-03T20:01:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -824,16 +850,17 @@ where
         };
 
         ensure_message_length(query.len(), 4)?;
+        // We drop the logs since the "eth_call" execution does not return any log.
+        // Also, for handle_query, we do not have associated costs.
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: What's more interesting is why you ignore `gas_final` - consider adding a comment with the explanation.
created_at: 2025-05-03T20:07:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,39 +906,42 @@ where
             db: self.db.clone(),
         };
 
-        let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        let result_state = {
+            let block_env = self.db.get_block_env()?;
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.reset_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: `fn transact` share a lot of the code with `transact_and_commit` - share "transact" part of the code?
created_at: 2025-05-03T20:09:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Since all these fields are basically counters I'd drop the `number_` prefix and just keep the names simple:
- `keys_reset_eq`
- `keys_reset_neq`
- `keys_set`
- etc.
created_at: 2025-05-03T20:12:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,6 +236,11 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut number_key_reset_eq = 0;
+        let mut number_key_reset_neq = 0;
+        let mut number_key_set = 0;
+        let mut number_key_set_zero = 0;
+        let mut number_key_release = 0;
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: I'd initialize `StorageStats` here and mutate its fields instead and, at the end of this function, call `self.storage_stats + this.storage_stats` instead of this imperative style here.
created_at: 2025-05-03T20:14:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,39 +906,42 @@ where
             db: self.db.clone(),
         };
 
-        let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        let result_state = {
+            let block_env = self.db.get_block_env()?;
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.reset_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: You added gas limit to `transact_and_commit` (https://github.com/linera-io/linera-protocol/pull/3715/commits/d020625c034a37f37b51872240e6b009a6521712) but not to `transact`. Is that intentional difference?
created_at: 2025-05-05T11:18:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,7 +490,8 @@ View or update the resource control policy
 * `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
 * `--service-as-oracle-query <SERVICE_AS_ORACLE_QUERY>` — Set the price per query to a service as an oracle
 * `--http-request <HTTP_REQUEST>` — Set the price for performing an HTTP request
-* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
+* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block
+* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block
path: CLI.md
user: login: MathieuDutSik
body: Yes.
created_at: 2025-05-05T12:29:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: That is incorrect.
EIP-3529 led to a decrease of the refund from 15000 to 4800

See for detail https://eips.ethereum.org/EIPS/eip-3529
created_at: 2025-05-05T12:31:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,7 +490,8 @@ View or update the resource control policy
 * `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
 * `--service-as-oracle-query <SERVICE_AS_ORACLE_QUERY>` — Set the price per query to a service as an oracle
 * `--http-request <HTTP_REQUEST>` — Set the price for performing an HTTP request
-* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
+* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block
+* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block
path: CLI.md
user: login: deuszx
body: IMHO that's wrong. The limit for the block gas is a limit for the execution time. If we allow for independent limits for both EVM and Wasm then we're effectively doubling our gas limit.

cc @afck @ma2bd 
created_at: 2025-05-05T12:38:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
     pub runtime: Arc<Mutex<Runtime>>,
     pub changes: EvmState,
 }
 
 impl<Runtime> Clone for DatabaseRuntime<Runtime> {
     fn clone(&self) -> Self {
         Self {
+            storage_stats: self.storage_stats.clone(),
             runtime: self.runtime.clone(),
             changes: self.changes.clone(),
         }
     }
 }
 
+#[repr(u8)]
+enum KeyTag {
+    /// Key prefix for the storage of the zero contract.
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: When operating the REVM, you will see calls for a contract named 0x0000000000000000000000000000000000000000.

See for yourself in https://github.com/MathieuDutSik/rust_example_codes/tree/main/test_revm01_zero_contract
created_at: 2025-05-05T12:54:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,11 +113,23 @@ impl<Runtime> DatabaseRuntime<Runtime> {
     }
 
     pub fn new(runtime: Runtime) -> Self {
+        let storage_stats = StorageStats::default();
         Self {
+            storage_stats: Arc::new(Mutex::new(storage_stats)),
             runtime: Arc::new(Mutex::new(runtime)),
             changes: HashMap::new(),
         }
     }
+
+    pub fn reset_storage_stats(&self) -> StorageStats {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: 1. The lack of documentation is detected at compilation, not at the CI level.
2. There is no error because the `DatabaseRuntime<Runtime>` is not publicly accessible.
created_at: 2025-05-05T13:07:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,11 +113,23 @@ impl<Runtime> DatabaseRuntime<Runtime> {
     }
 
     pub fn new(runtime: Runtime) -> Self {
+        let storage_stats = StorageStats::default();
         Self {
+            storage_stats: Arc::new(Mutex::new(storage_stats)),
             runtime: Arc::new(Mutex::new(runtime)),
             changes: HashMap::new(),
         }
     }
+
+    pub fn reset_storage_stats(&self) -> StorageStats {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Renamed to `take_storage_stats`, indeed that is a better name.
created_at: 2025-05-05T13:07:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Ok.
created_at: 2025-05-05T13:08:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Ok.
created_at: 2025-05-05T13:09:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: If we want to extend the API, we have two ways:
1. Encapsulate the `StorageSlot` in a container.
2. Submit a PR to the relevant GitHub repository https://github.com/bluealloy/revm

Option 2 is unlikely to succeed as there is little reason to do that. Option 1 gets you plenty of boilerplate for little benefit.
created_at: 2025-05-05T13:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
+                                batch.put_key_value(key, &value.present_value())?;
+                                number_key_set += 1;
+                            } else {
+                                number_key_set_zero += 1;
+                            }
+                        } else if value.present_value() != U256::ZERO {
+                            if value.present_value() == value.original_value() {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Same remark apply.
created_at: 2025-05-05T13:27:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
     pub runtime: Arc<Mutex<Runtime>>,
     pub changes: EvmState,
 }
 
 impl<Runtime> Clone for DatabaseRuntime<Runtime> {
     fn clone(&self) -> Self {
         Self {
+            storage_stats: self.storage_stats.clone(),
             runtime: self.runtime.clone(),
             changes: self.changes.clone(),
         }
     }
 }
 
+#[repr(u8)]
+enum KeyTag {
+    /// Key prefix for the storage of the zero contract.
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: I know, but it's not a "zero contract". Most often it's called "burn address" or "null address" b/c no one has a private key for it. Introduction of new nomenclature is unnecessary and confusing.
created_at: 2025-05-05T13:31:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: It is needed for interior mutability.
created_at: 2025-05-05T13:31:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: I was thinking about an extension trait implemented for this type.
created_at: 2025-05-05T13:32:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -227,7 +228,7 @@ pub enum ExecutionError {
     #[error("Excessive number of bytes written to storage")]
     ExcessiveWrite,
     #[error("Block execution required too much fuel")]
-    MaximumFuelExceeded,
+    MaximumFuelExceeded(VmRuntime),
path: linera-execution/src/lib.rs
user: login: MathieuDutSik
body: Yes, good point.
created_at: 2025-05-05T13:32:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -371,13 +396,25 @@ impl ResourceControlPolicy {
         self.http_request.try_mul(count as u128)
     }
 
-    pub(crate) fn fuel_price(&self, fuel: u64) -> Result<Amount, ArithmeticError> {
-        self.fuel_unit.try_mul(u128::from(fuel))
+    fn fuel_unit(&self, vm_runtime: VmRuntime) -> Amount {
path: linera-execution/src/policy.rs
user: login: MathieuDutSik
body: Ok.
created_at: 2025-05-05T13:34:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,14 +131,13 @@ where
     }
 
     /// Obtains the amount of fuel that could be spent by consuming the entire balance.
-    pub(crate) fn remaining_fuel(&self) -> u64 {
+    pub(crate) fn remaining_fuel(&self, vm_runtime: VmRuntime) -> u64 {
path: linera-execution/src/resources.rs
user: login: MathieuDutSik
body: How would the operation of buying look like in the code?

The fuel is tracked by the function `track_fuel`.
created_at: 2025-05-05T13:37:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -824,16 +850,17 @@ where
         };
 
         ensure_message_length(query.len(), 4)?;
+        // We drop the logs since the "eth_call" execution does not return any log.
+        // Also, for handle_query, we do not have associated costs.
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: I added the sentence "More generally, there is gas costs associated to service operation."
created_at: 2025-05-05T13:39:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,39 +906,42 @@ where
             db: self.db.clone(),
         };
 
-        let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        let result_state = {
+            let block_env = self.db.get_block_env()?;
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.reset_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: There is certainly a lot of similar code. But a major obstacle to sharing is that the functions use different types with Contract/service duality.
created_at: 2025-05-05T13:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,14 +131,13 @@ where
     }
 
     /// Obtains the amount of fuel that could be spent by consuming the entire balance.
-    pub(crate) fn remaining_fuel(&self) -> u64 {
+    pub(crate) fn remaining_fuel(&self, vm_runtime: VmRuntime) -> u64 {
path: linera-execution/src/resources.rs
user: login: deuszx
body: We know the fuel price and we know the amount the user is willing to spend for the execution. We divide `limit / fuel_price` and we get the `fuel_available`. That's the total number of "gas/fuel units" available for a block execution. During the execution we charge, in _gas units_, and as soon as we go below `0` then we "run out of gas".
created_at: 2025-05-05T13:42:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Ok.
created_at: 2025-05-05T13:44:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,6 +236,11 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut number_key_reset_eq = 0;
+        let mut number_key_reset_neq = 0;
+        let mut number_key_set = 0;
+        let mut number_key_set_zero = 0;
+        let mut number_key_release = 0;
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: You are right.
created_at: 2025-05-05T13:46:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,6 +236,11 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut number_key_reset_eq = 0;
+        let mut number_key_reset_neq = 0;
+        let mut number_key_set = 0;
+        let mut number_key_set_zero = 0;
+        let mut number_key_release = 0;
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Your change (https://github.com/linera-io/linera-protocol/commit/4da0b71f18e2f5322c0f4b6afce81bdd7209472a) takes a lock for the whole duration of the function. Is that not going to be a performance problem? Are we not executing multiple REVMs in parallel?
created_at: 2025-05-05T13:51:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Yes, that is also possible, but in my opinion, not really worth it.
created_at: 2025-05-05T14:05:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: I don't know if I agree. These nested, multiple `ifs` that encode specific EVM refund/charging rules should be immediately clear to read/follow.

lets let others option to weigh in.
created_at: 2025-05-05T14:10:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,39 +906,42 @@ where
             db: self.db.clone(),
         };
 
-        let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        let result_state = {
+            let block_env = self.db.get_block_env()?;
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.reset_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: How about the diff/commit I mentioned earlier? Should plain `transact` also have the fix?

created_at: 2025-05-05T14:10:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,35 +20,81 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
 
 use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
 
-#[repr(u8)]
-enum KeyTag {
-    /// Key prefix for the storage of the zero contract.
-    ZeroContractAddress,
-    /// Key prefix for the storage of the contract address.
-    ContractAddress,
+/// The cost of loading from storage
+const SLOAD_COST: u64 = 2100;
+
+/// The cost of storing a non-zero value in the storage
+const SSTORE_COST_SET: u64 = 20000;
+
+/// The cost of storing a zero value in the storage
+const SSTORE_COST_SET_ZERO: u64 = 100;
+
+/// The cost of storing the storage to the same value
+const SSTORE_COST_RESET_EQ: u64 = 100;
+
+/// The cost of storing the storage to a different value
+const SSTORE_COST_RESET_NEQ: u64 = 2900;
+
+/// The refund from releasing data
+const SSTORE_REFUND_RELEASE: u64 = 4800;
+
+#[derive(Clone, Default)]
+pub(crate) struct StorageStats {
+    number_key_reset_eq: u64,
+    number_key_reset_neq: u64,
+    number_key_set: u64,
+    number_key_set_zero: u64,
+    number_key_release: u64,
+    number_key_read: u64,
 }
 
-#[repr(u8)]
-pub enum KeyCategory {
-    AccountInfo,
-    AccountState,
-    Storage,
+impl StorageStats {
+    pub fn storage_costs(&self) -> u64 {
+        let mut storage_costs = 0;
+        storage_costs += self.number_key_reset_eq * SSTORE_COST_RESET_EQ;
+        storage_costs += self.number_key_reset_neq * SSTORE_COST_RESET_NEQ;
+        storage_costs += self.number_key_set * SSTORE_COST_SET;
+        storage_costs += self.number_key_set_zero * SSTORE_COST_SET_ZERO;
+        storage_costs += self.number_key_read * SLOAD_COST;
+        storage_costs
+    }
+
+    pub fn storage_refund(&self) -> u64 {
+        self.number_key_release * SSTORE_REFUND_RELEASE
+    }
 }
 
 pub(crate) struct DatabaseRuntime<Runtime> {
+    storage_stats: Arc<Mutex<StorageStats>>,
     pub runtime: Arc<Mutex<Runtime>>,
     pub changes: EvmState,
 }
 
 impl<Runtime> Clone for DatabaseRuntime<Runtime> {
     fn clone(&self) -> Self {
         Self {
+            storage_stats: self.storage_stats.clone(),
             runtime: self.runtime.clone(),
             changes: self.changes.clone(),
         }
     }
 }
 
+#[repr(u8)]
+enum KeyTag {
+    /// Key prefix for the storage of the zero contract.
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: Ok, renamed to `NullAddress`.
created_at: 2025-05-05T15:45:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,6 +236,11 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut number_key_reset_eq = 0;
+        let mut number_key_reset_neq = 0;
+        let mut number_key_set = 0;
+        let mut number_key_set_zero = 0;
+        let mut number_key_release = 0;
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: No.
The lock is for the REVM instance, and each REVM instance has its own storage stats. So, this does not block running multiple instances in parallel.

The lock occurs when the commit is done and takes a few nanoseconds. Anyway, it happens at the end of the execution of the contract, so this is done only once.
created_at: 2025-05-05T16:00:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,6 +236,11 @@ where
 {
     /// Effectively commits changes to storage.
     pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
+        let mut number_key_reset_eq = 0;
+        let mut number_key_reset_neq = 0;
+        let mut number_key_set = 0;
+        let mut number_key_set_zero = 0;
+        let mut number_key_release = 0;
path: linera-execution/src/evm/database.rs
user: login: deuszx
body: Thanks. I thought it locks the "RevmDatabase" that is shared (but it's not, my misunderstanding).
created_at: 2025-05-05T16:03:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,39 +906,42 @@ where
             db: self.db.clone(),
         };
 
-        let block_env = self.db.get_block_env()?;
-        let mut evm: Evm<'_, _, _> = Evm::builder()
-            .with_ref_db(&mut self.db)
-            .with_external_context(&mut inspector)
-            .modify_tx_env(|tx| {
-                tx.clear();
-                tx.transact_to = kind;
-                tx.data = tx_data;
-            })
-            .modify_block_env(|block| {
-                *block = block_env;
-            })
-            .append_handler_register(|handler| {
-                inspector_handle_register(handler);
-                let precompiles = handler.pre_execution.load_precompiles();
-                handler.pre_execution.load_precompiles = Arc::new(move || {
-                    let mut precompiles = precompiles.clone();
-                    precompiles.extend([(
-                        PRECOMPILE_ADDRESS,
-                        ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
-                    )]);
-                    precompiles
-                });
-            })
-            .build();
+        let result_state = {
+            let block_env = self.db.get_block_env()?;
+            let mut evm: Evm<'_, _, _> = Evm::builder()
+                .with_ref_db(&mut self.db)
+                .with_external_context(&mut inspector)
+                .modify_tx_env(|tx| {
+                    tx.clear();
+                    tx.transact_to = kind;
+                    tx.data = tx_data;
+                })
+                .modify_block_env(|block| {
+                    *block = block_env;
+                })
+                .append_handler_register(|handler| {
+                    inspector_handle_register(handler);
+                    let precompiles = handler.pre_execution.load_precompiles();
+                    handler.pre_execution.load_precompiles = Arc::new(move || {
+                        let mut precompiles = precompiles.clone();
+                        precompiles.extend([(
+                            PRECOMPILE_ADDRESS,
+                            ContextPrecompile::ContextStateful(Arc::new(GeneralServiceCall)),
+                        )]);
+                        precompiles
+                    });
+                })
+                .build();
 
-        let result_state = evm.transact().map_err(|error| {
-            let error = format!("{:?}", error);
-            let error = EvmExecutionError::TransactError(error);
-            ExecutionError::EvmError(error)
-        })?;
+            evm.transact().map_err(|error| {
+                let error = format!("{:?}", error);
+                let error = EvmExecutionError::TransactError(error);
+                ExecutionError::EvmError(error)
+            })
+        }?;
+        let storage_stats = self.db.reset_storage_stats();
         Ok((
-            process_execution_result(result_state.result)?,
+            process_execution_result(storage_stats, result_state.result)?,
             result_state.state,
         ))
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: If we run in `transact`, then we run in service, so there is no limit on gas usage.

Accordingly, the `remaining_fuel` is not available in `ServiceRuntime`, so we cannot set up `gas_limit` even if we wanted to.
created_at: 2025-05-05T16:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,14 +131,13 @@ where
     }
 
     /// Obtains the amount of fuel that could be spent by consuming the entire balance.
-    pub(crate) fn remaining_fuel(&self) -> u64 {
+    pub(crate) fn remaining_fuel(&self, vm_runtime: VmRuntime) -> u64 {
path: linera-execution/src/resources.rs
user: login: MathieuDutSik
body: Well, the limit in fuel is tracked by `tracked_fuel`. If we look at the code, then we see that it is doing addition, not division. So, I do not see how to optimize with that.

Secondly, when you write "At the start of the execution user buys the gas at the price defined by the protocol", my question is how much do you want to buy? You can know that only after the execution has finished.
created_at: 2025-05-05T16:16:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,7 +278,24 @@ where
                     batch.put_key_value(key_state, &account_state)?;
                     for (index, value) in &account.storage {
                         let key = Self::get_uint256_key(val, *index)?;
-                        batch.put_key_value(key, &value.present_value())?;
+                        if value.original_value() == U256::ZERO {
+                            if value.present_value() != U256::ZERO {
path: linera-execution/src/evm/database.rs
user: login: MathieuDutSik
body: I don't know what we can reasonably hope from such rewriting.

What I propose instead is to rewrite the if structure so that it is simpler and easier to read.
created_at: 2025-05-05T16:47:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -664,20 +677,27 @@ where
 }
 
 fn process_execution_result(
+    storage_stats: StorageStats,
     result: ExecutionResult,
 ) -> Result<ExecutionResultSuccess, ExecutionError> {
     match result {
         ExecutionResult::Success {
             reason,
-            gas_used: _,
-            gas_refunded: _,
+            gas_used,
+            gas_refunded,
             logs,
             output,
-        } => Ok(ExecutionResultSuccess {
-            reason,
-            logs,
-            output,
-        }),
+        } => {
+            let mut gas_final = gas_used;
+            gas_final -= storage_stats.storage_costs();
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: Maybe checked_sub here?
created_at: 2025-05-06T08:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -583,12 +593,13 @@ enum Choice {
 #[derive(Debug)]
 struct ExecutionResultSuccess {
     reason: SuccessReason,
+    gas_final: u64,
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: We should consider adding a dedicated type for this and make sure all operations are checked.
created_at: 2025-05-06T08:42:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -865,53 +897,63 @@ where
         forbid_execute_operation_origin(&vec[..4])?;
         let contract_address = Address::ZERO.create(0);
         let kind = TxKind::Call(contract_address);
-        let tx_data = Bytes::copy_from_slice(vec);
-        let (execution_result, _) = self.transact(kind, tx_data)?;
+        let (execution_result, _) = self.transact(kind, vec)?;
         Ok(execution_result)
     }
 
     fn transact(
         &mut self,
         kind: TxKind,
-        tx_data: Bytes,
+        vec: &[u8],
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: can you give it a more descriptive name? `vec` doesn't say anything about what this is
created_at: 2025-05-06T08:43:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -865,53 +897,63 @@ where
         forbid_execute_operation_origin(&vec[..4])?;
         let contract_address = Address::ZERO.create(0);
         let kind = TxKind::Call(contract_address);
-        let tx_data = Bytes::copy_from_slice(vec);
-        let (execution_result, _) = self.transact(kind, tx_data)?;
+        let (execution_result, _) = self.transact(kind, vec)?;
         Ok(execution_result)
     }
 
     fn transact(
         &mut self,
         kind: TxKind,
-        tx_data: Bytes,
+        vec: &[u8],
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: Ok, changed to `input`.
created_at: 2025-05-06T11:06:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -865,53 +897,63 @@ where
         forbid_execute_operation_origin(&vec[..4])?;
         let contract_address = Address::ZERO.create(0);
         let kind = TxKind::Call(contract_address);
-        let tx_data = Bytes::copy_from_slice(vec);
-        let (execution_result, _) = self.transact(kind, tx_data)?;
+        let (execution_result, _) = self.transact(kind, vec)?;
         Ok(execution_result)
     }
 
     fn transact(
         &mut self,
         kind: TxKind,
-        tx_data: Bytes,
+        vec: &[u8],
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: Since you consume it and turn to `tx_data` right away, why not simply pass `tx_data: Bytes` instead?
created_at: 2025-05-06T11:08:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -664,20 +677,27 @@ where
 }
 
 fn process_execution_result(
+    storage_stats: StorageStats,
     result: ExecutionResult,
 ) -> Result<ExecutionResultSuccess, ExecutionError> {
     match result {
         ExecutionResult::Success {
             reason,
-            gas_used: _,
-            gas_refunded: _,
+            gas_used,
+            gas_refunded,
             logs,
             output,
-        } => Ok(ExecutionResultSuccess {
-            reason,
-            logs,
-            output,
-        }),
+        } => {
+            let mut gas_final = gas_used;
+            gas_final -= storage_stats.storage_costs();
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: I think no.
Checked sub is about doing subtraction that can fail and then induce an error. This is the stuff that happens if you do not enough fuel for running your application.

However, in that case, you are subtracting the storage costs from the total gas. So, the total costs should be higher than the storage costs. If not then you have an error. That error creating a panic is in fact appropriate.
created_at: 2025-05-06T11:14:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -664,20 +677,27 @@ where
 }
 
 fn process_execution_result(
+    storage_stats: StorageStats,
     result: ExecutionResult,
 ) -> Result<ExecutionResultSuccess, ExecutionError> {
     match result {
         ExecutionResult::Success {
             reason,
-            gas_used: _,
-            gas_refunded: _,
+            gas_used,
+            gas_refunded,
             logs,
             output,
-        } => Ok(ExecutionResultSuccess {
-            reason,
-            logs,
-            output,
-        }),
+        } => {
+            let mut gas_final = gas_used;
+            gas_final -= storage_stats.storage_costs();
path: linera-execution/src/evm/revm.rs
user: login: deuszx
body: Yes, I understand. I just don't think panicing here is better than raising an appropriate error.
created_at: 2025-05-06T11:16:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -583,12 +593,13 @@ enum Choice {
 #[derive(Debug)]
 struct ExecutionResultSuccess {
     reason: SuccessReason,
+    gas_final: u64,
path: linera-execution/src/evm/revm.rs
user: login: MathieuDutSik
body: The thing is that REVM does not have a dedicated type for the gas. So, I do not see what can be achieved with that.

We could of course ask REVM to do switch to nested type, but what would they get from that? Only slowdown and not any improvement.

And REVM is doing subtractions that are unchecked. So, in think that we cannot achieve much that way.
created_at: 2025-05-06T11:26:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: deuszx
created_at: 2025-05-03T19:40:16Z
body: Can you explain more the concept of separating gas accounting for each contract separately vs what the EVM does? How does it affect the final outcomes, is it still equivalent to that of EVM? i.e. are failures of called contracts propagated back to the source caller with appropriate error codes?
author_association: CONTRIBUTOR
user: login: MathieuDutSik
created_at: 2025-05-04T13:51:33Z
body: > Can you explain more the concept of separating gas accounting for each contract separately vs what the EVM does? How does it affect the final outcomes, is it still equivalent to that of EVM? i.e. are failures of called contracts propagated back to the source caller with appropriate error codes?

In the EVM, when you do the execution of many contracts you get a bill for all of them with the execution being summed. This is in particular what you get by using REVM directly.

However, the execution in Linera is done differently, when a call to another contract is done, then a `fn execute_operation` is done. Those costs are accounted for separately.

Another part of the costs that has to be handled separately is the storage costs. The EVM has its own costs, but Linera also has costs. So, they should not be double-counted. Therefore, the costs associated to storage are subtracted from the gas costs.
author_association: CONTRIBUTOR
user: login: deuszx
created_at: 2025-05-04T17:50:10Z
body: > > Can you explain more the concept of separating gas accounting for each contract separately vs what the EVM does? How does it affect the final outcomes, is it still equivalent to that of EVM? i.e. are failures of called contracts propagated back to the source caller with appropriate error codes?
> 
> In the EVM, when you do the execution of many contracts you get a bill for all of them with the execution being summed. This is in particular what you get by using REVM directly.
> 
> However, the execution in Linera is done differently, when a call to another contract is done, then a `fn execute_operation` is done. Those costs are accounted for separately.
> 
> Another part of the costs that has to be handled separately is the storage costs. The EVM has its own costs, but Linera also has costs. So, they should not be double-counted. Therefore, the costs associated to storage are subtracted from the gas costs.

Yes, I know that. That's not what I asked about. I was asking whether gas accounting works across different VMs, whether the errors are propagated correctly. For example, if a Wasm contract - that uses 80% of allowed gas - calls an EVM contract that tries to use 30% - is the `OutOfGas` error propagated properly to the Wasm caller, as soon as the callee crosses the allowed threshold, and up to the block producer?
author_association: CONTRIBUTOR
user: login: MathieuDutSik
created_at: 2025-05-05T08:01:10Z
body: > > > Can you explain more the concept of separating gas accounting for each contract separately vs what the EVM does? How does it affect the final outcomes, is it still equivalent to that of EVM? i.e. are failures of called contracts propagated back to the source caller with appropriate error codes?
> > 
> > 
> > In the EVM, when you do the execution of many contracts you get a bill for all of them with the execution being summed. This is in particular what you get by using REVM directly.
> > However, the execution in Linera is done differently, when a call to another contract is done, then a `fn execute_operation` is done. Those costs are accounted for separately.
> > Another part of the costs that has to be handled separately is the storage costs. The EVM has its own costs, but Linera also has costs. So, they should not be double-counted. Therefore, the costs associated to storage are subtracted from the gas costs.
> 
> Yes, I know that. That's not what I asked about. I was asking whether gas accounting works across different VMs, whether the errors are propagated correctly. For example, if a Wasm contract - that uses 80% of allowed gas - calls an EVM contract that tries to use 30% - is the `OutOfGas` error propagated properly to the Wasm caller, as soon as the callee crosses the allowed threshold, and up to the block producer?

Ok, decomposing the calls, we see that the key operation is `fn consume_fuel`. If there is not enough fuel then the error `ExecutionError::MaximumFuelExceeded` is thrown via the `track_fuel` function.

That error is thrown via the `try_call_application`. There are two scenarios of failure:
* In the call via the precompile, the error is in the `PrecompileErrors::Fatal`.
* In the case of the inspector redirection, the calls are passed via the normal error handling.

So, yes, the errors are handled. 
author_association: CONTRIBUTOR
user: login: MathieuDutSik
created_at: 2025-05-06T13:29:03Z
body: > No blockers but we also don't have any tests for these. Do you plan on adding them in the next PR? We should test that:
> 
> 1. EVM execution is failing with OutOfGas as expected
> 2. That the gas is properly accounted for when one contract calls another
> 3. That the errors are propagated back to the caller
> 4. That Linera service is capped with `max_block_gas`
> 5. probably more

I added the tests for the EVM execution being OutOfGas, both for Contract and Service.
The tests that errors are transmitted already exists.

The test that contract calls are accounted for in multiple calls is more complicated to implement.
author_association: CONTRIBUTOR