Skip to content

Commit 5a7d759

Browse files
authored
refactor(levm,l1,l2): split block execution and update generation (#2519)
**Motivation** Currently during batch processing, the state transitions are calculated for every block and then merged, when it would be more performant to calculate them once at the end. **Description** This PR removes the account updates from the execution result and makes every consumer manually request them. <!-- Link to issues: Resolves #111, Resolves #222 --> Closes #2504
1 parent 1694e0a commit 5a7d759

File tree

12 files changed

+79
-75
lines changed

12 files changed

+79
-75
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### 2025-04-22
66

7+
- Avoid calculating state transitions after every block in bulk mode [2519](https://github.com/lambdaclass/ethrex/pull/2519)
8+
79
- Transform the inlined variant of NodeHash to a constant sized array [2516](https://github.com/lambdaclass/ethrex/pull/2516)
810

911
### 2025-04-11

crates/blockchain/blockchain.rs

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use error::MempoolError;
1010
use error::{ChainError, InvalidBlockError};
1111
use ethrex_common::constants::{GAS_PER_BLOB, MIN_BASE_FEE_PER_BLOB_GAS};
1212
use ethrex_common::types::requests::{compute_requests_hash, EncodedRequests, Requests};
13-
use ethrex_common::types::BlobsBundle;
1413
use ethrex_common::types::MempoolTransaction;
1514
use ethrex_common::types::{
1615
compute_receipts_root, validate_block_header, validate_cancun_header_fields,
1716
validate_prague_header_fields, validate_pre_cancun_header_fields, Block, BlockHash,
1817
BlockHeader, BlockNumber, ChainConfig, EIP4844Transaction, Receipt, Transaction,
1918
};
19+
use ethrex_common::types::{BlobsBundle, Fork};
2020

21-
use ethrex_common::{Address, H160, H256};
21+
use ethrex_common::{Address, H256};
2222
use mempool::Mempool;
2323
use std::collections::HashMap;
2424
use std::{ops::Div, time::Instant};
@@ -62,14 +62,18 @@ impl Blockchain {
6262
}
6363

6464
/// Executes a block withing a new vm instance and state
65-
async fn execute_block(&self, block: &Block) -> Result<BlockExecutionResult, ChainError> {
65+
async fn execute_block(
66+
&self,
67+
block: &Block,
68+
) -> Result<(BlockExecutionResult, Vec<AccountUpdate>), ChainError> {
6669
// Validate if it can be the new head and find the parent
6770
let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else {
6871
// If the parent is not present, we store it as pending.
6972
self.storage.add_pending_block(block.clone()).await?;
7073
return Err(ChainError::ParentNotFound);
7174
};
7275
let chain_config = self.storage.get_chain_config()?;
76+
let fork = chain_config.fork(block.header.timestamp);
7377

7478
// Validate the block pre-execution
7579
validate_block(block, &parent_header, &chain_config)?;
@@ -80,13 +84,14 @@ impl Blockchain {
8084
block.header.parent_hash,
8185
);
8286
let execution_result = vm.execute_block(block)?;
87+
let account_updates = vm.get_state_transitions(fork)?;
8388

8489
// Validate execution went alright
8590
validate_gas_used(&execution_result.receipts, &block.header)?;
8691
validate_receipts_root(&block.header, &execution_result.receipts)?;
8792
validate_requests_hash(&block.header, &chain_config, &execution_result.requests)?;
8893

89-
Ok(execution_result)
94+
Ok((execution_result, account_updates))
9095
}
9196

9297
/// Executes a block from a given vm instance an does not clear its state
@@ -114,11 +119,12 @@ impl Blockchain {
114119
&self,
115120
block: &Block,
116121
execution_result: BlockExecutionResult,
122+
account_updates: &[AccountUpdate],
117123
) -> Result<(), ChainError> {
118124
// Apply the account updates over the last block's state and compute the new state root
119125
let new_state_root = self
120126
.storage
121-
.apply_account_updates(block.header.parent_hash, &execution_result.account_updates)
127+
.apply_account_updates(block.header.parent_hash, account_updates)
122128
.await?
123129
.ok_or(ChainError::ParentStateNotFound)?;
124130

@@ -137,9 +143,9 @@ impl Blockchain {
137143

138144
pub async fn add_block(&self, block: &Block) -> Result<(), ChainError> {
139145
let since = Instant::now();
140-
let res = self.execute_block(block).await?;
146+
let (res, updates) = self.execute_block(block).await?;
141147
let executed = Instant::now();
142-
let result = self.store_block(block, res).await;
148+
let result = self.store_block(block, res, &updates).await;
143149
let stored = Instant::now();
144150
Self::print_add_block_logs(block, since, executed, stored);
145151
result
@@ -197,6 +203,8 @@ impl Blockchain {
197203
.storage
198204
.get_chain_config()
199205
.map_err(|e| (e.into(), None))?;
206+
let fork = chain_config.fork(first_block_header.timestamp);
207+
200208
let mut vm = Evm::new(
201209
self.evm_engine,
202210
self.storage.clone(),
@@ -205,12 +213,20 @@ impl Blockchain {
205213

206214
let blocks_len = blocks.len();
207215
let mut all_receipts: HashMap<BlockHash, Vec<Receipt>> = HashMap::new();
208-
let mut all_account_updates: HashMap<H160, AccountUpdate> = HashMap::new();
209216
let mut total_gas_used = 0;
210217
let mut transactions_count = 0;
211218

212219
let interval = Instant::now();
213220
for (i, block) in blocks.iter().enumerate() {
221+
if is_crossing_spuriousdragon(fork, chain_config.fork(block.header.timestamp)) {
222+
return Err((
223+
ChainError::Custom("Crossing fork boundary in bulk mode".into()),
224+
Some(BatchBlockProcessingFailure {
225+
last_valid_hash,
226+
failed_block_hash: block.hash(),
227+
}),
228+
));
229+
}
214230
// for the first block, we need to query the store
215231
let parent_header = if i == 0 {
216232
let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else {
@@ -228,11 +244,12 @@ impl Blockchain {
228244
blocks[i - 1].header.clone()
229245
};
230246

231-
let BlockExecutionResult {
232-
receipts,
233-
account_updates,
234-
..
235-
} = match self.execute_block_from_state(&parent_header, block, &chain_config, &mut vm) {
247+
let BlockExecutionResult { receipts, .. } = match self.execute_block_from_state(
248+
&parent_header,
249+
block,
250+
&chain_config,
251+
&mut vm,
252+
) {
236253
Ok(result) => result,
237254
Err(err) => {
238255
return Err((
@@ -245,44 +262,24 @@ impl Blockchain {
245262
}
246263
};
247264

248-
// Merge account updates
249-
for account_update in account_updates {
250-
let Some(cache) = all_account_updates.get_mut(&account_update.address) else {
251-
all_account_updates.insert(account_update.address, account_update);
252-
continue;
253-
};
254-
255-
cache.removed = account_update.removed;
256-
if let Some(code) = account_update.code {
257-
cache.code = Some(code);
258-
};
259-
260-
if let Some(info) = account_update.info {
261-
cache.info = Some(info);
262-
}
263-
264-
for (k, v) in account_update.added_storage.into_iter() {
265-
cache.added_storage.insert(k, v);
266-
}
267-
}
268-
269265
last_valid_hash = block.hash();
270266
total_gas_used += block.header.gas_used;
271267
transactions_count += block.body.transactions.len();
272268
all_receipts.insert(block.hash(), receipts);
273269
}
274270

271+
let account_updates = vm
272+
.get_state_transitions(fork)
273+
.map_err(|err| (ChainError::EvmError(err), None))?;
274+
275275
let Some(last_block) = blocks.last() else {
276276
return Err((ChainError::Custom("Last block not found".into()), None));
277277
};
278278

279279
// Apply the account updates over all blocks and compute the new state root
280280
let new_state_root = self
281281
.storage
282-
.apply_account_updates(
283-
first_block_header.parent_hash,
284-
&all_account_updates.into_values().collect::<Vec<_>>(),
285-
)
282+
.apply_account_updates(first_block_header.parent_hash, &account_updates)
286283
.await
287284
.map_err(|e| (e.into(), None))?
288285
.ok_or((ChainError::ParentStateNotFound, None))?;
@@ -653,5 +650,15 @@ fn get_total_blob_gas(tx: &EIP4844Transaction) -> u64 {
653650
GAS_PER_BLOB * tx.blob_versioned_hashes.len() as u64
654651
}
655652

653+
fn is_crossing_spuriousdragon(from: Fork, to: Fork) -> bool {
654+
if from >= Fork::SpuriousDragon {
655+
return false;
656+
}
657+
if to < Fork::SpuriousDragon {
658+
return false;
659+
}
660+
from != to
661+
}
662+
656663
#[cfg(test)]
657664
mod tests {}

crates/l2/prover/src/backends/exec.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ethrex_blockchain::{validate_block, validate_gas_used};
1+
use ethrex_blockchain::validate_block;
22
use ethrex_l2::utils::prover::proving_systems::{ProofCalldata, ProverType};
33
use ethrex_l2_sdk::calldata::Value;
44
use ethrex_vm::Evm;
@@ -54,11 +54,12 @@ fn execution_program(input: ProgramInput) -> Result<ProgramOutput, Box<dyn std::
5454
if !verify_db(&db, &state_trie, &storage_tries)? {
5555
return Err("invalid database".to_string().into());
5656
};
57+
let fork = db.chain_config.fork(block.header.timestamp);
5758

5859
let mut vm = Evm::from_execution_db(db.clone());
59-
let result = vm.execute_block(&block)?;
60-
let receipts = result.receipts;
61-
let account_updates = result.account_updates;
60+
let _result = vm.execute_block(&block)?;
61+
// let receipts = result.receipts;
62+
let account_updates = vm.get_state_transitions(fork)?;
6263
// validate_gas_used(&receipts, &block.header)?;
6364

6465
// Update state trie

crates/l2/prover/zkvm/interface/pico/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub fn main() {
4141

4242
let result = REVM::execute_block(&block, &mut state).expect("failed to execute block");
4343
let receipts = result.receipts;
44-
let account_updates = result.account_updates;
44+
let account_updates = REVM::get_state_transitions(&mut state);
4545
validate_gas_used(&receipts, &block.header).expect("invalid gas used");
4646

4747
// Output gas for measurement purposes

crates/l2/prover/zkvm/interface/risc0/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ fn main() {
3131
panic!("invalid database")
3232
};
3333

34+
let fork = db.chain_config.fork(block.header.timestamp);
3435
let mut evm = Evm::from_execution_db(db.clone());
3536
let result = evm.execute_block(&block).expect("failed to execute block");
3637
let receipts = result.receipts;
37-
let account_updates = result.account_updates;
38+
let account_updates = evm.get_state_transitions(fork).expect("failed to get state transitions");
3839
validate_gas_used(&receipts, &block.header).expect("invalid gas used");
3940

4041
// Output gas for measurement purposes

crates/l2/prover/zkvm/interface/sp1/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ pub fn main() {
3131
if !verify_db(&db, &state_trie, &storage_tries).expect("failed to validate database") {
3232
panic!("invalid database")
3333
};
34+
let fork = db.chain_config.fork(block.header.timestamp);
3435

3536
let mut evm = Evm::from_execution_db(db.clone());
3637
let result = evm.execute_block(&block).expect("failed to execute block");
3738
let receipts = result.receipts;
38-
let account_updates = result.account_updates;
39+
let account_updates = evm.get_state_transitions(fork).expect("failed to get state transitions");
3940
// validate_gas_used(&receipts, &block.header).expect("invalid gas used");
4041

4142
// Output gas for measurement purposes

crates/l2/sequencer/block_producer.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,21 @@ impl BlockProducer {
116116
let chain_config = store.get_chain_config()?;
117117
validate_block(&block, &head_header, &chain_config)?;
118118

119+
let account_updates = payload_build_result.account_updates;
120+
119121
let execution_result = BlockExecutionResult {
120-
account_updates: payload_build_result.account_updates,
121122
receipts: payload_build_result.receipts,
122123
requests: Vec::new(),
123124
};
124125

125126
blockchain
126-
.store_block(&block, execution_result.clone())
127+
.store_block(&block, execution_result.clone(), &account_updates)
127128
.await?;
128129
info!("Stored new block {:x}", block.hash());
129130
// WARN: We're not storing the payload into the Store because there's no use to it by the L2 for now.
130131

131132
// Cache execution result
132-
execution_cache.push(block.hash(), execution_result.account_updates)?;
133+
execution_cache.push(block.hash(), account_updates)?;
133134

134135
// Make the new head be part of the canonical chain
135136
apply_fork_choice(&store, block.hash(), block.hash(), block.hash()).await?;

crates/l2/sequencer/l1_committer.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,13 @@ impl Committer {
150150
warn!(
151151
"Could not find execution cache result for block {block_number}, falling back to re-execution"
152152
);
153-
Evm::default(self.store.clone(), block_to_commit.header.parent_hash)
154-
.execute_block(&block_to_commit)
155-
.map(|result| result.account_updates)?
153+
let mut vm = Evm::default(self.store.clone(), block_to_commit.header.parent_hash);
154+
vm.execute_block(&block_to_commit)?;
155+
let fork = self
156+
.store
157+
.get_chain_config()?
158+
.fork(block_to_commit.header.timestamp);
159+
vm.get_state_transitions(fork)?
156160
}
157161
};
158162

crates/l2/utils/prover/save_state.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -454,9 +454,9 @@ mod tests {
454454
block_hash: block.hash(),
455455
};
456456
let mut db = GeneralizedDatabase::new(Arc::new(store.clone()), CacheDB::new());
457-
let account_updates = LEVM::execute_block(blocks.last().unwrap(), &mut db)
458-
.unwrap()
459-
.account_updates;
457+
LEVM::execute_block(blocks.last().unwrap(), &mut db)?;
458+
let fork = db.store.get_chain_config().fork(block.header.timestamp);
459+
let account_updates = LEVM::get_state_transitions(&mut db, fork)?;
460460

461461
account_updates_vec.push(account_updates.clone());
462462

crates/vm/backends/levm/mod.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ impl LEVM {
4444
block: &Block,
4545
db: &mut GeneralizedDatabase,
4646
) -> Result<BlockExecutionResult, EvmError> {
47-
let chain_config = db.store.get_chain_config();
48-
let block_header = &block.header;
49-
let fork = chain_config.fork(block_header.timestamp);
5047
cfg_if::cfg_if! {
5148
if #[cfg(not(feature = "l2"))] {
49+
let chain_config = db.store.get_chain_config();
50+
let block_header = &block.header;
51+
let fork = chain_config.fork(block_header.timestamp);
5252
if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun {
5353
Self::beacon_root_contract_call(block_header, db)?;
5454
}
@@ -90,13 +90,7 @@ impl LEVM {
9090
}
9191
}
9292

93-
let account_updates = Self::get_state_transitions(db, fork)?;
94-
95-
Ok(BlockExecutionResult {
96-
receipts,
97-
requests,
98-
account_updates,
99-
})
93+
Ok(BlockExecutionResult { receipts, requests })
10094
}
10195

10296
pub fn execute_tx(
@@ -391,6 +385,7 @@ impl LEVM {
391385
) -> Result<ExecutionDB, ExecutionDBError> {
392386
let parent_hash = block.header.parent_hash;
393387
let chain_config = store.get_chain_config()?;
388+
let fork = chain_config.fork(block.header.timestamp);
394389

395390
let logger = Arc::new(DatabaseLogger::new(Arc::new(StoreWrapper {
396391
store: store.clone(),
@@ -400,9 +395,8 @@ impl LEVM {
400395
let mut db = GeneralizedDatabase::new(logger, CacheDB::new());
401396

402397
// pre-execute and get all state changes
403-
let execution_updates = Self::execute_block(block, &mut db)
404-
.map_err(Box::new)?
405-
.account_updates;
398+
let _ = Self::execute_block(block, &mut db);
399+
let execution_updates = Self::get_state_transitions(&mut db, fork).map_err(Box::new)?;
406400

407401
// index accessed account addresses and storage keys
408402
let state_accessed = logger_ref

0 commit comments

Comments
 (0)