Skip to content

Commit 12424da

Browse files
Implement the processing of the EVM gas usage. (#3715)
## 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.
1 parent 798915f commit 12424da

File tree

25 files changed

+662
-198
lines changed

25 files changed

+662
-198
lines changed

CLI.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,8 @@ View or update the resource control policy
473473
###### **Options:**
474474

475475
* `--block <BLOCK>` — Set the base price for creating a block
476-
* `--fuel-unit <FUEL_UNIT>` — Set the price per unit of fuel
476+
* `--wasm-fuel-unit <WASM_FUEL_UNIT>` — Set the price per unit of Wasm fuel
477+
* `--evm-fuel-unit <EVM_FUEL_UNIT>` — Set the price per unit of EVM fuel
477478
* `--read-operation <READ_OPERATION>` — Set the price per read operation
478479
* `--write-operation <WRITE_OPERATION>` — Set the price per write operation
479480
* `--byte-read <BYTE_READ>` — Set the price per byte read
@@ -489,7 +490,8 @@ View or update the resource control policy
489490
* `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
490491
* `--service-as-oracle-query <SERVICE_AS_ORACLE_QUERY>` — Set the price per query to a service as an oracle
491492
* `--http-request <HTTP_REQUEST>` — Set the price for performing an HTTP request
492-
* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
493+
* `--maximum-wasm-fuel-per-block <MAXIMUM_WASM_FUEL_PER_BLOCK>` — Set the maximum amount of Wasm fuel per block
494+
* `--maximum-evm-fuel-per-block <MAXIMUM_EVM_FUEL_PER_BLOCK>` — Set the maximum amount of EVM fuel per block
493495
* `--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
494496
* `--maximum-block-size <MAXIMUM_BLOCK_SIZE>` — Set the maximum size of a block, in bytes
495497
* `--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
533535
Possible values: `no-fees`, `testnet`
534536

535537
* `--block-price <BLOCK_PRICE>` — Set the base price for creating a block. (This will overwrite value from `--policy-config`)
536-
* `--fuel-unit-price <FUEL_UNIT_PRICE>` — Set the price per unit of fuel. (This will overwrite value from `--policy-config`)
538+
* `--wasm-fuel-unit-price <WASM_FUEL_UNIT_PRICE>` — Set the price per unit of Wasm fuel. (This will overwrite value from `--policy-config`)
539+
* `--evm-fuel-unit-price <EVM_FUEL_UNIT_PRICE>` — Set the price per unit of EVM fuel. (This will overwrite value from `--policy-config`)
537540
* `--read-operation-price <READ_OPERATION_PRICE>` — Set the price per read operation. (This will overwrite value from `--policy-config`)
538541
* `--write-operation-price <WRITE_OPERATION_PRICE>` — Set the price per write operation. (This will overwrite value from `--policy-config`)
539542
* `--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
549552
* `--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`)
550553
* `--service-as-oracle-query-price <SERVICE_AS_ORACLE_QUERY_PRICE>` — Set the price per query to a service as an oracle
551554
* `--http-request-price <HTTP_REQUEST_PRICE>` — Set the price for performing an HTTP request
552-
* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block. (This will overwrite value from `--policy-config`)
555+
* `--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`)
556+
* `--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`)
553557
* `--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
554558
* `--maximum-block-size <MAXIMUM_BLOCK_SIZE>` — Set the maximum size of a block. (This will overwrite value from `--policy-config`)
555559
* `--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`)

examples/meta-counter/src/contract.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl Contract for MetaCounterContract {
6060
} = operation;
6161

6262
let mut message = self.runtime.prepare_message(message).with_grant(Resources {
63-
fuel: fuel_grant,
63+
wasm_fuel: fuel_grant,
6464
..Resources::default()
6565
});
6666
if authenticated {

linera-base/src/data_types.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,10 @@ impl Display for Timestamp {
269269
Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, WitLoad, WitStore, WitType,
270270
)]
271271
pub struct Resources {
272-
/// An amount of execution fuel.
273-
pub fuel: u64,
272+
/// An amount of Wasm execution fuel.
273+
pub wasm_fuel: u64,
274+
/// An amount of EVM execution fuel.
275+
pub evm_fuel: u64,
274276
/// A number of read operations to be executed.
275277
pub read_operations: u32,
276278
/// A number of write operations to be executed.

linera-base/src/unit_tests.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ fn resources_test_case() -> Resources {
5151
blobs_to_publish: 73,
5252
blob_bytes_to_read: 67,
5353
blob_bytes_to_publish: 71,
54-
fuel: 1_000,
54+
wasm_fuel: 1_000,
55+
evm_fuel: 1_000,
5556
message_size: 4,
5657
messages: 93,
5758
read_operations: 12,
@@ -75,7 +76,8 @@ fn send_message_request_test_case() -> SendMessageRequest<Vec<u8>> {
7576
blobs_to_publish: 1000,
7677
blob_bytes_to_read: 10,
7778
blob_bytes_to_publish: 100,
78-
fuel: 8,
79+
wasm_fuel: 8,
80+
evm_fuel: 8,
7981
message_size: 1,
8082
messages: 0,
8183
read_operations: 1,

linera-chain/src/chain.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,30 +109,40 @@ static WASM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
109109
});
110110

111111
#[cfg(with_metrics)]
112-
static WASM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
112+
static EVM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
113113
register_histogram_vec(
114-
"wasm_num_reads_per_block",
115-
"Wasm number of reads per block",
114+
"evm_fuel_used_per_block",
115+
"EVM fuel used per block",
116+
&[],
117+
exponential_bucket_interval(10.0, 1_000_000.0),
118+
)
119+
});
120+
121+
#[cfg(with_metrics)]
122+
static VM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
123+
register_histogram_vec(
124+
"vm_num_reads_per_block",
125+
"VM number of reads per block",
116126
&[],
117127
exponential_bucket_interval(0.1, 100.0),
118128
)
119129
});
120130

121131
#[cfg(with_metrics)]
122-
static WASM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
132+
static VM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
123133
register_histogram_vec(
124-
"wasm_bytes_read_per_block",
125-
"Wasm number of bytes read per block",
134+
"vm_bytes_read_per_block",
135+
"VM number of bytes read per block",
126136
&[],
127137
exponential_bucket_interval(0.1, 10_000_000.0),
128138
)
129139
});
130140

131141
#[cfg(with_metrics)]
132-
static WASM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
142+
static VM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
133143
register_histogram_vec(
134-
"wasm_bytes_written_per_block",
135-
"Wasm number of bytes written per block",
144+
"vm_bytes_written_per_block",
145+
"VM number of bytes written per block",
136146
&[],
137147
exponential_bucket_interval(0.1, 10_000_000.0),
138148
)
@@ -1140,14 +1150,17 @@ where
11401150
NUM_BLOCKS_EXECUTED.with_label_values(&[]).inc();
11411151
WASM_FUEL_USED_PER_BLOCK
11421152
.with_label_values(&[])
1143-
.observe(tracker.fuel as f64);
1144-
WASM_NUM_READS_PER_BLOCK
1153+
.observe(tracker.wasm_fuel as f64);
1154+
EVM_FUEL_USED_PER_BLOCK
1155+
.with_label_values(&[])
1156+
.observe(tracker.evm_fuel as f64);
1157+
VM_NUM_READS_PER_BLOCK
11451158
.with_label_values(&[])
11461159
.observe(tracker.read_operations as f64);
1147-
WASM_BYTES_READ_PER_BLOCK
1160+
VM_BYTES_READ_PER_BLOCK
11481161
.with_label_values(&[])
11491162
.observe(tracker.bytes_read as f64);
1150-
WASM_BYTES_WRITTEN_PER_BLOCK
1163+
VM_BYTES_WRITTEN_PER_BLOCK
11511164
.with_label_values(&[])
11521165
.observe(tracker.bytes_written as f64);
11531166
}

linera-chain/src/unit_tests/chain_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ async fn test_application_permissions() -> anyhow::Result<()> {
314314
);
315315

316316
// Also, blocks without an application operation or incoming message are forbidden.
317-
let invalid_block = make_child_block(&value.clone());
317+
let invalid_block = make_child_block(&value);
318318
let result = chain
319319
.execute_block(&invalid_block, time, None, &[], None)
320320
.await;

linera-core/src/unit_tests/client_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,7 +1080,7 @@ where
10801080
let mut builder = TestBuilder::new(storage_builder, 4, 1, &mut signer)
10811081
.await?
10821082
.with_policy(ResourceControlPolicy {
1083-
maximum_fuel_per_block: 30_000,
1083+
maximum_wasm_fuel_per_block: 30_000,
10841084
blob_read: initial_balance + Amount::ONE,
10851085
blob_published: initial_balance + Amount::ONE,
10861086
blob_byte_read: initial_balance + Amount::ONE,

linera-core/src/unit_tests/wasm_client_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ async fn test_memory_fuel_limit(wasm_runtime: WasmRuntime) -> anyhow::Result<()>
882882
// Set a fuel limit that is enough to instantiate the application and do one increment
883883
// operation, but not ten. We also verify blob fees for the bytecode.
884884
let policy = ResourceControlPolicy {
885-
maximum_fuel_per_block: 30_000,
885+
maximum_wasm_fuel_per_block: 30_000,
886886
blob_read: Amount::from_tokens(10), // Should not be charged.
887887
blob_published: Amount::from_attos(100),
888888
blob_byte_read: Amount::from_tokens(10), // Should not be charged.

linera-execution/src/evm/database.rs

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,58 +20,116 @@ use revm_primitives::{address, BlobExcessGasAndPrice, BlockEnv, EvmState};
2020

2121
use crate::{BaseRuntime, Batch, ContractRuntime, ExecutionError, ViewError};
2222

23-
#[repr(u8)]
24-
enum KeyTag {
25-
/// Key prefix for the storage of the zero contract.
26-
ZeroContractAddress,
27-
/// Key prefix for the storage of the contract address.
28-
ContractAddress,
23+
/// The cost of loading from storage.
24+
const SLOAD_COST: u64 = 2100;
25+
26+
/// The cost of storing a non-zero value in the storage for the first time.
27+
const SSTORE_COST_SET: u64 = 20000;
28+
29+
/// The cost of not changing the state of the variable in the storage.
30+
const SSTORE_COST_NO_OPERATION: u64 = 100;
31+
32+
/// The cost of overwriting the storage to a different value.
33+
const SSTORE_COST_RESET: u64 = 2900;
34+
35+
/// The refund from releasing data.
36+
const SSTORE_REFUND_RELEASE: u64 = 4800;
37+
38+
#[derive(Clone, Default)]
39+
pub(crate) struct StorageStats {
40+
key_no_operation: u64,
41+
key_reset: u64,
42+
key_set: u64,
43+
key_release: u64,
44+
key_read: u64,
2945
}
3046

31-
#[repr(u8)]
32-
pub enum KeyCategory {
33-
AccountInfo,
34-
AccountState,
35-
Storage,
47+
impl StorageStats {
48+
pub fn storage_costs(&self) -> u64 {
49+
let mut storage_costs = 0;
50+
storage_costs += self.key_no_operation * SSTORE_COST_NO_OPERATION;
51+
storage_costs += self.key_reset * SSTORE_COST_RESET;
52+
storage_costs += self.key_set * SSTORE_COST_SET;
53+
storage_costs += self.key_read * SLOAD_COST;
54+
storage_costs
55+
}
56+
57+
pub fn storage_refund(&self) -> u64 {
58+
self.key_release * SSTORE_REFUND_RELEASE
59+
}
3660
}
3761

3862
pub(crate) struct DatabaseRuntime<Runtime> {
63+
storage_stats: Arc<Mutex<StorageStats>>,
3964
pub runtime: Arc<Mutex<Runtime>>,
4065
pub changes: EvmState,
4166
}
4267

4368
impl<Runtime> Clone for DatabaseRuntime<Runtime> {
4469
fn clone(&self) -> Self {
4570
Self {
71+
storage_stats: self.storage_stats.clone(),
4672
runtime: self.runtime.clone(),
4773
changes: self.changes.clone(),
4874
}
4975
}
5076
}
5177

78+
#[repr(u8)]
79+
enum KeyTag {
80+
/// Key prefix for the storage of the zero contract.
81+
NullAddress,
82+
/// Key prefix for the storage of the contract address.
83+
ContractAddress,
84+
}
85+
86+
#[repr(u8)]
87+
pub enum KeyCategory {
88+
AccountInfo,
89+
AccountState,
90+
Storage,
91+
}
92+
5293
impl<Runtime> DatabaseRuntime<Runtime> {
53-
fn get_uint256_key(val: u8, index: U256) -> Result<Vec<u8>, ExecutionError> {
94+
/// Encode the `index` of the EVM storage associated to the smart contract
95+
/// in a linera key.
96+
fn get_linera_key(val: u8, index: U256) -> Result<Vec<u8>, ExecutionError> {
5497
let mut key = vec![val, KeyCategory::Storage as u8];
5598
bcs::serialize_into(&mut key, &index)?;
5699
Ok(key)
57100
}
58101

102+
/// Returns the tag associated to the contract.
59103
fn get_contract_address_key(&self, address: &Address) -> Option<u8> {
60104
if address == &Address::ZERO {
61-
return Some(KeyTag::ZeroContractAddress as u8);
105+
return Some(KeyTag::NullAddress as u8);
62106
}
63107
if address == &Address::ZERO.create(0) {
64108
return Some(KeyTag::ContractAddress as u8);
65109
}
66110
None
67111
}
68112

113+
/// Creates a new `DatabaseRuntime`.
69114
pub fn new(runtime: Runtime) -> Self {
115+
let storage_stats = StorageStats::default();
70116
Self {
117+
storage_stats: Arc::new(Mutex::new(storage_stats)),
71118
runtime: Arc::new(Mutex::new(runtime)),
72119
changes: HashMap::new(),
73120
}
74121
}
122+
123+
/// Returns the current storage states and clears it to default.
124+
pub fn take_storage_stats(&self) -> StorageStats {
125+
let mut storage_stats_read = self
126+
.storage_stats
127+
.lock()
128+
.expect("The lock should be possible");
129+
let storage_stats = storage_stats_read.clone();
130+
*storage_stats_read = StorageStats::default();
131+
storage_stats
132+
}
75133
}
76134

77135
impl<Runtime> Database for DatabaseRuntime<Runtime>
@@ -151,7 +209,14 @@ where
151209
let Some(val) = val else {
152210
panic!("There is no storage associated to externally owned account");
153211
};
154-
let key = Self::get_uint256_key(val, index)?;
212+
let key = Self::get_linera_key(val, index)?;
213+
{
214+
let mut storage_stats = self
215+
.storage_stats
216+
.lock()
217+
.expect("The lock should be possible");
218+
storage_stats.key_read += 1;
219+
}
155220
let result = {
156221
let mut runtime = self.runtime.lock().expect("The lock should be possible");
157222
let promise = runtime.read_value_bytes_new(key)?;
@@ -171,6 +236,10 @@ where
171236
{
172237
/// Effectively commits changes to storage.
173238
pub fn commit_changes(&mut self) -> Result<(), ExecutionError> {
239+
let mut storage_stats = self
240+
.storage_stats
241+
.lock()
242+
.expect("The lock should be possible");
174243
let mut runtime = self.runtime.lock().expect("The lock should be possible");
175244
let mut batch = Batch::new();
176245
let mut list_new_balances = Vec::new();
@@ -207,8 +276,21 @@ where
207276
};
208277
batch.put_key_value(key_state, &account_state)?;
209278
for (index, value) in &account.storage {
210-
let key = Self::get_uint256_key(val, *index)?;
211-
batch.put_key_value(key, &value.present_value())?;
279+
if value.present_value() == value.original_value() {
280+
storage_stats.key_no_operation += 1;
281+
} else {
282+
let key = Self::get_linera_key(val, *index)?;
283+
if value.original_value() == U256::ZERO {
284+
batch.put_key_value(key, &value.present_value())?;
285+
storage_stats.key_set += 1;
286+
} else if value.present_value() == U256::ZERO {
287+
batch.delete_key(key);
288+
storage_stats.key_release += 1;
289+
} else {
290+
batch.put_key_value(key, &value.present_value())?;
291+
storage_stats.key_reset += 1;
292+
}
293+
}
212294
}
213295
}
214296
} else {
@@ -239,7 +321,7 @@ where
239321
{
240322
pub fn is_initialized(&self) -> Result<bool, ExecutionError> {
241323
let mut keys = Vec::new();
242-
for key_tag in [KeyTag::ZeroContractAddress, KeyTag::ContractAddress] {
324+
for key_tag in [KeyTag::NullAddress, KeyTag::ContractAddress] {
243325
let key = vec![key_tag as u8, KeyCategory::AccountInfo as u8];
244326
keys.push(key);
245327
}

0 commit comments

Comments
 (0)