diff --git a/crates/vm/levm/src/opcode_handlers/frame_tx.rs b/crates/vm/levm/src/opcode_handlers/frame_tx.rs index f5cdffa4330..cdc2edf7e79 100644 --- a/crates/vm/levm/src/opcode_handlers/frame_tx.rs +++ b/crates/vm/levm/src/opcode_handlers/frame_tx.rs @@ -65,11 +65,14 @@ pub(crate) fn compute_tx_max_cost(ctx: &crate::vm::FrameTxContext) -> Result, scope: u64, frame_target: ethrex_common::Address, -) -> Result<(), VMError> { +) -> Result { + let mut surcharge = 0; match scope { 0x1 => { // APPROVE_PAYMENT: increment nonce, deduct max cost, record payer. @@ -107,7 +110,7 @@ pub fn apply_approve( let tx_cost = compute_tx_max_cost(ctx)?; let sender = ctx.tx.sender; - vm.consume_keyed_nonces(sender)?; + surcharge = vm.consume_keyed_nonces(sender)?; // Payer balance underflow is a frame-level revert, not a consensus // fault: the outer restore_cache_state() path rolls back the nonce // increment above when RevertOpcode propagates. @@ -162,7 +165,7 @@ pub fn apply_approve( let tx_cost = compute_tx_max_cost(ctx)?; let sender = ctx.tx.sender; - vm.consume_keyed_nonces(sender)?; + surcharge = vm.consume_keyed_nonces(sender)?; // See scope 0x1 above for the Underflow → RevertOpcode rationale. match vm.decrease_account_balance(frame_target, tx_cost) { Ok(()) => {} @@ -182,7 +185,7 @@ pub fn apply_approve( return Err(ExceptionalHalt::InvalidOpcode.into()); } } - Ok(()) + Ok(surcharge) } /// APPROVE (0xAA) -- Frame transaction approval opcode. @@ -251,7 +254,22 @@ impl OpcodeHandler for OpApproveHandler { vm.current_call_frame.memory.len(), )?)?; - apply_approve(vm, scope_val, frame_target)?; + let approval_snapshot = vm + .frame_tx_context + .as_ref() + .map(|ctx| (ctx.payer_address, ctx.sender_approved)); + let surcharge = apply_approve(vm, scope_val, frame_target)?; + // The approval context is not database-backed, so the frame rollback that undoes the debit + // and the nonce consumption would leave the payer recorded and refunded at end of tx. + if let Err(err) = vm.current_call_frame.increase_consumed_gas(surcharge) { + if let (Some((payer, sender_approved)), Some(ctx)) = + (approval_snapshot, vm.frame_tx_context.as_mut()) + { + ctx.payer_address = payer; + ctx.sender_approved = sender_approved; + } + return Err(err.into()); + } let ctx = vm .frame_tx_context @@ -862,7 +880,14 @@ fn execute_default_verify( return Ok((false, 0, Vec::new())); } - apply_approve(vm, allowed_scope, target)?; + // The surcharge comes out of this frame's budget like any other gas, so a frame that cannot + // afford it approves nothing. The affordability gate has to run before any state is touched, + // hence the estimate here; what is charged below is what consumption actually took. + if vm.keyed_nonce_first_use_surcharge()? > frame.gas_limit { + return Ok((false, frame.gas_limit, Vec::new())); + } + + let charged = apply_approve(vm, allowed_scope, target)?; let ctx = vm .frame_tx_context @@ -870,7 +895,7 @@ fn execute_default_verify( .ok_or(ExceptionalHalt::InvalidOpcode)?; ctx.approve_called_in_current_frame = true; - Ok((true, 0, Vec::new())) + Ok((true, charged, Vec::new())) } #[cfg(test)] diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index 1d24a5cd4c2..f7646abf2a5 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -908,6 +908,18 @@ pub fn find_batch_end(frames: &[Frame], failed_idx: usize) -> usize { .unwrap_or(failed_idx) } +/// The EIP-8250 nonce manager storage slot holding the sequence number of +/// `(sender, key)`: `keccak256(zeros(12) || sender || key)`. +/// +/// The pre-check that prices a first use and the consumption that performs it +/// must address the same slot, so both derive it here. +fn keyed_nonce_slot(sender: Address, key: &U256) -> H256 { + let mut preimage = [0u8; 64]; + preimage[12..32].copy_from_slice(sender.as_bytes()); + preimage[32..64].copy_from_slice(&key.to_big_endian()); + H256(ethrex_crypto::keccak::keccak_hash(preimage)) +} + /// EIP-7906: install the transaction prestate map on `db` for `tx`, or clear it. /// /// The map is needed only by transactions that can execute TXTRACE / @@ -1662,7 +1674,7 @@ impl<'a> VM<'a> { /// EIP-8250's strict "consumption MUST NOT be reverted by an atomic-batch /// snapshot" durability is tracked for devnet/interop validation — see /// `docs/eip-8250.md`. Key-0 consumption matches existing EIP-8141 behaviour. - pub(crate) fn consume_keyed_nonces(&mut self, sender: Address) -> Result<(), VMError> { + pub(crate) fn consume_keyed_nonces(&mut self, sender: Address) -> Result { let (nonce_keys, next_seq) = match &self.tx { Transaction::FrameTransaction(ft) => ( ft.nonce_keys.clone(), @@ -1670,23 +1682,20 @@ impl<'a> VM<'a> { .checked_add(1) .ok_or(VMError::Internal(InternalError::Overflow))?, ), - _ => return Ok(()), + _ => return Ok(0), }; let nonce_manager = ethrex_common::types::frame_tx_nonce_manager(); + let mut surcharge: u64 = 0; for key in &nonce_keys { if key.is_zero() { self.increment_account_nonce(sender)?; continue; } - let mut preimage = [0u8; 64]; - preimage[12..32].copy_from_slice(sender.as_bytes()); - preimage[32..64].copy_from_slice(&key.to_big_endian()); - let slot = H256(ethrex_crypto::keccak::keccak_hash(preimage)); + let slot = keyed_nonce_slot(sender, key); let _ = self.db.get_account(nonce_manager)?; let current = self.get_storage_value(nonce_manager, slot)?; if current.is_zero() { - self.current_call_frame - .increase_consumed_gas(crate::gas_cost::KEYED_NONCE_FIRST_USE_GAS)?; + surcharge = surcharge.saturating_add(crate::gas_cost::KEYED_NONCE_FIRST_USE_GAS); } let slot_u256 = U256::from_big_endian(&slot.0); self.update_account_storage( @@ -1697,7 +1706,27 @@ impl<'a> VM<'a> { current, )?; } - Ok(()) + Ok(surcharge) + } + + /// The surcharge `consume_keyed_nonces` will charge, without consuming anything. + pub(crate) fn keyed_nonce_first_use_surcharge(&mut self) -> Result { + let (sender, nonce_keys) = match &self.tx { + Transaction::FrameTransaction(ft) => (ft.sender, ft.nonce_keys.clone()), + _ => return Ok(0), + }; + let nonce_manager = ethrex_common::types::frame_tx_nonce_manager(); + let mut surcharge: u64 = 0; + for key in &nonce_keys { + if key.is_zero() { + continue; + } + let slot = keyed_nonce_slot(sender, key); + if self.get_storage_value(nonce_manager, slot)?.is_zero() { + surcharge = surcharge.saturating_add(crate::gas_cost::KEYED_NONCE_FIRST_USE_GAS); + } + } + Ok(surcharge) } fn execute_frame_tx(&mut self) -> Result {