ethrex_levm/execution_handlers.rs
1use crate::{
2 constants::*,
3 errors::{ContextResult, ExceptionalHalt, InternalError, TxResult, VMError},
4 gas_cost::{CODE_DEPOSIT_COST, CODE_DEPOSIT_REGULAR_COST_PER_WORD},
5 utils::create_eth_transfer_log,
6 vm::VM,
7};
8
9use bytes::Bytes;
10use ethrex_common::types::{Code, Fork};
11
12impl<'a> VM<'a> {
13 pub fn handle_precompile_result(
14 precompile_result: Result<Bytes, VMError>,
15 gas_limit: u64,
16 gas_remaining: u64,
17 ) -> Result<ContextResult, VMError> {
18 match precompile_result {
19 Ok(output) => {
20 let gas_used = gas_limit
21 .checked_sub(gas_remaining)
22 .ok_or(InternalError::Underflow)?;
23 Ok(ContextResult {
24 result: TxResult::Success,
25 gas_used,
26 gas_spent: gas_used, // Will be updated in finalize_execution
27 output,
28 })
29 }
30 Err(error) => {
31 if error.should_propagate() {
32 return Err(error);
33 }
34
35 Ok(ContextResult {
36 result: TxResult::Revert(error),
37 gas_used: gas_limit,
38 gas_spent: gas_limit, // Will be updated in finalize_execution
39 output: Bytes::new(),
40 })
41 }
42 }
43 }
44
45 #[cold] // used in the hot path loop, called only really once.
46 pub fn handle_opcode_result(&mut self) -> Result<ContextResult, VMError> {
47 // On successful create check output validity
48 if self.is_create()? {
49 let validate_create = self.validate_contract_creation();
50
51 if let Err(error) = validate_create {
52 if error.should_propagate() {
53 return Err(error);
54 }
55
56 // EIP-8037 (Amsterdam+): roll back this frame's state gas in LIFO order
57 // BEFORE zeroing gas. Mirrors EELS `process_create_message`'s
58 // `refill_frame_state_gas` on the code-deposit ExceptionalHalt path.
59 // Must run before the `&mut self.current_call_frame` borrow below.
60 if self.env.config.fork >= Fork::Amsterdam {
61 let entry = self.current_call_frame.state_gas_used_at_entry;
62 self.refill_frame_state_gas(entry)?;
63 }
64
65 // Consume all gas because error was exceptional.
66 let callframe = &mut self.current_call_frame;
67 callframe.gas_remaining = 0;
68
69 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
70 let gas_used = callframe
71 .gas_limit
72 .checked_sub(callframe.gas_remaining as u64)
73 .ok_or(InternalError::Underflow)?;
74 return Ok(ContextResult {
75 result: TxResult::Revert(error),
76 gas_used,
77 gas_spent: gas_used, // Will be updated in finalize_execution
78 output: Bytes::new(),
79 });
80 }
81
82 // Set bytecode to the newly created contract.
83 let contract_address = self.current_call_frame.to;
84 let code = self.current_call_frame.output.clone();
85 self.update_account_bytecode(contract_address, Code::from_bytecode(code, self.crypto))?;
86 }
87
88 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
89 let gas_used = {
90 let callframe = &mut self.current_call_frame;
91 callframe
92 .gas_limit
93 .checked_sub(callframe.gas_remaining as u64)
94 .ok_or(InternalError::Underflow)?
95 };
96 Ok(ContextResult {
97 result: TxResult::Success,
98 gas_used,
99 gas_spent: gas_used, // Will be updated in finalize_execution
100 output: std::mem::take(&mut self.current_call_frame.output),
101 })
102 }
103
104 #[cold] // used in the hot path loop, called only really once.
105 pub fn handle_opcode_error(&mut self, error: VMError) -> Result<ContextResult, VMError> {
106 if error.should_propagate() {
107 return Err(error);
108 }
109
110 // EIP-8037 (Amsterdam+): roll back this frame's state gas in LIFO order BEFORE
111 // zeroing gas on exceptional halt. Covers both revert and exceptional-halt paths
112 // (mirrors EELS `process_message`'s `refill_frame_state_gas` on Revert/ExceptionalHalt).
113 // Must run before the `&mut self.current_call_frame` borrow below since refill needs `&mut self`.
114 if self.env.config.fork >= Fork::Amsterdam {
115 let entry = self.current_call_frame.state_gas_used_at_entry;
116 self.refill_frame_state_gas(entry)?;
117 }
118
119 let callframe = &mut self.current_call_frame;
120
121 // Unless error is caused by Revert Opcode, consume all gas left.
122 if !error.is_revert_opcode() {
123 callframe.gas_remaining = 0;
124 }
125
126 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
127 let gas_used = callframe
128 .gas_limit
129 .checked_sub(callframe.gas_remaining as u64)
130 .ok_or(InternalError::Underflow)?;
131 Ok(ContextResult {
132 result: TxResult::Revert(error),
133 gas_used,
134 gas_spent: gas_used, // Will be updated in finalize_execution
135 output: std::mem::take(&mut callframe.output),
136 })
137 }
138
139 /// Handles external create transaction.
140 pub fn handle_create_transaction(&mut self) -> Result<Option<ContextResult>, VMError> {
141 let new_contract_address = self.current_call_frame.to;
142
143 // EIP-7928: Record contract address in BAL before collision check.
144 // Per EELS reference, the address is tracked even when the create collides.
145 if let Some(recorder) = self.db.bal_recorder.as_mut() {
146 recorder.record_touched_address(new_contract_address);
147 }
148
149 let new_account = self.get_account_mut(new_contract_address)?;
150
151 if new_account.create_would_collide() {
152 // EIP-8037: `prepare_execution`'s in-region CREATE `NEW_ACCOUNT` charge
153 // runs before this collision check and fires whenever the
154 // target's pre-state was EMPTY_ACCOUNT — which can be true even for a
155 // colliding target in the EELS-documented "highly unlikely" case of a
156 // zero-nonce/zero-code/zero-balance address that nonetheless has storage
157 // (`create_would_collide` also considers storage; `is_empty()` doesn't).
158 //
159 // Known near-unreachable divergence (code review, accepted): if the
160 // reservoir/gas cannot cover that in-region NEW_ACCOUNT charge for such a
161 // storage-only colliding target, ethrex takes the region-OOG path (burns all
162 // gas) whereas EELS — which checks `account_deployable()` before
163 // `prepare_dispatch` — never charges and preserves the reservoir. This
164 // requires deriving a CREATE address that already holds storage with zero
165 // nonce/code/balance AND under-funding it, which is not reachable via real
166 // CREATE/CREATE2 address derivation; documented rather than guarded.
167 //
168 // Roll that charge back to the frame's entry baseline: EELS never runs
169 // `prepare_dispatch` for a colliding create (`process_message_call`
170 // short-circuits on `account_deployable() == False` before calling
171 // `process_create_message`), so a collision must leave `state_gas_used`
172 // (and any state-gas spill) net zero. No opcodes have run yet, so
173 // `frame_used = state_gas_used - entry` is exactly this charge, if any.
174 if self.env.config.fork >= Fork::Amsterdam {
175 let entry = self.current_call_frame.state_gas_used_at_entry;
176 self.refill_frame_state_gas(entry)?;
177 }
178
179 // Per EIP-684: a tx-level CREATE collision burns the
180 // full forwarded execution gas as `regular_gas_used`. Zero `gas_remaining`
181 // so `raw_consumed = gas_limit` for the downstream regular-gas formula in
182 // `default_hook::refund_sender`; otherwise the post-intrinsic leftover
183 // leaks back to the sender and never reaches the regular dimension.
184 self.current_call_frame.gas_remaining = 0;
185 return Ok(Some(ContextResult {
186 result: TxResult::Revert(ExceptionalHalt::AddressAlreadyOccupied.into()),
187 gas_used: self.env.gas_limit,
188 gas_spent: self.env.gas_limit, // Will be updated in finalize_execution
189 output: Bytes::new(),
190 }));
191 }
192
193 let value = self.current_call_frame.msg_value;
194 self.increase_account_balance(new_contract_address, value)?;
195
196 // EIP-7708: Emit transfer log for nonzero-value contract creation transactions.
197 // Origin is sender, new_contract_address is the recipient.
198 if self.env.config.fork >= Fork::Amsterdam && !value.is_zero() {
199 let log = create_eth_transfer_log(self.env.origin, new_contract_address, value);
200 self.substate.add_log(log);
201 }
202
203 self.increment_account_nonce(new_contract_address)?;
204
205 Ok(None)
206 }
207
208 /// Validates that the contract creation was successful, otherwise it returns an ExceptionalHalt.
209 fn validate_contract_creation(&mut self) -> Result<(), VMError> {
210 let fork = self.env.config.fork;
211 let code = &self.current_call_frame.output;
212
213 let code_length: u64 = code
214 .len()
215 .try_into()
216 .map_err(|_| InternalError::TypeConversion)?;
217
218 // 1. If the first byte of code is 0xEF
219 if code.first().is_some_and(|v| v == &EOF_PREFIX) {
220 return Err(ExceptionalHalt::InvalidContractPrefix.into());
221 }
222
223 // EIP-8037 (Amsterdam+): Per EELS process_create_message (bal@v5.4.0):
224 // 1. Size check first (reject oversized before any gas charges)
225 // 2. Keccak hash cost (regular gas)
226 // 3. State gas for code deposit
227 if fork >= Fork::Amsterdam {
228 // Size check BEFORE gas charges
229 if code_length > AMSTERDAM_MAX_CODE_SIZE {
230 return Err(ExceptionalHalt::ContractOutputTooBig.into());
231 }
232
233 let words = code_length.div_ceil(32);
234 let regular = words
235 .checked_mul(CODE_DEPOSIT_REGULAR_COST_PER_WORD)
236 .ok_or(InternalError::Overflow)?;
237 let state = code_length
238 .checked_mul(self.cost_per_state_byte)
239 .ok_or(InternalError::Overflow)?;
240
241 // Regular gas (keccak hash cost) before state gas
242 self.current_call_frame.increase_consumed_gas(regular)?;
243 if state > 0 {
244 self.increase_state_gas(state)?;
245 }
246 } else {
247 // Pre-Amsterdam: size check first, then regular gas charge
248 if code_length > MAX_CODE_SIZE {
249 return Err(ExceptionalHalt::ContractOutputTooBig.into());
250 }
251 let regular = code_length
252 .checked_mul(CODE_DEPOSIT_COST)
253 .ok_or(InternalError::Overflow)?;
254 self.current_call_frame.increase_consumed_gas(regular)?;
255 }
256
257 Ok(())
258 }
259}