use crate::{
errors::{ExceptionalHalt, InternalError, OpcodeResult, VMError},
gas_cost,
memory::calculate_memory_size,
opcode_handlers::OpcodeHandler,
utils::size_offset_to_usize,
vm::VM,
};
use ethrex_common::{Address, U256, types::FrameMode, types::Log};
fn index_to_usize(val: u64) -> Result<usize, VMError> {
usize::try_from(val).map_err(|_| ExceptionalHalt::InvalidOpcode.into())
}
pub fn u256_to_offset(value: U256) -> Option<usize> {
if value.0[1] != 0 || value.0[2] != 0 || value.0[3] != 0 {
return None;
}
usize::try_from(value.0[0]).ok()
}
pub(crate) fn compute_tx_max_cost(ctx: &crate::vm::FrameTxContext) -> Result<U256, VMError> {
let gas_cost = U256::from(ctx.tx.max_fee_per_gas)
.checked_mul(U256::from(ctx.max_gas))
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let blob_cost = U256::from(ctx.tx.blob_versioned_hashes.len())
.checked_mul(U256::from(131072u64))
.ok_or(ExceptionalHalt::InvalidOpcode)?
.checked_mul(ctx.blob_base_fee)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
gas_cost
.checked_add(blob_cost)
.ok_or(ExceptionalHalt::InvalidOpcode.into())
}
pub fn apply_approve(
vm: &mut VM<'_>,
scope: u64,
frame_target: ethrex_common::Address,
) -> Result<(), VMError> {
match scope {
0x1 => {
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
if ctx.payer_address.is_some() {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
if !ctx.sender_approved {
return Err(VMError::RevertOpcode);
}
let tx_cost = compute_tx_max_cost(ctx)?;
let sender = ctx.tx.sender;
vm.increment_account_nonce(sender)?;
match vm.decrease_account_balance(frame_target, tx_cost) {
Ok(()) => {}
Err(InternalError::Underflow) => return Err(VMError::RevertOpcode),
Err(e) => return Err(VMError::Internal(e)),
}
let ctx = vm
.frame_tx_context
.as_mut()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
ctx.payer_address = Some(frame_target);
}
0x2 => {
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
if ctx.sender_approved {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
if frame_target != ctx.tx.sender {
return Err(VMError::RevertOpcode);
}
let ctx = vm
.frame_tx_context
.as_mut()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
ctx.sender_approved = true;
}
0x3 => {
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
if ctx.sender_approved || ctx.payer_address.is_some() {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
if frame_target != ctx.tx.sender {
return Err(VMError::RevertOpcode);
}
let tx_cost = compute_tx_max_cost(ctx)?;
let sender = ctx.tx.sender;
vm.increment_account_nonce(sender)?;
match vm.decrease_account_balance(frame_target, tx_cost) {
Ok(()) => {}
Err(InternalError::Underflow) => return Err(VMError::RevertOpcode),
Err(e) => return Err(VMError::Internal(e)),
}
let ctx = vm
.frame_tx_context
.as_mut()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
ctx.sender_approved = true;
ctx.payer_address = Some(frame_target);
}
_ => {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
}
Ok(())
}
pub struct OpApproveHandler;
impl OpcodeHandler for OpApproveHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [offset, length, scope] = *vm.current_call_frame.stack.pop()?;
let (length, offset) = size_offset_to_usize(length, offset)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let current_frame = ctx
.tx
.frames
.get(ctx.current_frame_index)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let frame_target = current_frame.target.unwrap_or(ctx.tx.sender);
if vm.current_call_frame.to != frame_target {
return Err(VMError::RevertOpcode);
}
let allowed_scope = current_frame.scope_restriction();
let scope_val = u64::try_from(scope).unwrap_or(u64::MAX);
if scope_val == 0 || scope_val > 3 || (scope_val & u64::from(allowed_scope)) != scope_val {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
vm.current_call_frame
.increase_consumed_gas(gas_cost::exit_opcode(
calculate_memory_size(offset, length)?,
vm.current_call_frame.memory.len(),
)?)?;
apply_approve(vm, scope_val, frame_target)?;
let ctx = vm
.frame_tx_context
.as_mut()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
ctx.approve_called_in_current_frame = true;
if length != 0 {
vm.current_call_frame.output =
vm.current_call_frame.memory.load_range(offset, length)?;
}
Ok(OpcodeResult::Halt)
}
}
pub struct OpTxParamHandler;
impl OpcodeHandler for OpTxParamHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [param_id] = *vm.current_call_frame.stack.pop()?;
vm.current_call_frame
.increase_consumed_gas(gas_cost::TXPARAM)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let param_id = u64::try_from(param_id).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let result = load_tx_param(ctx, param_id)?;
vm.current_call_frame.stack.push(result)?;
Ok(OpcodeResult::Continue)
}
}
pub struct OpFrameDataLoadHandler;
impl OpcodeHandler for OpFrameDataLoadHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [offset, frame_index] = *vm.current_call_frame.stack.pop()?;
vm.current_call_frame
.increase_consumed_gas(gas_cost::FRAMEDATALOAD)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let idx = index_to_usize(frame_index)?;
let frame = ctx
.tx
.frames
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let mut word = [0u8; 32];
if let Some(byte_offset) = u256_to_offset(offset) {
let data = &frame.data;
let available = data.len().saturating_sub(byte_offset);
let copy_len = available.min(32);
if copy_len > 0
&& let Some(src) = data.get(byte_offset..byte_offset.saturating_add(copy_len))
{
if let Some(dst) = word.get_mut(..copy_len) {
dst.copy_from_slice(src);
}
}
}
vm.current_call_frame
.stack
.push(U256::from_big_endian(&word))?;
Ok(OpcodeResult::Continue)
}
}
pub struct OpFrameDataCopyHandler;
impl OpcodeHandler for OpFrameDataCopyHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [mem_offset, data_offset, length, frame_index] = *vm.current_call_frame.stack.pop()?;
let (length, mem_offset) = size_offset_to_usize(length, mem_offset)?;
let data_offset_opt = u256_to_offset(data_offset);
let new_memory_size = calculate_memory_size(mem_offset, length)?;
let current_memory_size = vm.current_call_frame.memory.len();
vm.current_call_frame
.increase_consumed_gas(gas_cost::framedatacopy(
new_memory_size,
current_memory_size,
length,
)?)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let idx = index_to_usize(frame_index)?;
let frame = ctx
.tx
.frames
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
if length == 0 {
return Ok(OpcodeResult::Continue);
}
let data = &frame.data;
let mut buf = vec![0u8; length];
if let Some(data_offset) = data_offset_opt {
let available = data.len().saturating_sub(data_offset);
let copy_len = length.min(available);
if let (Some(dst), Some(src)) = (
buf.get_mut(..copy_len),
data.get(data_offset..data_offset.saturating_add(copy_len)),
) {
dst.copy_from_slice(src);
}
}
vm.current_call_frame.memory.store_data(mem_offset, &buf)?;
Ok(OpcodeResult::Continue)
}
}
pub struct OpFrameParamHandler;
impl OpcodeHandler for OpFrameParamHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [frame_index, param_id] = *vm.current_call_frame.stack.pop()?;
vm.current_call_frame
.increase_consumed_gas(gas_cost::FRAMEPARAM)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let frame_index = u64::try_from(frame_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let idx = index_to_usize(frame_index)?;
let frame = ctx
.tx
.frames
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let param_id = u64::try_from(param_id).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let result: U256 = match param_id {
0x00 => {
address_to_u256(frame.target.unwrap_or(ctx.tx.sender))
}
0x01 => {
U256::from(frame.gas_limit)
}
0x02 => {
U256::from(frame.mode)
}
0x03 => {
U256::from(frame.flags)
}
0x04 => {
U256::from(frame.data.len())
}
0x05 => {
if idx >= ctx.current_frame_index {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
let (status, _, _) = ctx
.frame_results
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
U256::from(*status)
}
0x06 => {
U256::from(frame.scope_restriction())
}
0x07 => {
U256::from(u8::from(frame.is_atomic_batch()))
}
0x08 => {
frame.value
}
_ => return Err(ExceptionalHalt::InvalidOpcode.into()),
};
vm.current_call_frame.stack.push(result)?;
Ok(OpcodeResult::Continue)
}
}
pub struct OpSigParamHandler;
impl OpcodeHandler for OpSigParamHandler {
#[inline(always)]
fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
let [signature_index, param] = *vm.current_call_frame.stack.pop()?;
let param = u64::try_from(param).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let signature_index =
u64::try_from(signature_index).map_err(|_| ExceptionalHalt::InvalidOpcode)?;
let idx = index_to_usize(signature_index)?;
if param == 0x04 {
let [mem_offset, data_offset, length] = *vm.current_call_frame.stack.pop()?;
let (length, mem_offset) = size_offset_to_usize(length, mem_offset)?;
let data_offset_opt = u256_to_offset(data_offset);
let new_memory_size = calculate_memory_size(mem_offset, length)?;
let current_memory_size = vm.current_call_frame.memory.len();
vm.current_call_frame
.increase_consumed_gas(gas_cost::framedatacopy(
new_memory_size,
current_memory_size,
length,
)?)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let sig = ctx
.tx
.signatures
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
if sig.scheme != ethrex_common::types::FRAME_SIG_SCHEME_ARBITRARY {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
if length == 0 {
return Ok(OpcodeResult::Continue);
}
let data = &sig.signature;
let mut buf = vec![0u8; length];
if let Some(data_offset) = data_offset_opt {
let available = data.len().saturating_sub(data_offset);
let copy_len = length.min(available);
if let (Some(dst), Some(src)) = (
buf.get_mut(..copy_len),
data.get(data_offset..data_offset.saturating_add(copy_len)),
) {
dst.copy_from_slice(src);
}
}
vm.current_call_frame.memory.store_data(mem_offset, &buf)?;
return Ok(OpcodeResult::Continue);
}
vm.current_call_frame
.increase_consumed_gas(gas_cost::SIGPARAM)?;
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let sig = ctx
.tx
.signatures
.get(idx)
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let result = match param {
0x00 => {
if sig.scheme == ethrex_common::types::FRAME_SIG_SCHEME_ARBITRARY {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
address_to_u256(sig.signer.unwrap_or(ctx.tx.sender))
}
0x01 => U256::from(sig.scheme),
0x02 => {
if sig.msg.is_empty() {
U256::zero()
} else {
U256::from_big_endian(&sig.msg)
}
}
0x03 => U256::from(sig.signature.len()),
_ => return Err(ExceptionalHalt::InvalidOpcode.into()),
};
vm.current_call_frame.stack.push(result)?;
Ok(OpcodeResult::Continue)
}
}
pub fn load_tx_param(ctx: &crate::vm::FrameTxContext, param_id: u64) -> Result<U256, VMError> {
match param_id {
0x00 => Ok(U256::from(0x06u8)), 0x01 => Ok(U256::from(ctx.tx.nonce)),
0x02 => Ok(address_to_u256(ctx.tx.sender)),
0x03 => Ok(U256::from(ctx.tx.max_priority_fee_per_gas)),
0x04 => Ok(U256::from(ctx.tx.max_fee_per_gas)),
0x05 => Ok(ctx.tx.max_fee_per_blob_gas),
0x06 => compute_tx_max_cost(ctx),
0x07 => Ok(U256::from(ctx.tx.blob_versioned_hashes.len())),
0x08 => {
let mut bytes = [0u8; 32];
bytes.copy_from_slice(ctx.sig_hash.as_bytes());
Ok(U256::from_big_endian(&bytes))
}
0x09 => Ok(U256::from(ctx.tx.frames.len())),
0x0A => Ok(U256::from(ctx.current_frame_index)),
0x0B => Ok(U256::from(ctx.tx.signatures.len())),
_ => Err(ExceptionalHalt::InvalidOpcode.into()),
}
}
pub fn address_to_u256(addr: ethrex_common::Address) -> U256 {
let mut bytes = [0u8; 32];
bytes[12..].copy_from_slice(addr.as_bytes());
U256::from_big_endian(&bytes)
}
pub fn execute_default_code(
vm: &mut VM<'_>,
frame: ðrex_common::types::Frame,
target: Address,
) -> Result<(bool, u64, Vec<Log>), VMError> {
match frame.execution_mode() {
FrameMode::Verify => execute_default_verify(vm, frame, target),
FrameMode::Sender | FrameMode::Default => Ok((true, 0, Vec::new())),
}
}
fn execute_default_verify(
vm: &mut VM<'_>,
frame: ðrex_common::types::Frame,
target: Address,
) -> Result<(bool, u64, Vec<Log>), VMError> {
let ctx = vm
.frame_tx_context
.as_ref()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
let allowed_scope = u64::from(frame.scope_restriction());
if allowed_scope == 0 {
return Ok((false, 0, Vec::new()));
}
if (allowed_scope & 0x02) != 0 && target != ctx.tx.sender {
return Ok((false, 0, Vec::new()));
}
let sig_index = if (allowed_scope & 0x02) != 0 { 0 } else { 1 };
let sender_sig_ok = ctx.tx.signatures.get(sig_index).is_some_and(|s| {
s.scheme == ethrex_common::types::FRAME_SIG_SCHEME_SECP256K1
&& s.msg.is_empty()
&& s.signer.unwrap_or(ctx.tx.sender) == target
});
if !sender_sig_ok {
return Ok((false, 0, Vec::new()));
}
apply_approve(vm, allowed_scope, target)?;
let ctx = vm
.frame_tx_context
.as_mut()
.ok_or(ExceptionalHalt::InvalidOpcode)?;
ctx.approve_called_in_current_frame = true;
Ok((true, 0, Vec::new()))
}
#[cfg(test)]
mod max_cost_tests {
use super::{compute_tx_max_cost, load_tx_param};
use crate::vm::FrameTxContext;
use ethrex_common::{H256, U256, types::FrameTransaction};
fn ctx(max_fee: u64, blobs: usize, blob_base_fee: u64, max_gas: u64) -> FrameTxContext {
let tx = FrameTransaction {
max_fee_per_gas: max_fee,
max_fee_per_blob_gas: U256::from(blob_base_fee).saturating_mul(U256::from(1_000u64)),
blob_versioned_hashes: vec![H256::zero(); blobs],
..Default::default()
};
FrameTxContext {
sender_approved: false,
payer_address: None,
frame_results: Vec::new(),
current_frame_index: 0,
sig_hash: H256::zero(),
tx,
approve_called_in_current_frame: false,
max_gas,
blob_base_fee: U256::from(blob_base_fee),
}
}
#[test]
fn max_cost_is_max_fee_times_limit_plus_base_rate_blob_cost() {
let c = ctx(10, 2, 5, 100_000);
assert_eq!(compute_tx_max_cost(&c).unwrap(), U256::from(2_310_720u64));
let c = ctx(7, 0, 999, 21_000);
assert_eq!(compute_tx_max_cost(&c).unwrap(), U256::from(147_000u64));
}
#[test]
fn txparam_0x06_reports_the_same_maximum_cost_approve_debits() {
let c = ctx(10, 2, 5, 100_000);
assert_eq!(
load_tx_param(&c, 0x06).unwrap(),
compute_tx_max_cost(&c).unwrap()
);
}
}