Skip to main content

ethrex_levm/opcode_handlers/
frame_tx.rs

1//! # EIP-8141 Frame Transaction opcodes
2//!
3//! Includes:
4//!   - `APPROVE` (0xAA)
5//!   - `TXPARAM` (0xB0)
6//!   - `FRAMEDATALOAD` (0xB1)
7//!   - `FRAMEDATACOPY` (0xB2)
8//!   - `FRAMEPARAM` (0xB3)
9//!   - `SIGPARAM` (0xB4)
10//!   - Default code for EOAs: `VERIFY` has the signature-check behavior;
11//!     `SENDER` and `DEFAULT` return successfully as if calling empty code
12//!     (EIP-8141 §"Default code").
13
14use crate::{
15    errors::{ExceptionalHalt, InternalError, OpcodeResult, VMError},
16    gas_cost,
17    memory::calculate_memory_size,
18    opcode_handlers::OpcodeHandler,
19    utils::size_offset_to_usize,
20    vm::VM,
21};
22use ethrex_common::{Address, U256, types::FrameMode, types::Log};
23
24/// Convert a u64 index to usize, returning InvalidOpcode on overflow.
25fn index_to_usize(val: u64) -> Result<usize, VMError> {
26    usize::try_from(val).map_err(|_| ExceptionalHalt::InvalidOpcode.into())
27}
28
29/// Convert a U256 offset to usize, returning None when the value does not fit
30/// in usize on the current target. Used by FRAMEDATALOAD and FRAMEDATACOPY so
31/// out-of-range offsets are treated as past-the-end rather than as an
32/// exceptional halt (per the EIP-8141 spec the load returns zero and the copy
33/// writes zero bytes).
34pub fn u256_to_offset(value: U256) -> Option<usize> {
35    if value.0[1] != 0 || value.0[2] != 0 || value.0[3] != 0 {
36        return None;
37    }
38    usize::try_from(value.0[0]).ok()
39}
40
41/// Compute the transaction's MAXIMUM cost (EIP-8141 §Gas Accounting: APPROVE must
42/// "collect the transaction's maximum cost from payer"):
43/// `max_cost = max_fee_per_gas * max_gas
44///           + len(blob_hashes) * 131072 * blob_base_fee`.
45/// This is the single definition of "maximum cost": APPROVE (scopes 0x1/0x3)
46/// debits it from the payer, TXPARAM(0x06) reports it, and the
47/// mempool paymaster reservation reserves it. The end-of-tx refund returns
48/// `max_cost - effective_gas_price * total_gas_used - base-rate blob burn`, so
49/// the payer nets the effective-rate cost of the gas actually used plus the
50/// EIP-4844 blob burn (intrinsic gas is inside `total_gas_used`, so it stays
51/// non-refundable).
52pub(crate) fn compute_tx_max_cost(ctx: &crate::vm::FrameTxContext) -> Result<U256, VMError> {
53    let gas_cost = U256::from(ctx.tx.max_fee_per_gas)
54        .checked_mul(U256::from(ctx.max_gas))
55        .ok_or(ExceptionalHalt::InvalidOpcode)?;
56    let blob_cost = U256::from(ctx.tx.blob_versioned_hashes.len())
57        .checked_mul(U256::from(131072u64))
58        .ok_or(ExceptionalHalt::InvalidOpcode)?
59        .checked_mul(ctx.blob_base_fee)
60        .ok_or(ExceptionalHalt::InvalidOpcode)?;
61    gas_cost
62        .checked_add(blob_cost)
63        .ok_or(ExceptionalHalt::InvalidOpcode.into())
64}
65
66/// Apply APPROVE side effects for the given scope.
67/// This is shared between OpApproveHandler and (future) default code.
68pub fn apply_approve(
69    vm: &mut VM<'_>,
70    scope: u64,
71    frame_target: ethrex_common::Address,
72) -> Result<(), VMError> {
73    match scope {
74        0x1 => {
75            // APPROVE_PAYMENT: increment nonce, deduct max cost, record payer.
76            // Per spec, the single transaction-scoped variable `payer` is
77            // set on success; `payer.is_some()` is the source of truth for
78            // "payment has been approved".
79            let ctx = vm
80                .frame_tx_context
81                .as_ref()
82                .ok_or(ExceptionalHalt::InvalidOpcode)?;
83            if ctx.payer_address.is_some() {
84                return Err(ExceptionalHalt::InvalidOpcode.into());
85            }
86            // EIP-8141: payment approval must not precede the sender's execution
87            // approval. Per the spec's APPROVE_PAYMENT rules, revert the frame
88            // while sender_approved == false (the sender authorizes execution
89            // first; only then may a payer be bound and the max cost collected).
90            if !ctx.sender_approved {
91                return Err(VMError::RevertOpcode);
92            }
93            let tx_cost = compute_tx_max_cost(ctx)?;
94            let sender = ctx.tx.sender;
95
96            vm.increment_account_nonce(sender)?;
97            // Payer balance underflow is a frame-level revert, not a consensus
98            // fault: the outer restore_cache_state() path rolls back the nonce
99            // increment above when RevertOpcode propagates.
100            match vm.decrease_account_balance(frame_target, tx_cost) {
101                Ok(()) => {}
102                Err(InternalError::Underflow) => return Err(VMError::RevertOpcode),
103                Err(e) => return Err(VMError::Internal(e)),
104            }
105
106            let ctx = vm
107                .frame_tx_context
108                .as_mut()
109                .ok_or(ExceptionalHalt::InvalidOpcode)?;
110            ctx.payer_address = Some(frame_target);
111        }
112        0x2 => {
113            // APPROVE_EXECUTION: set sender_approved (requires frame_target == tx.sender)
114            let ctx = vm
115                .frame_tx_context
116                .as_ref()
117                .ok_or(ExceptionalHalt::InvalidOpcode)?;
118            if ctx.sender_approved {
119                return Err(ExceptionalHalt::InvalidOpcode.into());
120            }
121            if frame_target != ctx.tx.sender {
122                return Err(VMError::RevertOpcode);
123            }
124            let ctx = vm
125                .frame_tx_context
126                .as_mut()
127                .ok_or(ExceptionalHalt::InvalidOpcode)?;
128            ctx.sender_approved = true;
129        }
130        0x3 => {
131            // APPROVE_EXECUTION_AND_PAYMENT: both, in one atomic step.
132            let ctx = vm
133                .frame_tx_context
134                .as_ref()
135                .ok_or(ExceptionalHalt::InvalidOpcode)?;
136            if ctx.sender_approved || ctx.payer_address.is_some() {
137                return Err(ExceptionalHalt::InvalidOpcode.into());
138            }
139            if frame_target != ctx.tx.sender {
140                return Err(VMError::RevertOpcode);
141            }
142            let tx_cost = compute_tx_max_cost(ctx)?;
143            let sender = ctx.tx.sender;
144
145            vm.increment_account_nonce(sender)?;
146            // See scope 0x1 above for the Underflow → RevertOpcode rationale.
147            match vm.decrease_account_balance(frame_target, tx_cost) {
148                Ok(()) => {}
149                Err(InternalError::Underflow) => return Err(VMError::RevertOpcode),
150                Err(e) => return Err(VMError::Internal(e)),
151            }
152
153            let ctx = vm
154                .frame_tx_context
155                .as_mut()
156                .ok_or(ExceptionalHalt::InvalidOpcode)?;
157            ctx.sender_approved = true;
158            ctx.payer_address = Some(frame_target);
159        }
160        _ => {
161            // scope 0 and any other value are invalid
162            return Err(ExceptionalHalt::InvalidOpcode.into());
163        }
164    }
165    Ok(())
166}
167
168/// APPROVE (0xAA) -- Frame transaction approval opcode.
169///
170/// Pops [offset, length, scope] from the stack.
171/// - scope 0x1 (APPROVE_PAYMENT): increment nonce, deduct tx cost, record payer
172/// - scope 0x2 (APPROVE_EXECUTION): set sender_approved (requires resolved_target == tx.sender)
173/// - scope 0x3 (APPROVE_EXECUTION_AND_PAYMENT): both, in one atomic step
174/// - scope 0x0 (APPROVE_NONE) and any value > 3: invalid (exceptional halt)
175///
176/// The requested scope must also be a subset of the frame's allowed scope, taken
177/// from flags bits 0-1 (`frame.scope_restriction()`). When the allowed scope is 0
178/// (APPROVE_SCOPE_NONE) no approval may be granted in the frame at all, so APPROVE
179/// halts (consistent with `execute_default_verify`).
180///
181/// On success, copies memory[offset..offset+length] to output and halts the frame.
182pub struct OpApproveHandler;
183impl OpcodeHandler for OpApproveHandler {
184    #[inline(always)]
185    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
186        let [offset, length, scope] = *vm.current_call_frame.stack.pop()?;
187        let (length, offset) = size_offset_to_usize(length, offset)?;
188
189        // Must be in a frame transaction context
190        let ctx = vm
191            .frame_tx_context
192            .as_ref()
193            .ok_or(ExceptionalHalt::InvalidOpcode)?;
194
195        // The executing contract must be the frame's target
196        let current_frame = ctx
197            .tx
198            .frames
199            .get(ctx.current_frame_index)
200            .ok_or(ExceptionalHalt::InvalidOpcode)?;
201        let frame_target = current_frame.target.unwrap_or(ctx.tx.sender);
202        if vm.current_call_frame.to != frame_target {
203            return Err(VMError::RevertOpcode);
204        }
205
206        // Enforce scope restriction from flags bits 0-1.
207        // allowed_scope == 0 is APPROVE_SCOPE_NONE: no approval may be granted
208        // in this frame at all (consistent with execute_default_verify).
209        let allowed_scope = current_frame.scope_restriction();
210        let scope_val = u64::try_from(scope).unwrap_or(u64::MAX);
211        // requested scope must be a non-zero subset of a (necessarily non-zero) allowed_scope
212        if scope_val == 0 || scope_val > 3 || (scope_val & u64::from(allowed_scope)) != scope_val {
213            return Err(ExceptionalHalt::InvalidOpcode.into());
214        }
215
216        // Charge gas (memory expansion, same as RETURN)
217        vm.current_call_frame
218            .increase_consumed_gas(gas_cost::exit_opcode(
219                calculate_memory_size(offset, length)?,
220                vm.current_call_frame.memory.len(),
221            )?)?;
222
223        apply_approve(vm, scope_val, frame_target)?;
224
225        let ctx = vm
226            .frame_tx_context
227            .as_mut()
228            .ok_or(ExceptionalHalt::InvalidOpcode)?;
229        ctx.approve_called_in_current_frame = true;
230
231        // Copy memory to output (like RETURN)
232        if length != 0 {
233            vm.current_call_frame.output =
234                vm.current_call_frame.memory.load_range(offset, length)?;
235        }
236
237        Ok(OpcodeResult::Halt)
238    }
239}
240
241/// TXPARAM (0xB0) -- Load a transaction parameter as a 32-byte word.
242/// Gas cost: 2
243pub struct OpTxParamHandler;
244impl OpcodeHandler for OpTxParamHandler {
245    #[inline(always)]
246    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
247        let [param_id] = *vm.current_call_frame.stack.pop()?;
248
249        vm.current_call_frame
250            .increase_consumed_gas(gas_cost::TXPARAM)?;
251
252        let ctx = vm
253            .frame_tx_context
254            .as_ref()
255            .ok_or(ExceptionalHalt::InvalidOpcode)?;
256
257        let param_id = u64::try_from(param_id).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
258        let result = load_tx_param(ctx, param_id)?;
259        vm.current_call_frame.stack.push(result)?;
260
261        Ok(OpcodeResult::Continue)
262    }
263}
264
265/// FRAMEDATALOAD (0xB1) -- Load one 32-byte word from a frame's data.
266/// Stack: [offset, frameIndex] with offset on top (popped first); frameIndex is
267/// the deeper operand. Gas cost: 3.
268pub struct OpFrameDataLoadHandler;
269impl OpcodeHandler for OpFrameDataLoadHandler {
270    #[inline(always)]
271    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
272        let [offset, frame_index] = *vm.current_call_frame.stack.pop()?;
273
274        vm.current_call_frame
275            .increase_consumed_gas(gas_cost::FRAMEDATALOAD)?;
276
277        let ctx = vm
278            .frame_tx_context
279            .as_ref()
280            .ok_or(ExceptionalHalt::InvalidOpcode)?;
281
282        let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
283        let idx = index_to_usize(frame_index)?;
284        let frame = ctx
285            .tx
286            .frames
287            .get(idx)
288            .ok_or(ExceptionalHalt::InvalidOpcode)?;
289
290        // Out-of-usize offsets are past-the-end: the word stays zero-filled.
291        let mut word = [0u8; 32];
292        if let Some(byte_offset) = u256_to_offset(offset) {
293            let data = &frame.data;
294            let available = data.len().saturating_sub(byte_offset);
295            let copy_len = available.min(32);
296            if copy_len > 0
297                && let Some(src) = data.get(byte_offset..byte_offset.saturating_add(copy_len))
298            {
299                // copy_len <= 32 == word.len(), so this slice is in bounds.
300                if let Some(dst) = word.get_mut(..copy_len) {
301                    dst.copy_from_slice(src);
302                }
303            }
304        }
305
306        vm.current_call_frame
307            .stack
308            .push(U256::from_big_endian(&word))?;
309
310        Ok(OpcodeResult::Continue)
311    }
312}
313
314/// FRAMEDATACOPY (0xB2) -- Copy frame data into memory.
315/// Takes [memOffset, dataOffset, length, frameIndex] from the stack.
316/// Gas cost matches CALLDATACOPY.
317pub struct OpFrameDataCopyHandler;
318impl OpcodeHandler for OpFrameDataCopyHandler {
319    #[inline(always)]
320    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
321        let [mem_offset, data_offset, length, frame_index] = *vm.current_call_frame.stack.pop()?;
322        let (length, mem_offset) = size_offset_to_usize(length, mem_offset)?;
323        // Out-of-usize data_offset is past-the-end: destination stays zero-filled.
324        let data_offset_opt = u256_to_offset(data_offset);
325
326        let new_memory_size = calculate_memory_size(mem_offset, length)?;
327        let current_memory_size = vm.current_call_frame.memory.len();
328        // Charging memory-expansion gas before the frame-context guard below is
329        // intentional: the caller pays for the memory growth it requested even
330        // when the opcode then halts for running outside a frame tx.
331        vm.current_call_frame
332            .increase_consumed_gas(gas_cost::framedatacopy(
333                new_memory_size,
334                current_memory_size,
335                length,
336            )?)?;
337
338        // Frame-context and frame_index checks precede the zero-length early
339        // return: an out-of-bounds frameIndex halts exceptionally even when
340        // length == 0 (EIP-8141 §FRAMEDATACOPY, consensus parity with FRAMEDATALOAD).
341        let ctx = vm
342            .frame_tx_context
343            .as_ref()
344            .ok_or(ExceptionalHalt::InvalidOpcode)?;
345
346        let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
347        let idx = index_to_usize(frame_index)?;
348        let frame = ctx
349            .tx
350            .frames
351            .get(idx)
352            .ok_or(ExceptionalHalt::InvalidOpcode)?;
353
354        if length == 0 {
355            return Ok(OpcodeResult::Continue);
356        }
357
358        let data = &frame.data;
359        let mut buf = vec![0u8; length];
360        if let Some(data_offset) = data_offset_opt {
361            let available = data.len().saturating_sub(data_offset);
362            let copy_len = length.min(available);
363            if let (Some(dst), Some(src)) = (
364                buf.get_mut(..copy_len),
365                data.get(data_offset..data_offset.saturating_add(copy_len)),
366            ) {
367                dst.copy_from_slice(src);
368            }
369        }
370
371        vm.current_call_frame.memory.store_data(mem_offset, &buf)?;
372
373        Ok(OpcodeResult::Continue)
374    }
375}
376
377/// FRAMEPARAM (0xB3) -- Load a frame parameter as a 32-byte word.
378/// Stack: [param, frameIndex] with frameIndex on top (matches SIGPARAM). Gas cost: 2.
379pub struct OpFrameParamHandler;
380impl OpcodeHandler for OpFrameParamHandler {
381    #[inline(always)]
382    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
383        let [frame_index, param_id] = *vm.current_call_frame.stack.pop()?;
384
385        vm.current_call_frame
386            .increase_consumed_gas(gas_cost::FRAMEPARAM)?;
387
388        let ctx = vm
389            .frame_tx_context
390            .as_ref()
391            .ok_or(ExceptionalHalt::InvalidOpcode)?;
392
393        let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
394        let idx = index_to_usize(frame_index)?;
395        let frame = ctx
396            .tx
397            .frames
398            .get(idx)
399            .ok_or(ExceptionalHalt::InvalidOpcode)?;
400
401        let param_id = u64::try_from(param_id).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
402        let result: U256 = match param_id {
403            0x00 => {
404                // target
405                address_to_u256(frame.target.unwrap_or(ctx.tx.sender))
406            }
407            0x01 => {
408                // gas_limit
409                U256::from(frame.gas_limit)
410            }
411            0x02 => {
412                // mode
413                U256::from(frame.mode)
414            }
415            0x03 => {
416                // flags
417                U256::from(frame.flags)
418            }
419            0x04 => {
420                // len(data)
421                U256::from(frame.data.len())
422            }
423            0x05 => {
424                // status -- exceptional halt if current/future frame.
425                // Returns the EIP-8141 status code: 0 = failure, 1 = success,
426                // 2 = skipped (atomic-batch failure).
427                if idx >= ctx.current_frame_index {
428                    return Err(ExceptionalHalt::InvalidOpcode.into());
429                }
430                let (status, _, _) = ctx
431                    .frame_results
432                    .get(idx)
433                    .ok_or(ExceptionalHalt::InvalidOpcode)?;
434                U256::from(*status)
435            }
436            0x06 => {
437                // allowed_scope (flags & 0x03)
438                U256::from(frame.scope_restriction())
439            }
440            0x07 => {
441                // atomic_batch ((flags >> 2) & 1, returns 0 or 1)
442                U256::from(u8::from(frame.is_atomic_batch()))
443            }
444            0x08 => {
445                // value -- EIP-8141 FRAMEPARAM table
446                frame.value
447            }
448            _ => return Err(ExceptionalHalt::InvalidOpcode.into()),
449        };
450
451        vm.current_call_frame.stack.push(result)?;
452
453        Ok(OpcodeResult::Continue)
454    }
455}
456
457/// SIGPARAM (0xB4) -- signature-scoped metadata and data copy (EIP-8141).
458/// Metadata (params 0x00-0x03): stack `[param, signatureIndex]` with
459/// `signatureIndex` on top; gas 2; returns one word (0x00 effective signer,
460/// 0x01 scheme, 0x02 msg, 0x03 len(signature)). Copy (param 0x04): takes
461/// `[signatureIndex, param, memOffset, dataOffset, length]` with `signatureIndex`
462/// on top (popped first), matching `CALLDATACOPY`'s operand order; CALLDATACOPY
463/// gas; copies an ARBITRARY signature's raw bytes into memory (zero-filled past
464/// the end) and pushes nothing — any other scheme halts.
465pub struct OpSigParamHandler;
466impl OpcodeHandler for OpSigParamHandler {
467    #[inline(always)]
468    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
469        let [signature_index, param] = *vm.current_call_frame.stack.pop()?;
470        let param = u64::try_from(param).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
471        let signature_index =
472            u64::try_from(signature_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
473        let idx = index_to_usize(signature_index)?;
474
475        // 0x04: copy the referenced ARBITRARY signature's raw bytes into memory,
476        // CALLDATACOPY-style (see FRAMEDATACOPY). Pops three more operands.
477        if param == 0x04 {
478            let [mem_offset, data_offset, length] = *vm.current_call_frame.stack.pop()?;
479            let (length, mem_offset) = size_offset_to_usize(length, mem_offset)?;
480            let data_offset_opt = u256_to_offset(data_offset);
481
482            let new_memory_size = calculate_memory_size(mem_offset, length)?;
483            let current_memory_size = vm.current_call_frame.memory.len();
484            // Charge memory-expansion gas before the context/scheme guards: the
485            // caller pays for the growth it requested even if the opcode halts.
486            vm.current_call_frame
487                .increase_consumed_gas(gas_cost::framedatacopy(
488                    new_memory_size,
489                    current_memory_size,
490                    length,
491                )?)?;
492
493            let ctx = vm
494                .frame_tx_context
495                .as_ref()
496                .ok_or(ExceptionalHalt::InvalidOpcode)?;
497            let sig = ctx
498                .tx
499                .signatures
500                .get(idx)
501                .ok_or(ExceptionalHalt::InvalidOpcode)?;
502            // 0x04 is only defined for ARBITRARY signatures.
503            if sig.scheme != ethrex_common::types::FRAME_SIG_SCHEME_ARBITRARY {
504                return Err(ExceptionalHalt::InvalidOpcode.into());
505            }
506            if length == 0 {
507                return Ok(OpcodeResult::Continue);
508            }
509            let data = &sig.signature;
510            let mut buf = vec![0u8; length];
511            if let Some(data_offset) = data_offset_opt {
512                let available = data.len().saturating_sub(data_offset);
513                let copy_len = length.min(available);
514                if let (Some(dst), Some(src)) = (
515                    buf.get_mut(..copy_len),
516                    data.get(data_offset..data_offset.saturating_add(copy_len)),
517                ) {
518                    dst.copy_from_slice(src);
519                }
520            }
521            vm.current_call_frame.memory.store_data(mem_offset, &buf)?;
522            return Ok(OpcodeResult::Continue);
523        }
524
525        // Metadata (0x00-0x03): fixed gas, returns one word.
526        vm.current_call_frame
527            .increase_consumed_gas(gas_cost::SIGPARAM)?;
528        let ctx = vm
529            .frame_tx_context
530            .as_ref()
531            .ok_or(ExceptionalHalt::InvalidOpcode)?;
532        let sig = ctx
533            .tx
534            .signatures
535            .get(idx)
536            .ok_or(ExceptionalHalt::InvalidOpcode)?;
537        let result = match param {
538            // Resolved signer: an absent signer resolves to tx.sender. EIP-8141
539            // assigns no resolved signer to ARBITRARY entries, so asking for one
540            // is an exceptional halt.
541            0x00 => {
542                if sig.scheme == ethrex_common::types::FRAME_SIG_SCHEME_ARBITRARY {
543                    return Err(ExceptionalHalt::InvalidOpcode.into());
544                }
545                address_to_u256(sig.signer.unwrap_or(ctx.tx.sender))
546            }
547            0x01 => U256::from(sig.scheme),
548            0x02 => {
549                // msg: 0 when empty (canonical sig_hash case), else the 32-byte digest.
550                if sig.msg.is_empty() {
551                    U256::zero()
552                } else {
553                    U256::from_big_endian(&sig.msg)
554                }
555            }
556            0x03 => U256::from(sig.signature.len()),
557            _ => return Err(ExceptionalHalt::InvalidOpcode.into()),
558        };
559        vm.current_call_frame.stack.push(result)?;
560        Ok(OpcodeResult::Continue)
561    }
562}
563
564// -- Helper functions --
565
566pub fn load_tx_param(ctx: &crate::vm::FrameTxContext, param_id: u64) -> Result<U256, VMError> {
567    match param_id {
568        0x00 => Ok(U256::from(0x06u8)), // tx_type (EIP-8141 = type 6)
569        0x01 => Ok(U256::from(ctx.tx.nonce)),
570        0x02 => Ok(address_to_u256(ctx.tx.sender)),
571        0x03 => Ok(U256::from(ctx.tx.max_priority_fee_per_gas)),
572        0x04 => Ok(U256::from(ctx.tx.max_fee_per_gas)),
573        0x05 => Ok(ctx.tx.max_fee_per_blob_gas),
574        0x06 => compute_tx_max_cost(ctx),
575        0x07 => Ok(U256::from(ctx.tx.blob_versioned_hashes.len())),
576        0x08 => {
577            let mut bytes = [0u8; 32];
578            bytes.copy_from_slice(ctx.sig_hash.as_bytes());
579            Ok(U256::from_big_endian(&bytes))
580        }
581        0x09 => Ok(U256::from(ctx.tx.frames.len())),
582        0x0A => Ok(U256::from(ctx.current_frame_index)),
583        0x0B => Ok(U256::from(ctx.tx.signatures.len())),
584        _ => Err(ExceptionalHalt::InvalidOpcode.into()),
585    }
586}
587
588pub fn address_to_u256(addr: ethrex_common::Address) -> U256 {
589    let mut bytes = [0u8; 32];
590    bytes[12..].copy_from_slice(addr.as_bytes());
591    U256::from_big_endian(&bytes)
592}
593
594// -- Default code for EOAs (EIP-8141) --
595
596/// Execute default code for an EOA target in a frame transaction.
597///
598/// When a frame targets an address with no deployed code (an EOA), the protocol
599/// runs built-in "default code" instead of executing a normal CALL. `VERIFY`
600/// runs the signature-check logic; `SENDER` and `DEFAULT` return successfully
601/// as if calling empty code (EIP-8141 §"Default code").
602///
603/// Returns `(success, gas_used, logs)`.
604pub fn execute_default_code(
605    vm: &mut VM<'_>,
606    frame: &ethrex_common::types::Frame,
607    target: Address,
608) -> Result<(bool, u64, Vec<Log>), VMError> {
609    match frame.execution_mode() {
610        FrameMode::Verify => execute_default_verify(vm, frame, target),
611        // EIP-8141 §"Default code": a SENDER or DEFAULT frame whose target has no code "returns
612        // successfully as if calling empty code" — this is what makes a plain
613        // ETH transfer to an EOA work (spec §EOA support / Example 1).
614        // Consumes no execution gas (the frame's value transfer is handled by
615        // the caller's deferred transfer).
616        FrameMode::Sender | FrameMode::Default => Ok((true, 0, Vec::new())),
617    }
618}
619
620fn execute_default_verify(
621    vm: &mut VM<'_>,
622    frame: &ethrex_common::types::Frame,
623    target: Address,
624) -> Result<(bool, u64, Vec<Log>), VMError> {
625    let ctx = vm
626        .frame_tx_context
627        .as_ref()
628        .ok_or(ExceptionalHalt::InvalidOpcode)?;
629
630    // Read allowed scope from flags bits 0-1
631    let allowed_scope = u64::from(frame.scope_restriction());
632    if allowed_scope == 0 {
633        return Ok((false, 0, Vec::new()));
634    }
635
636    // If scope includes APPROVE_EXECUTION and resolved_target != tx.sender, revert
637    if (allowed_scope & 0x02) != 0 && target != ctx.tx.sender {
638        return Ok((false, 0, Vec::new()));
639    }
640
641    // EIP-8141: the default account approves only if the signature at a specific
642    // index — 0 when the allowed scope includes APPROVE_EXECUTION, else 1 (the
643    // payment-only case, where index 0 belongs to the sender's own verify frame)
644    // — is a SECP256K1 signature over the canonical sig_hash (empty msg) whose
645    // resolved signer is the resolved target. An absent signer resolves to
646    // tx.sender. Signatures were already validated in execute_frame_tx, so a
647    // match here is sufficient — no in-frame crypto.
648    let sig_index = if (allowed_scope & 0x02) != 0 { 0 } else { 1 };
649    let sender_sig_ok = ctx.tx.signatures.get(sig_index).is_some_and(|s| {
650        s.scheme == ethrex_common::types::FRAME_SIG_SCHEME_SECP256K1
651            && s.msg.is_empty()
652            && s.signer.unwrap_or(ctx.tx.sender) == target
653    });
654    if !sender_sig_ok {
655        return Ok((false, 0, Vec::new()));
656    }
657
658    apply_approve(vm, allowed_scope, target)?;
659
660    let ctx = vm
661        .frame_tx_context
662        .as_mut()
663        .ok_or(ExceptionalHalt::InvalidOpcode)?;
664    ctx.approve_called_in_current_frame = true;
665
666    Ok((true, 0, Vec::new()))
667}
668
669#[cfg(test)]
670mod max_cost_tests {
671    use super::{compute_tx_max_cost, load_tx_param};
672    use crate::vm::FrameTxContext;
673    use ethrex_common::{H256, U256, types::FrameTransaction};
674
675    fn ctx(max_fee: u64, blobs: usize, blob_base_fee: u64, max_gas: u64) -> FrameTxContext {
676        let tx = FrameTransaction {
677            max_fee_per_gas: max_fee,
678            // Deliberately far above the base fee: `max_fee_per_blob_gas` bounds
679            // inclusion only and must not reach `max_cost`.
680            max_fee_per_blob_gas: U256::from(blob_base_fee).saturating_mul(U256::from(1_000u64)),
681            blob_versioned_hashes: vec![H256::zero(); blobs],
682            ..Default::default()
683        };
684        FrameTxContext {
685            sender_approved: false,
686            payer_address: None,
687            frame_results: Vec::new(),
688            current_frame_index: 0,
689            sig_hash: H256::zero(),
690            tx,
691            approve_called_in_current_frame: false,
692            max_gas,
693            blob_base_fee: U256::from(blob_base_fee),
694        }
695    }
696
697    #[test]
698    fn max_cost_is_max_fee_times_limit_plus_base_rate_blob_cost() {
699        // 10 * 100_000 + 2 * 131072 * 5 = 1_000_000 + 1_310_720
700        let c = ctx(10, 2, 5, 100_000);
701        assert_eq!(compute_tx_max_cost(&c).unwrap(), U256::from(2_310_720u64));
702        // No blobs: just max_fee * max_gas.
703        let c = ctx(7, 0, 999, 21_000);
704        assert_eq!(compute_tx_max_cost(&c).unwrap(), U256::from(147_000u64));
705    }
706
707    #[test]
708    fn txparam_0x06_reports_the_same_maximum_cost_approve_debits() {
709        // TXPARAM(0x06) and the APPROVE debit must stay one definition of
710        // "maximum cost"; a split between them is a consensus bug.
711        let c = ctx(10, 2, 5, 100_000);
712        assert_eq!(
713            load_tx_param(&c, 0x06).unwrap(),
714            compute_tx_max_cost(&c).unwrap()
715        );
716    }
717}