Skip to content

Commit 96d1b07

Browse files
tomip01ilitteriJereSaloCopilotLeanSerra
authored
fix(l1): ethrex_replay reduce cases were OOM happen with SP1 (#4076)
**Motivation** Some of the blocks execution with `ethrex_replay` ans SP1 where failing due to Out Of Memory. We fix some of them here **Description** - Remove unnecessary clones and variables from the `from_nodes(..)` function - Change the behavior of the `storage_tries`, we build them on demand and store them in a hash map indexed by the address **How to Test** Running this with an url for a Hoodi client (using a cache for the block is faster): ``` cargo run --release --features sp1 -- execute block 989143 --rpc-url <RPC_URL> --network hoodi ``` This block now succeeds when in the base branch it doesn't. **What's next** This could be more optimized following this [issue](#4094) Advances #4089 --------- Co-authored-by: ilitteri <ilitteri@fi.uba.ar> Co-authored-by: Jeremías Salomón <48994069+JereSalo@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: JereSalo <jeresalo17@gmail.com> Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Co-authored-by: LeanSerra <46695152+LeanSerra@users.noreply.github.com>
1 parent 5dfb043 commit 96d1b07

File tree

8 files changed

+70
-88
lines changed

8 files changed

+70
-88
lines changed

cmd/ethrex_replay/src/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub async fn run_tx(
3333
.ok_or(eyre::Error::msg("missing block data"))?;
3434
let mut remaining_gas = block.header.gas_limit;
3535
let mut prover_db = cache.witness;
36-
prover_db.rebuild_tries()?;
36+
prover_db.rebuild_state_trie()?;
3737
let mut wrapped_db = ExecutionWitnessWrapper::new(prover_db);
3838

3939
let vm_type = if l2 { VMType::L2 } else { VMType::L1 };

crates/blockchain/blockchain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,10 @@ impl Blockchain {
350350
codes,
351351
//TODO: See if we should call rebuild_tries() here for initializing these fields so that we don't have an inconsistent struct. (#4056)
352352
state_trie: None,
353-
storage_tries: None,
354353
block_headers,
355354
chain_config,
356355
state_trie_nodes: used_trie_nodes.into_iter().map(Bytes::from).collect(),
356+
storage_tries: HashMap::new(),
357357
parent_block_header: self
358358
.storage
359359
.get_block_header_by_hash(first_block_header.parent_hash)?

crates/common/trie/trie.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -262,21 +262,17 @@ impl Trie {
262262
.map(|node| {
263263
(
264264
NodeHash::from_slice(&Keccak256::new_with_prefix(node).finalize()),
265-
node,
265+
node.clone(),
266266
)
267267
})
268268
.collect::<HashMap<_, _>>();
269-
let nodes = storage
270-
.iter()
271-
.map(|(node_hash, nodes)| (*node_hash, (*nodes).clone()))
272-
.collect::<HashMap<_, _>>();
273269
let Some(root) = root else {
274-
let in_memory_trie = Box::new(InMemoryTrieDB::new(Arc::new(Mutex::new(nodes))));
270+
let in_memory_trie = Box::new(InMemoryTrieDB::new(Arc::new(Mutex::new(storage))));
275271
return Ok(Trie::new(in_memory_trie));
276272
};
277273

278274
fn inner(
279-
storage: &mut HashMap<NodeHash, &Vec<u8>>,
275+
storage: &mut HashMap<NodeHash, Vec<u8>>,
280276
node: &NodeRLP,
281277
) -> Result<Node, TrieError> {
282278
Ok(match Node::decode_raw(node)? {
@@ -288,7 +284,7 @@ impl Trie {
288284

289285
if hash.is_valid() {
290286
*choice = match storage.remove(&hash) {
291-
Some(rlp) => inner(storage, rlp)?.into(),
287+
Some(rlp) => inner(storage, &rlp)?.into(),
292288
None => hash.into(),
293289
};
294290
}
@@ -302,7 +298,7 @@ impl Trie {
302298
};
303299

304300
node.child = match storage.remove(&hash) {
305-
Some(rlp) => inner(storage, rlp)?.into(),
301+
Some(rlp) => inner(storage, &rlp)?.into(),
306302
None => hash.into(),
307303
};
308304

@@ -313,11 +309,7 @@ impl Trie {
313309
}
314310

315311
let root = inner(&mut storage, root)?.into();
316-
let nodes = storage
317-
.into_iter()
318-
.map(|(node_hash, nodes)| (node_hash, nodes.clone()))
319-
.collect::<HashMap<_, _>>();
320-
let in_memory_trie = Box::new(InMemoryTrieDB::new(Arc::new(Mutex::new(nodes))));
312+
let in_memory_trie = Box::new(InMemoryTrieDB::new(Arc::new(Mutex::new(storage))));
321313

322314
let mut trie = Trie::new(in_memory_trie);
323315
trie.root = root;

crates/common/types/block_execution_witness.rs

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,10 @@ pub struct ExecutionWitnessResult {
5151
#[serde(skip)]
5252
#[rkyv(with = rkyv::with::Skip)]
5353
pub state_trie: Option<Trie>,
54-
// Indexed by account
55-
// Pruned storage MPT
54+
// Storage tries accessed by account address
5655
#[serde(skip)]
5756
#[rkyv(with = rkyv::with::Skip)]
58-
pub storage_tries: Option<HashMap<Address, Trie>>,
57+
pub storage_tries: HashMap<Address, Trie>,
5958
// Block headers needed for BLOCKHASH opcode
6059
pub block_headers: HashMap<u64, BlockHeader>,
6160
// Parent block header to get the initial state root
@@ -85,36 +84,17 @@ pub enum ExecutionWitnessError {
8584
}
8685

8786
impl ExecutionWitnessResult {
88-
pub fn rebuild_tries(&mut self) -> Result<(), ExecutionWitnessError> {
87+
/// Use the state nodes to build the state trie and store them in `self.state_trie`
88+
/// This function will fail if the state trie cannot be rebuilt.
89+
pub fn rebuild_state_trie(&mut self) -> Result<(), ExecutionWitnessError> {
8990
let state_trie = rebuild_trie(self.parent_block_header.state_root, &self.state_trie_nodes)?;
90-
91-
// Keys can either be account addresses or storage slots. They have different sizes,
92-
// so we filter them by size. The from_slice method panics if the input has the wrong size.
93-
let addresses: Vec<Address> = self
94-
.keys
95-
.iter()
96-
.filter(|k| k.len() == Address::len_bytes())
97-
.map(|k| Address::from_slice(k))
98-
.collect();
99-
100-
let storage_tries: HashMap<Address, Trie> = HashMap::from_iter(
101-
addresses
102-
.iter()
103-
.filter_map(|addr| {
104-
Some((
105-
*addr,
106-
Self::rebuild_storage_trie(addr, &state_trie, &self.state_trie_nodes)?,
107-
))
108-
})
109-
.collect::<Vec<(Address, Trie)>>(),
110-
);
111-
11291
self.state_trie = Some(state_trie);
113-
self.storage_tries = Some(storage_tries);
11492

11593
Ok(())
11694
}
11795

96+
/// Helper function to rebuild the storage trie for a given account address
97+
/// Returns if root is not empty, an Option with the rebuilt trie
11898
// This function is an option because we expect it to fail sometimes, and we just want to filter it
11999
pub fn rebuild_storage_trie(address: &H160, trie: &Trie, state: &[Bytes]) -> Option<Trie> {
120100
let account_state_rlp = trie.get(&hash_address(address)).ok()??;
@@ -128,12 +108,14 @@ impl ExecutionWitnessResult {
128108
rebuild_trie(account_state.storage_root, state).ok()
129109
}
130110

111+
/// Helper function to apply account updates to the execution witness
112+
/// It updates the state trie and storage tries with the given account updates
113+
/// Returns an error if the updates cannot be applied
131114
pub fn apply_account_updates(
132115
&mut self,
133116
account_updates: &[AccountUpdate],
134117
) -> Result<(), ExecutionWitnessError> {
135-
let (Some(state_trie), Some(storage_tries_map)) =
136-
(self.state_trie.as_mut(), self.storage_tries.as_mut())
118+
let (Some(state_trie), storage_tries) = (self.state_trie.as_mut(), &mut self.storage_tries)
137119
else {
138120
return Err(ExecutionWitnessError::ApplyAccountUpdates(
139121
"Tried to apply account updates before rebuilding the tries".to_string(),
@@ -169,10 +151,9 @@ impl ExecutionWitnessResult {
169151
}
170152
// Store the added storage in the account's storage trie and compute its new root
171153
if !update.added_storage.is_empty() {
172-
let storage_trie =
173-
storage_tries_map.entry(update.address).or_insert_with(|| {
174-
Trie::from_nodes(None, &[]).expect("failed to create empty trie")
175-
});
154+
let storage_trie = storage_tries.entry(update.address).or_insert_with(|| {
155+
Trie::from_nodes(None, &[]).expect("failed to create empty trie")
156+
});
176157

177158
// Inserts must come before deletes, otherwise deletes might require extra nodes
178159
// Example:
@@ -207,6 +188,8 @@ impl ExecutionWitnessResult {
207188
Ok(())
208189
}
209190

191+
/// Returns the root hash of the state trie
192+
/// Returns an error if the state trie is not built yet
210193
pub fn state_trie_root(&self) -> Result<H256, ExecutionWitnessError> {
211194
let state_trie = self
212195
.state_trie
@@ -254,6 +237,8 @@ impl ExecutionWitnessResult {
254237
Ok(None)
255238
}
256239

240+
/// Retrieves the parent block header for the specified block number
241+
/// Searches within `self.block_headers`
257242
pub fn get_block_parent_header(
258243
&self,
259244
block_number: u64,
@@ -263,6 +248,8 @@ impl ExecutionWitnessResult {
263248
.ok_or(ExecutionWitnessError::MissingParentHeaderOf(block_number))
264249
}
265250

251+
/// Retrieves the account info based on what is stored in the state trie.
252+
/// Returns an error if the state trie is not rebuilt or if decoding the account state fails.
266253
pub fn get_account_info(
267254
&self,
268255
address: Address,
@@ -289,6 +276,8 @@ impl ExecutionWitnessResult {
289276
}))
290277
}
291278

279+
/// Fetches the block hash for a specific block number.
280+
/// Looks up `self.block_headers` and computes the hash if it is not already computed.
292281
pub fn get_block_hash(&self, block_number: u64) -> Result<H256, ExecutionWitnessError> {
293282
self.block_headers
294283
.get(&block_number)
@@ -300,21 +289,31 @@ impl ExecutionWitnessResult {
300289
})
301290
}
302291

292+
/// Retrieves a storage slot value for an account in its storage trie.
293+
///
294+
/// Lazily builds the storage trie for the address if not already available.
295+
/// This lazy loading approach minimizes memory usage by only building tries when needed.
303296
pub fn get_storage_slot(
304-
&self,
297+
&mut self,
305298
address: Address,
306299
key: H256,
307300
) -> Result<Option<U256>, ExecutionWitnessError> {
308-
let storage_tries_map =
309-
self.storage_tries
310-
.as_ref()
311-
.ok_or(ExecutionWitnessError::Database(
312-
"ExecutionWitness: Tried to get storage slot before rebuilding tries"
301+
let storage_trie = if let Some(storage_trie) = self.storage_tries.get(&address) {
302+
storage_trie
303+
} else {
304+
let Some(state_trie) = self.state_trie.as_ref() else {
305+
return Err(ExecutionWitnessError::Database(
306+
"ExecutionWitness: Tried to get storage slot before rebuilding state trie."
313307
.to_string(),
314-
))?;
308+
));
309+
};
310+
let Some(storage_trie) =
311+
Self::rebuild_storage_trie(&address, state_trie, &self.state_trie_nodes)
312+
else {
313+
return Ok(None);
314+
};
315315

316-
let Some(storage_trie) = storage_tries_map.get(&address) else {
317-
return Ok(None);
316+
self.storage_tries.entry(address).or_insert(storage_trie)
318317
};
319318
let hashed_key = hash_key(&key);
320319
if let Some(encoded_key) = storage_trie
@@ -331,10 +330,13 @@ impl ExecutionWitnessResult {
331330
}
332331
}
333332

333+
/// Retrieves the chain configuration for the execution witness.
334334
pub fn get_chain_config(&self) -> Result<ChainConfig, ExecutionWitnessError> {
335335
Ok(self.chain_config)
336336
}
337337

338+
/// Retrieves the account code for a specific account.
339+
/// Returns an Err if the code is not found.
338340
pub fn get_account_code(&self, code_hash: H256) -> Result<bytes::Bytes, ExecutionWitnessError> {
339341
if code_hash == *EMPTY_KECCACK_HASH {
340342
return Ok(Bytes::new());
@@ -447,16 +449,3 @@ pub fn rebuild_trie(initial_state: H256, state: &[Bytes]) -> Result<Trie, Execut
447449
)
448450
.map_err(|e| ExecutionWitnessError::RebuildTrie(format!("Failed to build state trie {e}")))
449451
}
450-
451-
// This function is an option because we expect it to fail sometimes, and we just want to filter it
452-
pub fn rebuild_storage_trie(address: &H160, trie: &Trie, state: &[Bytes]) -> Option<Trie> {
453-
let account_state_rlp = trie.get(&hash_address(address)).ok()??;
454-
455-
let account_state = AccountState::decode(&account_state_rlp).ok()?;
456-
457-
if account_state.storage_root == *EMPTY_TRIE_HASH {
458-
return None;
459-
}
460-
461-
rebuild_trie(account_state.storage_root, state).ok()
462-
}

crates/l2/prover/zkvm/interface/sp1/Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/l2/prover/zkvm/interface/src/execution.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pub fn stateless_validation_l2(
163163
codes: db.codes.clone(),
164164
keys: db.keys.clone(),
165165
state_trie: None,
166-
storage_tries: None,
166+
storage_tries: HashMap::new(),
167167
state_trie_nodes: db.state_trie_nodes.clone(),
168168
parent_block_header: db.parent_block_header.clone(),
169169
};
@@ -192,7 +192,7 @@ pub fn stateless_validation_l2(
192192
// Check state diffs are valid
193193
let blob_versioned_hash = if !validium {
194194
initial_db
195-
.rebuild_tries()
195+
.rebuild_state_trie()
196196
.map_err(|_| StatelessExecutionError::InvalidInitialStateTrie)?;
197197
let wrapped_db = ExecutionWitnessWrapper::new(initial_db);
198198
let state_diff = prepare_state_diff(
@@ -234,7 +234,7 @@ fn execute_stateless(
234234
mut db: ExecutionWitnessResult,
235235
elasticity_multiplier: u64,
236236
) -> Result<StatelessResult, StatelessExecutionError> {
237-
db.rebuild_tries()
237+
db.rebuild_state_trie()
238238
.map_err(|_| StatelessExecutionError::InvalidInitialStateTrie)?;
239239

240240
let mut wrapped_db = ExecutionWitnessWrapper::new(db);
@@ -271,7 +271,6 @@ fn execute_stateless(
271271
let initial_state_hash = wrapped_db
272272
.state_trie_root()
273273
.map_err(StatelessExecutionError::ExecutionWitness)?;
274-
275274
if initial_state_hash != parent_block_header.state_root {
276275
return Err(StatelessExecutionError::InvalidInitialStateTrie);
277276
}

crates/l2/tee/quote-gen/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/networking/rpc/debug/execution_witness.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ pub fn execution_witness_from_rpc_chain_config(
9292
state_trie_nodes: rpc_witness.state,
9393
keys: rpc_witness.keys,
9494
codes,
95-
state_trie: None, // `None` because we'll rebuild the tries afterwards
96-
storage_tries: None, // `None` because we'll rebuild the tries afterwards
95+
state_trie: None, // `None` because we'll rebuild the tries afterwards
96+
storage_tries: HashMap::new(), // empty map because we'll rebuild the tries afterwards
9797
block_headers,
9898
chain_config,
9999
parent_block_header: parent_header,
100100
};
101101

102-
witness.rebuild_tries()?;
102+
witness.rebuild_state_trie()?;
103103

104104
Ok(witness)
105105
}

0 commit comments

Comments
 (0)